[13780] | 1 | #region License Information
|
---|
| 2 | /* HeuristicLab
|
---|
| 3 | * Copyright (C) 2002-2016 Heuristic and Evolutionary Algorithms Laboratory (HEAL)
|
---|
| 4 | *
|
---|
| 5 | * This file is part of HeuristicLab.
|
---|
| 6 | *
|
---|
| 7 | * HeuristicLab is free software: you can redistribute it and/or modify
|
---|
| 8 | * it under the terms of the GNU General Public License as published by
|
---|
| 9 | * the Free Software Foundation, either version 3 of the License, or
|
---|
| 10 | * (at your option) any later version.
|
---|
| 11 | *
|
---|
| 12 | * HeuristicLab is distributed in the hope that it will be useful,
|
---|
| 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
---|
| 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
---|
| 15 | * GNU General Public License for more details.
|
---|
| 16 | *
|
---|
| 17 | * You should have received a copy of the GNU General Public License
|
---|
| 18 | * along with HeuristicLab. If not, see <http://www.gnu.org/licenses/>.
|
---|
| 19 | */
|
---|
| 20 | #endregion
|
---|
| 21 |
|
---|
| 22 | using System;
|
---|
| 23 | using System.Collections.Generic;
|
---|
| 24 | using System.Drawing;
|
---|
[13817] | 25 | using System.Globalization;
|
---|
[13780] | 26 | using System.Linq;
|
---|
| 27 | using System.Windows.Forms;
|
---|
| 28 | using System.Windows.Forms.DataVisualization.Charting;
|
---|
| 29 | using HeuristicLab.Common;
|
---|
| 30 | using HeuristicLab.Visualization.ChartControlsExtensions;
|
---|
| 31 |
|
---|
| 32 | namespace HeuristicLab.Problems.DataAnalysis.Views {
|
---|
| 33 | public partial class GradientChart : EnhancedChart {
|
---|
[13820] | 34 | private ModifiableDataset dataset;
|
---|
[13780] | 35 |
|
---|
[13817] | 36 | public bool ShowLegend { get; set; }
|
---|
[13820] | 37 | public bool ShowXAxisLabel { get; set; }
|
---|
| 38 | public bool ShowYAxisLabel { get; set; }
|
---|
| 39 | public bool ShowCursor { get; set; }
|
---|
[13817] | 40 |
|
---|
[13780] | 41 | private bool useMedianValues;
|
---|
| 42 | public bool UseMedianValues {
|
---|
| 43 | get { return useMedianValues; }
|
---|
| 44 | set {
|
---|
| 45 | if (value == useMedianValues) return;
|
---|
| 46 | useMedianValues = value;
|
---|
| 47 | OnChartPropertyChanged(this, EventArgs.Empty);
|
---|
| 48 | UpdateChart();
|
---|
| 49 | }
|
---|
| 50 | }
|
---|
| 51 |
|
---|
| 52 | private int row;
|
---|
| 53 | public int Row {
|
---|
| 54 | get { return row; }
|
---|
| 55 | set {
|
---|
| 56 | if (row == value) return;
|
---|
| 57 | row = value;
|
---|
| 58 | OnChartPropertyChanged(this, EventArgs.Empty);
|
---|
| 59 | UpdateChart();
|
---|
| 60 | }
|
---|
| 61 | }
|
---|
| 62 |
|
---|
| 63 | private double min;
|
---|
| 64 | public double Min {
|
---|
| 65 | get { return min; }
|
---|
| 66 | set {
|
---|
| 67 | if (value.IsAlmost(min)) return;
|
---|
| 68 | min = value;
|
---|
| 69 | OnChartPropertyChanged(this, EventArgs.Empty);
|
---|
| 70 | UpdateChart();
|
---|
| 71 | }
|
---|
| 72 | }
|
---|
| 73 |
|
---|
| 74 | private double max;
|
---|
| 75 | public double Max {
|
---|
| 76 | get { return max; }
|
---|
| 77 | set {
|
---|
| 78 | if (value.IsAlmost(max)) return;
|
---|
| 79 | max = value;
|
---|
| 80 | OnChartPropertyChanged(this, EventArgs.Empty);
|
---|
| 81 | UpdateChart();
|
---|
| 82 | }
|
---|
| 83 | }
|
---|
| 84 |
|
---|
| 85 | private int points;
|
---|
| 86 | public int Points {
|
---|
| 87 | get { return points; }
|
---|
| 88 | set {
|
---|
| 89 | if (value == points) return;
|
---|
| 90 | points = value;
|
---|
| 91 | OnChartPropertyChanged(this, EventArgs.Empty);
|
---|
| 92 | UpdateChart();
|
---|
| 93 | }
|
---|
| 94 | }
|
---|
| 95 |
|
---|
| 96 | private IRegressionProblemData problemData;
|
---|
| 97 | public IRegressionProblemData ProblemData {
|
---|
| 98 | get { return problemData; }
|
---|
| 99 | set {
|
---|
| 100 | if (!SolutionsCompatibleWithProblemData(value, solutionList))
|
---|
| 101 | throw new ArgumentException("The problem data provided does not contain all the variables required by the solutions.");
|
---|
| 102 | problemData = value;
|
---|
[13820] | 103 | UpdateDataset();
|
---|
[13780] | 104 | UpdateChart();
|
---|
| 105 | }
|
---|
| 106 | }
|
---|
| 107 |
|
---|
| 108 | public string Target {
|
---|
| 109 | get { return Solutions.First().ProblemData.TargetVariable; }
|
---|
| 110 | }
|
---|
| 111 |
|
---|
| 112 | private string variable;
|
---|
| 113 | public string Variable {
|
---|
| 114 | get { return variable; }
|
---|
| 115 | set {
|
---|
| 116 | if (variable == value) return;
|
---|
| 117 | if (!ProblemData.Dataset.DoubleVariables.Contains(value))
|
---|
| 118 | throw new ArgumentException("The variable must be present in the problem dataset.");
|
---|
| 119 | OnChartPropertyChanged(this, EventArgs.Empty);
|
---|
| 120 | variable = value;
|
---|
| 121 | var values = ProblemData.Dataset.GetReadOnlyDoubleValues(variable);
|
---|
| 122 | min = values.Min();
|
---|
| 123 | max = values.Max();
|
---|
| 124 | UpdateChart();
|
---|
| 125 | }
|
---|
| 126 | }
|
---|
| 127 |
|
---|
| 128 | private List<IRegressionSolution> solutionList;
|
---|
| 129 | public IEnumerable<IRegressionSolution> Solutions {
|
---|
| 130 | get { return solutionList; }
|
---|
| 131 | set {
|
---|
| 132 | if (!value.Any())
|
---|
| 133 | throw new ArgumentException("At least one solution must be provided.");
|
---|
| 134 | if (SolutionsCompatibleWithProblemData(problemData, value))
|
---|
| 135 | solutionList = new List<IRegressionSolution>(value);
|
---|
| 136 | else
|
---|
| 137 | throw new ArgumentException("The provided solution collection is not compatible with the existing problem data.");
|
---|
| 138 | UpdateChart();
|
---|
| 139 | }
|
---|
| 140 | }
|
---|
| 141 |
|
---|
| 142 | public VerticalLineAnnotation VerticalLineAnnotation {
|
---|
| 143 | get { return (VerticalLineAnnotation)Annotations.SingleOrDefault(x => x is VerticalLineAnnotation); }
|
---|
| 144 | }
|
---|
| 145 |
|
---|
| 146 | public GradientChart() {
|
---|
| 147 | InitializeComponent();
|
---|
| 148 | RegisterEvents();
|
---|
| 149 | }
|
---|
| 150 |
|
---|
| 151 | public void AddSolution(IRegressionSolution solution) {
|
---|
| 152 | if (!SolutionsCompatibleWithProblemData(problemData, new[] { solution })) {
|
---|
| 153 | throw new ArgumentException("The solution is not compatible with the problem data.");
|
---|
| 154 | }
|
---|
| 155 | solutionList.Add(solution);
|
---|
| 156 | UpdateChart();
|
---|
| 157 | }
|
---|
| 158 |
|
---|
| 159 | public void RemoveSolution(IRegressionSolution solution) {
|
---|
| 160 | var removed = solutionList.RemoveAll(x => x == solution);
|
---|
| 161 | if (removed > 0)
|
---|
| 162 | UpdateChart();
|
---|
| 163 | }
|
---|
| 164 |
|
---|
| 165 | private static bool SolutionsCompatibleWithProblemData(IRegressionProblemData pd, IEnumerable<IRegressionSolution> solutions) {
|
---|
| 166 | if (pd == null || !solutions.Any()) return true;
|
---|
| 167 | if (solutions.Any(x => x.ProblemData.TargetVariable != pd.TargetVariable)) return false;
|
---|
| 168 | var variables = new HashSet<string>(pd.Dataset.DoubleVariables);
|
---|
| 169 | return solutions.SelectMany(x => x.ProblemData.Dataset.DoubleVariables).All(variables.Contains);
|
---|
| 170 | }
|
---|
| 171 |
|
---|
| 172 |
|
---|
| 173 | public void Configure(IEnumerable<IRegressionSolution> solutions, IRegressionProblemData pd, double min, double max, int points) {
|
---|
| 174 | if (!SolutionsCompatibleWithProblemData(pd, solutions))
|
---|
| 175 | throw new ArgumentException("Solutions are not compatible with the problem data.");
|
---|
| 176 | this.solutionList = new List<IRegressionSolution>(solutions);
|
---|
| 177 | this.problemData = pd;
|
---|
| 178 | this.variable = pd.Dataset.DoubleVariables.First();
|
---|
| 179 | this.min = min;
|
---|
| 180 | this.max = max;
|
---|
| 181 | this.points = points;
|
---|
| 182 |
|
---|
[13820] | 183 | UpdateDataset();
|
---|
[13780] | 184 | UpdateChart();
|
---|
| 185 | }
|
---|
| 186 |
|
---|
[13808] | 187 | public void Configure(IEnumerable<IRegressionSolution> solutions, IRegressionProblemData pd, ModifiableDataset dataset, string variable, double min, double max, int points) {
|
---|
| 188 | if (!SolutionsCompatibleWithProblemData(pd, solutions))
|
---|
| 189 | throw new ArgumentException("Solutions are not compatible with the problem data.");
|
---|
| 190 | this.solutionList = new List<IRegressionSolution>(solutions);
|
---|
| 191 | this.problemData = pd;
|
---|
| 192 | this.variable = variable;
|
---|
[13820] | 193 | this.dataset = dataset;
|
---|
[13808] | 194 | this.min = min;
|
---|
| 195 | this.max = max;
|
---|
| 196 | this.points = points;
|
---|
| 197 | }
|
---|
| 198 |
|
---|
| 199 | public void UpdateChart() {
|
---|
[13780] | 200 | // throw exceptions?
|
---|
[13820] | 201 | if (dataset == null || solutionList == null || !solutionList.Any())
|
---|
[13780] | 202 | return;
|
---|
[13818] | 203 | if (min.IsAlmost(max) || min > max || points == 0)
|
---|
[13780] | 204 | return;
|
---|
| 205 | Series.Clear();
|
---|
[13820] | 206 |
|
---|
| 207 | var defaultValue = dataset.GetDoubleValue(variable, 0);
|
---|
[13825] | 208 | var vla = VerticalLineAnnotation;
|
---|
| 209 | vla.X = defaultValue;
|
---|
| 210 | vla.Visible = ShowCursor;
|
---|
| 211 | Annotations.Clear();
|
---|
| 212 | Annotations.Add(vla);
|
---|
[13820] | 213 |
|
---|
[13818] | 214 | double axisMin, axisMax, axisInterval;
|
---|
| 215 | // calculate X-axis interval
|
---|
| 216 | ChartUtil.CalculateAxisInterval(min, max, 5, out axisMin, out axisMax, out axisInterval);
|
---|
| 217 | var axis = ChartAreas[0].AxisX;
|
---|
| 218 | axis.Minimum = axisMin;
|
---|
| 219 | axis.Maximum = axisMax;
|
---|
| 220 | axis.Interval = axisInterval;
|
---|
| 221 |
|
---|
[13780] | 222 | for (int i = 0; i < solutionList.Count; ++i) {
|
---|
| 223 | var solution = solutionList[i];
|
---|
| 224 | var series = PlotSeries(solution);
|
---|
| 225 | series.Name = Target + " " + i;
|
---|
| 226 | Series.Add(series);
|
---|
| 227 | }
|
---|
[13818] | 228 | // calculate Y-axis interval
|
---|
| 229 | double ymin = 0, ymax = 0;
|
---|
| 230 | foreach (var v in Series[0].Points.Select(x => x.YValues[0])) {
|
---|
| 231 | if (ymin > v) ymin = v;
|
---|
| 232 | if (ymax < v) ymax = v;
|
---|
| 233 | }
|
---|
| 234 | ChartUtil.CalculateAxisInterval(ymin, ymax, 5, out axisMin, out axisMax, out axisInterval);
|
---|
| 235 | axis = ChartAreas[0].AxisY;
|
---|
| 236 | axis.Minimum = axisMin;
|
---|
| 237 | axis.Maximum = axisMax;
|
---|
| 238 | axis.Interval = axisInterval;
|
---|
[13820] | 239 |
|
---|
| 240 | if (ShowXAxisLabel) {
|
---|
| 241 | ChartAreas[0].AxisX.Title = Variable + " : " + defaultValue.ToString("N3", CultureInfo.CurrentCulture); // set axis title
|
---|
| 242 | }
|
---|
| 243 |
|
---|
[13818] | 244 | AddStripLines(); // add strip lines
|
---|
[13817] | 245 | if (ShowLegend)
|
---|
| 246 | AddLegends();
|
---|
[13780] | 247 | }
|
---|
| 248 |
|
---|
[13820] | 249 | private void UpdateDataset() {
|
---|
[13780] | 250 | var variables = ProblemData.Dataset.DoubleVariables.ToList();
|
---|
| 251 | var variableValues = new List<double>[variables.Count];
|
---|
| 252 |
|
---|
| 253 | if (UseMedianValues) {
|
---|
| 254 | for (int i = 0; i < variables.Count; ++i) {
|
---|
| 255 | var median = ProblemData.Dataset.GetDoubleValues(variables[i], ProblemData.TrainingIndices).Median();
|
---|
| 256 | variableValues[i] = new List<double> { median };
|
---|
| 257 | }
|
---|
| 258 | } else {
|
---|
| 259 | for (int i = 0; i < variables.Count; ++i) {
|
---|
| 260 | var variableValue = ProblemData.Dataset.GetDoubleValue(variables[i], Row);
|
---|
| 261 | variableValues[i] = new List<double> { variableValue };
|
---|
| 262 | }
|
---|
| 263 | }
|
---|
[13820] | 264 | dataset = new ModifiableDataset(variables, variableValues);
|
---|
[13780] | 265 | }
|
---|
| 266 |
|
---|
| 267 | private double GetEstimatedValue(IRegressionSolution solution, double x) {
|
---|
[13820] | 268 | var v = dataset.GetDoubleValue(Variable, 0);
|
---|
| 269 | dataset.SetVariableValue(x, Variable, 0);
|
---|
| 270 | var y = solution.Model.GetEstimatedValues(dataset, new[] { 0 }).Single();
|
---|
| 271 | dataset.SetVariableValue(v, Variable, 0);
|
---|
[13780] | 272 | return y;
|
---|
| 273 | }
|
---|
| 274 |
|
---|
| 275 | private Series PlotSeries(IRegressionSolution solution) {
|
---|
[13820] | 276 | var v = dataset.GetDoubleValue(variable, 0);
|
---|
[13780] | 277 | var series = new Series { ChartType = SeriesChartType.Point };
|
---|
| 278 |
|
---|
| 279 | var step = (max - min) / points;
|
---|
| 280 | var axisX = ChartAreas[0].AxisX;
|
---|
[13820] | 281 | if (ShowXAxisLabel) {
|
---|
| 282 | axisX.Title = Variable + " : " + v.ToString("N3", CultureInfo.CurrentCulture);
|
---|
| 283 | }
|
---|
[13780] | 284 | var axisY = ChartAreas[0].AxisY;
|
---|
[13820] | 285 | if (ShowYAxisLabel) { axisY.Title = Target; }
|
---|
[13780] | 286 | double y;
|
---|
| 287 | // lefthand section outside of the training range
|
---|
| 288 | for (double x = axisX.Minimum; x < min; x += step) {
|
---|
| 289 | y = GetEstimatedValue(solution, x);
|
---|
| 290 | series.Points.Add(new DataPoint(x, y) { MarkerSize = 2, MarkerColor = Color.Orange });
|
---|
| 291 | }
|
---|
| 292 | // points in the trainig range
|
---|
| 293 | for (double x = min; x < max; x += step) {
|
---|
| 294 | y = GetEstimatedValue(solution, x);
|
---|
| 295 | series.Points.Add(new DataPoint(x, y) { MarkerSize = 2 });
|
---|
| 296 | }
|
---|
| 297 | // righthand section outside of the training range
|
---|
| 298 | for (double x = max; x < axisX.Maximum; x += step) {
|
---|
| 299 | y = GetEstimatedValue(solution, x);
|
---|
| 300 | series.Points.Add(new DataPoint(x, y) { MarkerSize = 2, MarkerColor = Color.Orange });
|
---|
| 301 | }
|
---|
| 302 |
|
---|
[13820] | 303 | if (ShowCursor) {
|
---|
| 304 | y = GetEstimatedValue(solution, v);
|
---|
| 305 | series.Points.Add(new DataPoint(v, y) { MarkerSize = 5, MarkerColor = Color.Red });
|
---|
| 306 | }
|
---|
| 307 | if (ShowLegend) {
|
---|
| 308 | series.IsVisibleInLegend = true;
|
---|
| 309 | }
|
---|
[13780] | 310 |
|
---|
| 311 | return series;
|
---|
| 312 | }
|
---|
| 313 |
|
---|
| 314 | private void AddLegends() {
|
---|
| 315 | Legends.Clear();
|
---|
| 316 | var legend = new Legend();
|
---|
| 317 | legend.Alignment = StringAlignment.Center;
|
---|
| 318 | legend.LegendStyle = LegendStyle.Row;
|
---|
| 319 | legend.Docking = Docking.Top;
|
---|
| 320 | Legends.Add(legend);
|
---|
| 321 | foreach (var s in Series) {
|
---|
| 322 | s.Legend = legend.Name;
|
---|
| 323 | }
|
---|
| 324 | }
|
---|
| 325 |
|
---|
| 326 | private void AddStripLines() {
|
---|
| 327 | var axisX = ChartAreas[0].AxisX;
|
---|
| 328 | axisX.StripLines.Clear();
|
---|
| 329 | axisX.StripLines.Add(new StripLine { BackColor = Color.FromArgb(30, Color.Green), IntervalOffset = axisX.Minimum, StripWidth = min - axisX.Minimum });
|
---|
| 330 | axisX.StripLines.Add(new StripLine { BackColor = Color.FromArgb(30, Color.Green), IntervalOffset = max, StripWidth = axisX.Maximum - max });
|
---|
| 331 | }
|
---|
| 332 |
|
---|
| 333 | private void RegisterEvents() {
|
---|
| 334 | AnnotationPositionChanging += chart_AnnotationPositionChanging;
|
---|
| 335 | MouseMove += chart_MouseMove;
|
---|
| 336 | FormatNumber += chart_FormatNumber;
|
---|
| 337 | }
|
---|
| 338 |
|
---|
| 339 | #region events
|
---|
[13817] | 340 | public event EventHandler VariableValueChanged;
|
---|
| 341 | public void OnVariableValueChanged(object sender, EventArgs args) {
|
---|
| 342 | var changed = VariableValueChanged;
|
---|
| 343 | if (changed == null) return;
|
---|
| 344 | changed(sender, args);
|
---|
| 345 | }
|
---|
| 346 |
|
---|
[13780] | 347 | public event EventHandler ChartPropertyChanged;
|
---|
| 348 | public void OnChartPropertyChanged(object sender, EventArgs args) {
|
---|
| 349 | var changed = ChartPropertyChanged;
|
---|
| 350 | if (changed == null) return;
|
---|
| 351 | changed(sender, args);
|
---|
| 352 | }
|
---|
| 353 |
|
---|
[13818] | 354 | private void chart_AnnotationPositionChanged(object sender, EventArgs e) {
|
---|
| 355 | var annotation = VerticalLineAnnotation;
|
---|
| 356 | var x = annotation.X;
|
---|
[13820] | 357 | dataset.SetVariableValue(x, Variable, 0);
|
---|
[13780] | 358 | for (int i = 0; i < solutionList.Count; ++i) {
|
---|
| 359 | var y = GetEstimatedValue(solutionList[i], x);
|
---|
| 360 | var s = Series[i];
|
---|
| 361 | var n = s.Points.Count;
|
---|
| 362 | s.Points[n - 1] = new DataPoint(x, y) { MarkerColor = Color.Red, MarkerSize = 5 };
|
---|
| 363 | }
|
---|
[13820] | 364 | if (ShowXAxisLabel) {
|
---|
| 365 | ChartAreas[0].AxisX.Title = Variable + " : " + x.ToString("N3", CultureInfo.CurrentCulture);
|
---|
| 366 | }
|
---|
[13780] | 367 | Update();
|
---|
[13817] | 368 | OnVariableValueChanged(this, EventArgs.Empty);
|
---|
[13780] | 369 | }
|
---|
| 370 |
|
---|
[13818] | 371 | private void chart_AnnotationPositionChanging(object sender, AnnotationPositionChangingEventArgs e) {
|
---|
| 372 | var step = (max - min) / points;
|
---|
| 373 | e.NewLocationX = step * (long)Math.Round(e.NewLocationX / step);
|
---|
| 374 | var axisX = ChartAreas[0].AxisX;
|
---|
| 375 | if (e.NewLocationX > axisX.Maximum)
|
---|
| 376 | e.NewLocationX = axisX.Maximum;
|
---|
| 377 | if (e.NewLocationX < axisX.Minimum)
|
---|
| 378 | e.NewLocationX = axisX.Minimum;
|
---|
| 379 | }
|
---|
| 380 |
|
---|
[13780] | 381 | private void chart_MouseMove(object sender, MouseEventArgs e) {
|
---|
| 382 | this.Cursor = HitTest(e.X, e.Y).ChartElementType == ChartElementType.Annotation ? Cursors.VSplit : Cursors.Default;
|
---|
| 383 | }
|
---|
| 384 |
|
---|
| 385 | private void chart_FormatNumber(object sender, FormatNumberEventArgs e) {
|
---|
| 386 | if (e.ElementType == ChartElementType.AxisLabels) {
|
---|
| 387 | switch (e.Format) {
|
---|
| 388 | case "CustomAxisXFormat":
|
---|
| 389 | break;
|
---|
| 390 | case "CustomAxisYFormat":
|
---|
| 391 | var v = e.Value;
|
---|
| 392 | e.LocalizedValue = string.Format("{0,5}", v);
|
---|
| 393 | break;
|
---|
| 394 | default:
|
---|
| 395 | break;
|
---|
| 396 | }
|
---|
| 397 | }
|
---|
| 398 | }
|
---|
| 399 |
|
---|
| 400 | private void GradientChart_DragDrop(object sender, DragEventArgs e) {
|
---|
| 401 | var data = e.Data.GetData(HeuristicLab.Common.Constants.DragDropDataFormat);
|
---|
| 402 | if (data != null) {
|
---|
| 403 | var solution = data as IRegressionSolution;
|
---|
| 404 | if (!Solutions.Contains(solution))
|
---|
| 405 | AddSolution(solution);
|
---|
| 406 | }
|
---|
| 407 | }
|
---|
| 408 |
|
---|
| 409 | private void GradientChart_DragEnter(object sender, DragEventArgs e) {
|
---|
| 410 | if (!e.Data.GetDataPresent(HeuristicLab.Common.Constants.DragDropDataFormat)) return;
|
---|
| 411 | e.Effect = DragDropEffects.None;
|
---|
| 412 |
|
---|
| 413 | var data = e.Data.GetData(HeuristicLab.Common.Constants.DragDropDataFormat);
|
---|
| 414 | var regressionSolution = data as IRegressionSolution;
|
---|
| 415 | if (regressionSolution != null) {
|
---|
| 416 | e.Effect = DragDropEffects.Copy;
|
---|
| 417 | }
|
---|
| 418 | }
|
---|
[13817] | 419 | #endregion
|
---|
[13780] | 420 | }
|
---|
| 421 | }
|
---|