Free cookie consent management tool by TermsFeed Policy Generator

source: branches/HeuristicLab.RegressionSolutionGradientView/HeuristicLab.Problems.DataAnalysis.Views/3.4/GradientChart.cs @ 13842

Last change on this file since 13842 was 13842, checked in by pfleck, 8 years ago

#2597

  • Reduced memory consumption greatly by reusing existing datapoints from existing series instead of creating new series on update.
  • Rearranged methods and properties in GradientChart.
  • Added properties to set fixed axis limits instead of calculation.
File size: 20.9 KB
Line 
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
22using System;
23using System.Collections.Generic;
24using System.Drawing;
25using System.Globalization;
26using System.Linq;
27using System.Threading;
28using System.Threading.Tasks;
29using System.Windows.Forms;
30using System.Windows.Forms.DataVisualization.Charting;
31using HeuristicLab.Common;
32using HeuristicLab.MainForm.WindowsForms;
33using HeuristicLab.Visualization.ChartControlsExtensions;
34
35namespace HeuristicLab.Problems.DataAnalysis.Views {
36  public partial class GradientChart : UserControl {
37    private ModifiableDataset sharedFixedVariables; // used for syncronising variable values between charts
38    private ModifiableDataset internalDataset; // holds the x values for each point drawn
39
40    private CancellationTokenSource cancelCurrentRecalculateSource;
41
42    private readonly List<IRegressionSolution> solutions;
43    private readonly Dictionary<IRegressionSolution, Series> seriesCache;
44    private readonly Dictionary<IRegressionSolution, Series> ciSeriesCache;
45
46    #region Properties
47    public bool ShowLegend {
48      get { return chart.Legends[0].Enabled; }
49      set { chart.Legends[0].Enabled = value; }
50    }
51    public bool ShowXAxisLabel {
52      get { return chart.ChartAreas[0].AxisX.Enabled == AxisEnabled.True; }
53      set { chart.ChartAreas[0].AxisX.Enabled = value ? AxisEnabled.True : AxisEnabled.False; }
54    }
55    public bool ShowYAxisLabel {
56      get { return chart.ChartAreas[0].AxisY.Enabled == AxisEnabled.True; }
57      set { chart.ChartAreas[0].AxisY.Enabled = value ? AxisEnabled.True : AxisEnabled.False; }
58    }
59    public bool ShowCursor {
60      get { return chart.Annotations[0].Visible; }
61      set { chart.Annotations[0].Visible = value; }
62    }
63
64    private int xAxisTicks = 5;
65    public int XAxisTicks {
66      get { return xAxisTicks; }
67      set { xAxisTicks = value; }
68    }
69    private double? fixedXAxisMin;
70    public double? FixedXAxisMin {
71      get { return fixedXAxisMin; }
72      set {
73        if ((value.HasValue && fixedXAxisMin.HasValue && !value.Value.IsAlmost(fixedXAxisMin.Value)) || (value.HasValue != fixedXAxisMin.HasValue)) {
74          fixedXAxisMin = value;
75          RecalculateInternalDataset();
76        }
77      }
78    }
79    private double? fixedXAxisMax;
80    public double? FixedXAxisMax {
81      get { return fixedXAxisMax; }
82      set {
83        if ((value.HasValue && fixedXAxisMax.HasValue && !value.Value.IsAlmost(fixedXAxisMax.Value)) || (value.HasValue != fixedXAxisMax.HasValue)) {
84          fixedXAxisMax = value;
85          RecalculateInternalDataset();
86        }
87      }
88    }
89
90    private int yAxisTicks = 5;
91    public int YAxisTicks {
92      get { return yAxisTicks; }
93      set { yAxisTicks = value; }
94    }
95    private double? fixedYAxisMin;
96    public double? FixedYAxisMin {
97      get { return fixedYAxisMin; }
98      set {
99        if ((value.HasValue && fixedYAxisMin.HasValue && !value.Value.IsAlmost(fixedYAxisMin.Value)) || (value.HasValue != fixedYAxisMin.HasValue)) {
100          fixedYAxisMin = value;
101        }
102      }
103    }
104    private double? fixedYAxisMax;
105    public double? FixedYAxisMax {
106      get { return fixedYAxisMax; }
107      set {
108        if ((value.HasValue && fixedYAxisMax.HasValue && !value.Value.IsAlmost(fixedYAxisMax.Value)) || (value.HasValue != fixedYAxisMax.HasValue)) {
109          fixedYAxisMax = value;
110        }
111      }
112    }
113
114    private double trainingMin = double.MinValue;
115    public double TrainingMin {
116      get { return trainingMin; }
117      set { trainingMin = value; }
118    }
119    private double trainingMax = double.MaxValue;
120    public double TrainingMax {
121      get { return trainingMax; }
122      set { trainingMax = value; }
123    }
124
125    private int drawingSteps = 1000;
126    public int DrawingSteps {
127      get { return drawingSteps; }
128      set {
129        if (value != drawingSteps) {
130          drawingSteps = value;
131          RecalculateInternalDataset();
132          ResizeAllSeriesData();
133        }
134      }
135    }
136
137    private string freeVariable;
138    public string FreeVariable {
139      get { return freeVariable; }
140      set {
141        if (value == freeVariable) return;
142        if (solutions.Any(s => !s.ProblemData.Dataset.DoubleVariables.Contains(value))) {
143          throw new ArgumentException("Variable does not exist in the ProblemData of the Solutions.");
144        }
145        freeVariable = value;
146        RecalculateInternalDataset();
147      }
148    }
149
150    private VerticalLineAnnotation VerticalLineAnnotation {
151      get { return (VerticalLineAnnotation)chart.Annotations.SingleOrDefault(x => x is VerticalLineAnnotation); }
152    }
153    #endregion
154
155    public GradientChart() {
156      InitializeComponent();
157
158      solutions = new List<IRegressionSolution>();
159      seriesCache = new Dictionary<IRegressionSolution, Series>();
160      ciSeriesCache = new Dictionary<IRegressionSolution, Series>();
161
162      // Configure axis
163      chart.CustomizeAllChartAreas();
164      chart.ChartAreas[0].CursorX.IsUserSelectionEnabled = true;
165      chart.ChartAreas[0].AxisX.ScaleView.Zoomable = true;
166      chart.ChartAreas[0].CursorX.Interval = 0;
167
168      chart.ChartAreas[0].CursorY.IsUserSelectionEnabled = true;
169      chart.ChartAreas[0].AxisY.ScaleView.Zoomable = true;
170      chart.ChartAreas[0].CursorY.Interval = 0;
171
172      Disposed += GradientChart_Disposed;
173    }
174    private void GradientChart_Disposed(object sender, EventArgs e) {
175      if (cancelCurrentRecalculateSource != null) {
176        if (cancelCurrentRecalculateSource.IsCancellationRequested)
177          cancelCurrentRecalculateSource.Cancel();
178      }
179    }
180
181    public void Configure(IEnumerable<IRegressionSolution> solutions, ModifiableDataset sharedFixedVariables, string freeVariable, int drawingSteps, bool initializeAxisRanges = true) {
182      if (!SolutionsCompatible(solutions))
183        throw new ArgumentException("Solutions are not compatible with the problem data.");
184      this.freeVariable = freeVariable;
185      this.drawingSteps = drawingSteps;
186
187      this.solutions.Clear();
188      this.solutions.AddRange(solutions);
189
190      // add an event such that whenever a value is changed in the shared dataset,
191      // this change is reflected in the internal dataset (where the value becomes a whole column)
192      if (this.sharedFixedVariables != null)
193        this.sharedFixedVariables.ItemChanged -= sharedFixedVariables_ItemChanged;
194      this.sharedFixedVariables = sharedFixedVariables;
195      this.sharedFixedVariables.ItemChanged += sharedFixedVariables_ItemChanged;
196
197      RecalculateTrainingLimits(initializeAxisRanges);
198      RecalculateInternalDataset();
199
200      chart.Series.Clear();
201      seriesCache.Clear();
202      ciSeriesCache.Clear();
203      foreach (var solution in this.solutions) {
204        var series = CreateSeries(solution);
205        seriesCache.Add(solution, series.Item1);
206        if (series.Item2 != null)
207          ciSeriesCache.Add(solution, series.Item2);
208      }
209
210      ResizeAllSeriesData();
211      OrderAndColorSeries();
212    }
213
214    public async Task RecalculateAsync() {
215      if (IsDisposed
216        || sharedFixedVariables == null || !solutions.Any() || string.IsNullOrEmpty(freeVariable)
217        || trainingMin.IsAlmost(trainingMax) || trainingMin > trainingMax || drawingSteps == 0)
218        return;
219
220      statusLabel.Visible = true;
221      Update(); // immediately show label
222
223      // cancel previous recalculate call
224      if (cancelCurrentRecalculateSource != null)
225        cancelCurrentRecalculateSource.Cancel();
226      cancelCurrentRecalculateSource = new CancellationTokenSource();
227
228      // Set cursor and x-axis
229      var defaultValue = sharedFixedVariables.GetDoubleValue(freeVariable, 0);
230      VerticalLineAnnotation.X = defaultValue;
231      chart.ChartAreas[0].AxisX.Title = FreeVariable + " : " + defaultValue.ToString("N3", CultureInfo.CurrentCulture);
232      SetupAxis(chart.ChartAreas[0].AxisX, trainingMin, trainingMax, XAxisTicks, fixedXAxisMin, fixedXAxisMax);
233
234      // Update series
235      var cancellationToken = cancelCurrentRecalculateSource.Token;
236      try {
237        await UpdateSeriesData(cancellationToken);
238        chart.Update();
239
240        // Set y-axis
241        double ymin = 0, ymax = 0;
242        foreach (var vs in chart.Series.SelectMany(series => series.Points.Select(s => s.YValues))) {
243          for (int i = 0; i < vs.Length; i++) {
244            var v = vs[i];
245            if (ymin > v) ymin = v;
246            if (ymax < v) ymax = v;
247          }
248        }
249        SetupAxis(chart.ChartAreas[0].AxisY, ymin, ymax, YAxisTicks, FixedYAxisMin, FixedYAxisMax);
250        chart.ChartAreas[0].RecalculateAxesScale();
251
252        UpdateOutOfTrainingRangeStripLines();
253
254        statusLabel.Visible = false;
255        Update(); // immediately show
256      }
257      catch (OperationCanceledException) { }
258      catch (AggregateException ae) {
259        if (!ae.InnerExceptions.Any(e => e is OperationCanceledException))
260          throw;
261      }
262    }
263
264    private static void SetupAxis(Axis axis, double minValue, double maxValue, int ticks, double? fixedAxisMin, double? fixedAxisMax) {
265      double axisMin, axisMax, axisInterval;
266      ChartUtil.CalculateAxisInterval(minValue, maxValue, ticks, out axisMin, out axisMax, out axisInterval);
267      axis.Minimum = fixedAxisMin ?? axisMin;
268      axis.Maximum = fixedAxisMax ?? axisMax;
269      axis.Interval = (axisMax - axisMin) / ticks;
270    }
271
272    private void RecalculateTrainingLimits(bool initializeAxisRanges) {
273      trainingMin = solutions.Select(s => s.ProblemData.Dataset.GetDoubleValues(freeVariable, s.ProblemData.TrainingIndices).Min()).Max();
274      trainingMax = solutions.Select(s => s.ProblemData.Dataset.GetDoubleValues(freeVariable, s.ProblemData.TrainingIndices).Max()).Min();
275
276      if (initializeAxisRanges) {
277        double xmin, xmax, xinterval;
278        ChartUtil.CalculateAxisInterval(trainingMin, trainingMax, XAxisTicks, out xmin, out xmax, out xinterval);
279        FixedXAxisMin = xmin;
280        FixedXAxisMax = xmax;
281      }
282    }
283
284    private void RecalculateInternalDataset() {
285      // we expand the range in order to get nice tick intervals on the x axis
286      double xmin, xmax, xinterval;
287      ChartUtil.CalculateAxisInterval(trainingMin, trainingMax, XAxisTicks, out xmin, out xmax, out xinterval);
288
289      if (FixedXAxisMin.HasValue) xmin = FixedXAxisMin.Value;
290      if (FixedXAxisMax.HasValue) xmax = FixedXAxisMax.Value;
291      double step = (xmax - xmin) / drawingSteps;
292
293      var xvalues = new List<double>();
294      for (int i = 0; i < drawingSteps; i++)
295        xvalues.Add(xmin + i * step);
296
297      var variables = sharedFixedVariables.DoubleVariables.ToList();
298      internalDataset = new ModifiableDataset(variables,
299        variables.Select(x => x == FreeVariable
300          ? xvalues
301          : Enumerable.Repeat(sharedFixedVariables.GetDoubleValue(x, 0), xvalues.Count).ToList()
302        )
303      );
304    }
305
306    private Tuple<Series, Series> CreateSeries(IRegressionSolution solution) {
307      var series = new Series {
308        ChartType = SeriesChartType.Line,
309        Name = solution.ProblemData.TargetVariable + " " + solutions.IndexOf(solution)
310      };
311      series.LegendText = series.Name;
312
313      var confidenceBoundSolution = solution as IConfidenceBoundRegressionSolution;
314      Series confidenceIntervalSeries = null;
315      if (confidenceBoundSolution != null) {
316        confidenceIntervalSeries = new Series {
317          ChartType = SeriesChartType.Range,
318          YValuesPerPoint = 2,
319          Name = "95% Conf. Interval " + series.Name,
320          IsVisibleInLegend = false
321        };
322      }
323      return Tuple.Create(series, confidenceIntervalSeries);
324    }
325
326    private void OrderAndColorSeries() {
327      chart.SuspendRepaint();
328
329      chart.Series.Clear();
330      // Add mean series for applying palette colors
331      foreach (var solution in solutions) {
332        chart.Series.Add(seriesCache[solution]);
333      }
334
335      chart.Palette = ChartColorPalette.BrightPastel;
336      chart.ApplyPaletteColors();
337      chart.Palette = ChartColorPalette.None;
338
339      // Add confidence interval series before its coresponding series for correct z index
340      foreach (var solution in solutions) {
341        Series ciSeries;
342        if (ciSeriesCache.TryGetValue(solution, out ciSeries)) {
343          var series = seriesCache[solution];
344          ciSeries.Color =  Color.FromArgb(40, series.Color);
345          int idx = chart.Series.IndexOf(seriesCache[solution]);
346          chart.Series.Insert(idx, ciSeries);
347        }
348      }
349
350      chart.ResumeRepaint(true);
351    }
352
353    private Task UpdateSeriesData(CancellationToken cancellationToken) {
354      return Task.Run(() => {
355        Parallel.ForEach(solutions, new ParallelOptions { CancellationToken = cancellationToken }, solution => {
356          var xvalues = internalDataset.GetDoubleValues(FreeVariable).ToList();
357          var yvalues = solution.Model.GetEstimatedValues(internalDataset, Enumerable.Range(0, internalDataset.Rows)).ToList();
358
359          var series = seriesCache[solution];
360          for (int i = 0; i < xvalues.Count; i++)
361            series.Points[i].SetValueXY(xvalues[i], yvalues[i]);
362
363          var confidenceBoundSolution = solution as IConfidenceBoundRegressionSolution;
364          if (confidenceBoundSolution != null) {
365            var confidenceIntervalSeries = ciSeriesCache[solution];
366
367            cancellationToken.ThrowIfCancellationRequested();
368            var variances =
369              confidenceBoundSolution.Model.GetEstimatedVariances(internalDataset,
370                Enumerable.Range(0, internalDataset.Rows)).ToList();
371            for (int i = 0; i < xvalues.Count; i++) {
372              var lower = yvalues[i] - 1.96 * Math.Sqrt(variances[i]);
373              var upper = yvalues[i] + 1.96 * Math.Sqrt(variances[i]);
374              confidenceIntervalSeries.Points[i].SetValueXY(xvalues[i], lower, upper);
375            }
376          }
377          cancellationToken.ThrowIfCancellationRequested();
378        });
379      }, cancellationToken);
380    }
381
382    private void ResizeAllSeriesData() {
383      var xvalues = internalDataset.GetDoubleValues(FreeVariable).ToList();
384      foreach (var solution in solutions)
385        ResizeSeriesData(solution, xvalues);
386    }
387    private void ResizeSeriesData(IRegressionSolution solution, IList<double> xvalues = null) {
388      if (xvalues == null)
389        xvalues = internalDataset.GetDoubleValues(FreeVariable).ToList();
390
391      var series = seriesCache[solution];
392      series.Points.SuspendUpdates();
393      for (int i = 0; i < xvalues.Count; i++)
394        series.Points.Add(new DataPoint(xvalues[i], 0.0));
395      series.Points.ResumeUpdates();
396
397      Series confidenceIntervalSeries;
398      if (ciSeriesCache.TryGetValue(solution, out confidenceIntervalSeries)) {
399        confidenceIntervalSeries.Points.SuspendUpdates();
400        for (int i = 0; i < xvalues.Count; i++)
401          confidenceIntervalSeries.Points.Add(new DataPoint(xvalues[i], new[] { -1.0, 1.0 }));
402        confidenceIntervalSeries.Points.ResumeUpdates();
403      }
404    }
405
406    public async Task AddSolutionAsync(IRegressionSolution solution) {
407      if (!SolutionsCompatible(solutions.Concat(new[] { solution })))
408        throw new ArgumentException("The solution is not compatible with the problem data.");
409      if (solutions.Contains(solution))
410        return;
411
412      solutions.Add(solution);
413      RecalculateTrainingLimits(true);
414
415      var series = CreateSeries(solution);
416      seriesCache.Add(solution, series.Item1);
417      if (series.Item2 != null)
418        ciSeriesCache.Add(solution, series.Item2);
419
420      ResizeSeriesData(solution);
421      OrderAndColorSeries();
422
423      await RecalculateAsync();
424    }
425    public async Task RemoveSolutionAsync(IRegressionSolution solution) {
426      if (!solutions.Remove(solution))
427        return;
428
429      RecalculateTrainingLimits(true);
430
431      seriesCache.Remove(solution);
432      ciSeriesCache.Remove(solution);
433
434      await RecalculateAsync();
435    }
436
437    private static bool SolutionsCompatible(IEnumerable<IRegressionSolution> solutions) {
438      foreach (var solution1 in solutions) {
439        var variables1 = solution1.ProblemData.Dataset.DoubleVariables;
440        foreach (var solution2 in solutions) {
441          if (solution1 == solution2)
442            continue;
443          var variables2 = solution2.ProblemData.Dataset.DoubleVariables;
444          if (!variables1.All(variables2.Contains))
445            return false;
446        }
447      }
448      return true;
449    }
450
451    private void UpdateOutOfTrainingRangeStripLines() {
452      var axisX = chart.ChartAreas[0].AxisX;
453      var lowerStripLine = axisX.StripLines[0];
454      var upperStripLine = axisX.StripLines[1];
455
456      lowerStripLine.IntervalOffset = axisX.Minimum;
457      lowerStripLine.StripWidth = trainingMin - axisX.Minimum;
458
459      upperStripLine.IntervalOffset = trainingMax;
460      upperStripLine.StripWidth = axisX.Maximum - trainingMax;
461    }
462
463    #region Events
464    public event EventHandler VariableValueChanged;
465    public void OnVariableValueChanged(object sender, EventArgs args) {
466      var changed = VariableValueChanged;
467      if (changed == null) return;
468      changed(sender, args);
469    }
470
471    private void sharedFixedVariables_ItemChanged(object o, EventArgs<int, int> e) {
472      if (o != sharedFixedVariables) return;
473      var variables = sharedFixedVariables.DoubleVariables.ToList();
474      var rowIndex = e.Value;
475      var columnIndex = e.Value2;
476
477      var variableName = variables[columnIndex];
478      if (variableName == FreeVariable) return;
479      var v = sharedFixedVariables.GetDoubleValue(variableName, rowIndex);
480      var values = new List<double>(Enumerable.Repeat(v, DrawingSteps));
481      internalDataset.ReplaceVariable(variableName, values);
482    }
483
484    private double oldCurserPosition = double.NaN;
485    private void chart_AnnotationPositionChanging(object sender, AnnotationPositionChangingEventArgs e) {
486      if (oldCurserPosition.IsAlmost(e.NewLocationX))
487        return;
488      oldCurserPosition = e.NewLocationX;
489
490      var step = (trainingMax - trainingMin) / drawingSteps;
491      e.NewLocationX = step * (long)Math.Round(e.NewLocationX / step);
492      var axisX = chart.ChartAreas[0].AxisX;
493      if (e.NewLocationX > axisX.Maximum)
494        e.NewLocationX = axisX.Maximum;
495      if (e.NewLocationX < axisX.Minimum)
496        e.NewLocationX = axisX.Minimum;
497
498      var annotation = VerticalLineAnnotation;
499      var x = annotation.X;
500      sharedFixedVariables.SetVariableValue(x, FreeVariable, 0);
501
502      chart.ChartAreas[0].AxisX.Title = FreeVariable + " : " + x.ToString("N3", CultureInfo.CurrentCulture);
503      chart.Update();
504
505      OnVariableValueChanged(this, EventArgs.Empty);
506    }
507
508    private void chart_MouseMove(object sender, MouseEventArgs e) {
509      bool hitCursor = chart.HitTest(e.X, e.Y).ChartElementType == ChartElementType.Annotation;
510      chart.Cursor = hitCursor ? Cursors.VSplit : Cursors.Default;
511    }
512
513    private void chart_FormatNumber(object sender, FormatNumberEventArgs e) {
514      if (e.ElementType == ChartElementType.AxisLabels) {
515        switch (e.Format) {
516          case "CustomAxisXFormat":
517            break;
518          case "CustomAxisYFormat":
519            var v = e.Value;
520            e.LocalizedValue = string.Format("{0,5}", v);
521            break;
522          default:
523            break;
524        }
525      }
526    }
527
528    private void chart_DragDrop(object sender, DragEventArgs e) {
529      var data = e.Data.GetData(HeuristicLab.Common.Constants.DragDropDataFormat);
530      if (data != null) {
531        var solution = data as IRegressionSolution;
532        if (!solutions.Contains(solution))
533          AddSolutionAsync(solution);
534      }
535    }
536    private void chart_DragEnter(object sender, DragEventArgs e) {
537      if (!e.Data.GetDataPresent(HeuristicLab.Common.Constants.DragDropDataFormat)) return;
538      e.Effect = DragDropEffects.None;
539
540      var data = e.Data.GetData(HeuristicLab.Common.Constants.DragDropDataFormat);
541      var regressionSolution = data as IRegressionSolution;
542      if (regressionSolution != null) {
543        e.Effect = DragDropEffects.Copy;
544      }
545    }
546    #endregion
547  }
548}
Note: See TracBrowser for help on using the repository browser.