Free cookie consent management tool by TermsFeed Policy Generator

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

Last change on this file since 13829 was 13829, checked in by bburlacu, 9 years ago

#2597: Speed up response calculation in the GradientChart by using an internal dataset to store variable values.

File size: 15.6 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.Windows.Forms;
28using System.Windows.Forms.DataVisualization.Charting;
29using HeuristicLab.Common;
30using HeuristicLab.Visualization.ChartControlsExtensions;
31
32namespace HeuristicLab.Problems.DataAnalysis.Views {
33  public partial class GradientChart : EnhancedChart {
34    private ModifiableDataset sharedDataset; // used for syncronising variable values between charts
35    private ModifiableDataset internalDataset; // used to cache values and speed up calculations
36
37    public bool ShowLegend { get; set; }
38    public bool ShowXAxisLabel { get; set; }
39    public bool ShowYAxisLabel { get; set; }
40    public bool ShowCursor { get; set; }
41
42    private bool useMedianValues;
43    public bool UseMedianValues {
44      get { return useMedianValues; }
45      set {
46        if (value == useMedianValues) return;
47        useMedianValues = value;
48        OnChartPropertyChanged(this, EventArgs.Empty);
49        UpdateChart();
50      }
51    }
52
53    private int row;
54    public int Row {
55      get { return row; }
56      set {
57        if (row == value) return;
58        row = value;
59        OnChartPropertyChanged(this, EventArgs.Empty);
60        UpdateChart();
61      }
62    }
63
64    private double min;
65    public double Min {
66      get { return min; }
67      set {
68        if (value.IsAlmost(min)) return;
69        min = value;
70        OnChartPropertyChanged(this, EventArgs.Empty);
71        UpdateChart();
72      }
73    }
74
75    private double max;
76    public double Max {
77      get { return max; }
78      set {
79        if (value.IsAlmost(max)) return;
80        max = value;
81        OnChartPropertyChanged(this, EventArgs.Empty);
82        UpdateChart();
83      }
84    }
85
86    private int points;
87    public int Points {
88      get { return points; }
89      set {
90        if (value == points) return;
91        points = value;
92        OnChartPropertyChanged(this, EventArgs.Empty);
93        UpdateChart();
94      }
95    }
96
97    private IRegressionProblemData problemData;
98    public IRegressionProblemData ProblemData {
99      get { return problemData; }
100      set {
101        if (!SolutionsCompatibleWithProblemData(value, solutionList))
102          throw new ArgumentException("The problem data provided does not contain all the variables required by the solutions.");
103        problemData = value;
104        UpdateDataset();
105        UpdateChart();
106      }
107    }
108
109    public string Target {
110      get { return Solutions.First().ProblemData.TargetVariable; }
111    }
112
113    private string variable;
114    public string Variable {
115      get { return variable; }
116      set {
117        if (variable == value) return;
118        if (!ProblemData.Dataset.DoubleVariables.Contains(value))
119          throw new ArgumentException("The variable must be present in the problem dataset.");
120        OnChartPropertyChanged(this, EventArgs.Empty);
121        variable = value;
122        var values = ProblemData.Dataset.GetReadOnlyDoubleValues(variable);
123        min = values.Min();
124        max = values.Max();
125        UpdateChart();
126      }
127    }
128
129    private List<IRegressionSolution> solutionList;
130    public IEnumerable<IRegressionSolution> Solutions {
131      get { return solutionList; }
132      set {
133        if (!value.Any())
134          throw new ArgumentException("At least one solution must be provided.");
135        if (SolutionsCompatibleWithProblemData(problemData, value))
136          solutionList = new List<IRegressionSolution>(value);
137        else
138          throw new ArgumentException("The provided solution collection is not compatible with the existing problem data.");
139        UpdateChart();
140      }
141    }
142
143    public VerticalLineAnnotation VerticalLineAnnotation {
144      get { return (VerticalLineAnnotation)Annotations.SingleOrDefault(x => x is VerticalLineAnnotation); }
145    }
146
147    public GradientChart() {
148      InitializeComponent();
149      RegisterEvents();
150    }
151
152    public void AddSolution(IRegressionSolution solution) {
153      if (!SolutionsCompatibleWithProblemData(problemData, new[] { solution })) {
154        throw new ArgumentException("The solution is not compatible with the problem data.");
155      }
156      solutionList.Add(solution);
157      UpdateChart();
158    }
159
160    public void RemoveSolution(IRegressionSolution solution) {
161      var removed = solutionList.RemoveAll(x => x == solution);
162      if (removed > 0)
163        UpdateChart();
164    }
165
166    private static bool SolutionsCompatibleWithProblemData(IRegressionProblemData pd, IEnumerable<IRegressionSolution> solutions) {
167      if (pd == null || !solutions.Any()) return true;
168      if (solutions.Any(x => x.ProblemData.TargetVariable != pd.TargetVariable)) return false;
169      var variables = new HashSet<string>(pd.Dataset.DoubleVariables);
170      return solutions.SelectMany(x => x.ProblemData.Dataset.DoubleVariables).All(variables.Contains);
171    }
172
173
174    public void Configure(IEnumerable<IRegressionSolution> solutions, IRegressionProblemData pd, double min, double max, int points) {
175      if (!SolutionsCompatibleWithProblemData(pd, solutions))
176        throw new ArgumentException("Solutions are not compatible with the problem data.");
177      this.solutionList = new List<IRegressionSolution>(solutions);
178      this.problemData = pd;
179      this.variable = pd.Dataset.DoubleVariables.First();
180      this.min = min;
181      this.max = max;
182      this.points = points;
183
184      UpdateDataset();
185      UpdateChart();
186    }
187
188    public void Configure(IEnumerable<IRegressionSolution> solutions, IRegressionProblemData pd, ModifiableDataset dataset, string variable, double min, double max, int points) {
189      if (!SolutionsCompatibleWithProblemData(pd, solutions))
190        throw new ArgumentException("Solutions are not compatible with the problem data.");
191      this.solutionList = new List<IRegressionSolution>(solutions);
192      this.problemData = pd;
193      this.variable = variable;
194      this.sharedDataset = dataset;
195      this.min = min;
196      this.max = max;
197      this.points = points;
198
199      // add an event such that whenever a value is changed in the shared dataset,
200      // this change is reflected in the internal dataset (where the value becomes a whole column)
201      var variables = sharedDataset.DoubleVariables.ToList();
202      sharedDataset.ItemChanged += (o, e) => {
203        var rowIndex = e.Value;
204        var columnIndex = e.Value2;
205        var ds = (ModifiableDataset)o;
206        var variableName = variables[columnIndex];
207        if (variableName == Variable) return;
208        var v = ds.GetDoubleValue(variableName, rowIndex);
209        var values = new List<double>(Enumerable.Repeat(v, Points));
210        internalDataset.ReplaceVariable(variableName, values);
211      };
212
213      // configure internal dataset. we also expand the range in order to get nice tick intervals on the x axis
214      const int tics = 5;
215      double xmin, xmax, xinterval;
216      ChartUtil.CalculateAxisInterval(min, max, tics, out xmin, out xmax, out xinterval);
217      var step = (xmax - xmin) / points;
218      var xvalues = new List<double>();
219      for (int i = 0; i < points; ++i) { xvalues.Add(xmin + i * step); }
220      internalDataset = new ModifiableDataset(variables, variables.Select(x => x == Variable ? xvalues : new List<double>(Enumerable.Repeat(sharedDataset.GetDoubleValue(x, 0), xvalues.Count))));
221    }
222
223    public void UpdateChart() {
224      // throw exceptions?
225      if (sharedDataset == null || solutionList == null || !solutionList.Any())
226        return;
227      if (min.IsAlmost(max) || min > max || points == 0)
228        return;
229      Series.Clear();
230      var vla = VerticalLineAnnotation;
231      Annotations.Clear();
232      var defaultValue = sharedDataset.GetDoubleValue(variable, 0);
233      vla.Visible = ShowCursor;
234      Annotations.Add(vla);
235      vla.X = defaultValue;
236
237      double axisMin, axisMax, axisInterval;
238      // calculate X-axis interval
239      ChartUtil.CalculateAxisInterval(min, max, 5, out axisMin, out axisMax, out axisInterval);
240      var axis = ChartAreas[0].AxisX;
241      axis.Minimum = axisMin;
242      axis.Maximum = axisMax;
243      axis.Interval = axisInterval;
244
245      for (int i = 0; i < solutionList.Count; ++i) {
246        var solution = solutionList[i];
247        var series = PlotSeries(solution);
248        series.Name = Target + " " + i;
249        Series.Add(series);
250      }
251      // calculate Y-axis interval
252      double ymin = 0, ymax = 0;
253      foreach (var v in Series[0].Points.Select(x => x.YValues[0])) {
254        if (ymin > v) ymin = v;
255        if (ymax < v) ymax = v;
256      }
257      ChartUtil.CalculateAxisInterval(ymin, ymax, 5, out axisMin, out axisMax, out axisInterval);
258      axis = ChartAreas[0].AxisY;
259      axis.Minimum = axisMin;
260      axis.Maximum = axisMax;
261      axis.Interval = axisInterval;
262
263      if (ShowXAxisLabel) {
264        ChartAreas[0].AxisX.Title = Variable + " : " + defaultValue.ToString("N3", CultureInfo.CurrentCulture); // set axis title
265      }
266
267      AddStripLines(); // add strip lines
268      if (ShowLegend)
269        AddLegends();
270    }
271
272    private void UpdateDataset() {
273      var variables = ProblemData.Dataset.DoubleVariables.ToList();
274      var variableValues = new List<double>[variables.Count];
275
276      if (UseMedianValues) {
277        for (int i = 0; i < variables.Count; ++i) {
278          var median = ProblemData.Dataset.GetDoubleValues(variables[i], ProblemData.TrainingIndices).Median();
279          variableValues[i] = new List<double> { median };
280        }
281      } else {
282        for (int i = 0; i < variables.Count; ++i) {
283          var variableValue = ProblemData.Dataset.GetDoubleValue(variables[i], Row);
284          variableValues[i] = new List<double> { variableValue };
285        }
286      }
287      sharedDataset = new ModifiableDataset(variables, variableValues);
288    }
289
290    private Series PlotSeries(IRegressionSolution solution) {
291      var v = sharedDataset.GetDoubleValue(variable, 0);
292      var series = new Series { ChartType = SeriesChartType.Point };
293      // get values from series
294      var xvalues = internalDataset.GetReadOnlyDoubleValues(Variable);
295      var yvalues = solution.Model.GetEstimatedValues(internalDataset, Enumerable.Range(0, internalDataset.Rows));
296      int i = 0;
297      foreach (var y in yvalues) {
298        var x = xvalues[i++];
299        series.Points.Add(new DataPoint(x, y) { MarkerSize = 2, MarkerColor = Color.DodgerBlue });
300      }
301      if (ShowCursor) {
302        var y = solution.Model.GetEstimatedValues(sharedDataset, new[] { 0 }).Single();
303        series.Points.Add(new DataPoint(v, y) { MarkerSize = 5, MarkerColor = Color.Red });
304      }
305      if (ShowLegend) {
306        series.IsVisibleInLegend = true;
307      }
308      return series;
309    }
310
311    private void AddLegends() {
312      Legends.Clear();
313      var legend = new Legend();
314      legend.Alignment = StringAlignment.Center;
315      legend.LegendStyle = LegendStyle.Row;
316      legend.Docking = Docking.Top;
317      Legends.Add(legend);
318      foreach (var s in Series) {
319        s.Legend = legend.Name;
320      }
321    }
322
323    private void AddStripLines() {
324      var axisX = ChartAreas[0].AxisX;
325      axisX.StripLines.Clear();
326      axisX.StripLines.Add(new StripLine { BackColor = Color.FromArgb(30, Color.Green), IntervalOffset = axisX.Minimum, StripWidth = min - axisX.Minimum });
327      axisX.StripLines.Add(new StripLine { BackColor = Color.FromArgb(30, Color.Green), IntervalOffset = max, StripWidth = axisX.Maximum - max });
328    }
329
330    private void RegisterEvents() {
331      AnnotationPositionChanging += chart_AnnotationPositionChanging;
332      MouseMove += chart_MouseMove;
333      FormatNumber += chart_FormatNumber;
334    }
335
336    #region events
337    public event EventHandler VariableValueChanged;
338    public void OnVariableValueChanged(object sender, EventArgs args) {
339      var changed = VariableValueChanged;
340      if (changed == null) return;
341      changed(sender, args);
342    }
343
344    public event EventHandler ChartPropertyChanged;
345    public void OnChartPropertyChanged(object sender, EventArgs args) {
346      var changed = ChartPropertyChanged;
347      if (changed == null) return;
348      changed(sender, args);
349    }
350
351    private void chart_AnnotationPositionChanged(object sender, EventArgs e) {
352      var annotation = VerticalLineAnnotation;
353      var x = annotation.X;
354      sharedDataset.SetVariableValue(x, Variable, 0);
355      for (int i = 0; i < solutionList.Count; ++i) {
356        var y = solutionList[i].Model.GetEstimatedValues(sharedDataset, new[] { 0 }).Single();
357        var s = Series[i];
358        var n = s.Points.Count;
359        s.Points[n - 1] = new DataPoint(x, y) { MarkerColor = Color.Red, MarkerSize = 5 };
360      }
361      if (ShowXAxisLabel) {
362        ChartAreas[0].AxisX.Title = Variable + " : " + x.ToString("N3", CultureInfo.CurrentCulture);
363      }
364      Update();
365      OnVariableValueChanged(this, EventArgs.Empty);
366    }
367
368    private void chart_AnnotationPositionChanging(object sender, AnnotationPositionChangingEventArgs e) {
369      var step = (max - min) / points;
370      e.NewLocationX = step * (long)Math.Round(e.NewLocationX / step);
371      var axisX = ChartAreas[0].AxisX;
372      if (e.NewLocationX > axisX.Maximum)
373        e.NewLocationX = axisX.Maximum;
374      if (e.NewLocationX < axisX.Minimum)
375        e.NewLocationX = axisX.Minimum;
376    }
377
378    private void chart_MouseMove(object sender, MouseEventArgs e) {
379      this.Cursor = HitTest(e.X, e.Y).ChartElementType == ChartElementType.Annotation ? Cursors.VSplit : Cursors.Default;
380    }
381
382    private void chart_FormatNumber(object sender, FormatNumberEventArgs e) {
383      if (e.ElementType == ChartElementType.AxisLabels) {
384        switch (e.Format) {
385          case "CustomAxisXFormat":
386            break;
387          case "CustomAxisYFormat":
388            var v = e.Value;
389            e.LocalizedValue = string.Format("{0,5}", v);
390            break;
391          default:
392            break;
393        }
394      }
395    }
396
397    private void GradientChart_DragDrop(object sender, DragEventArgs e) {
398      var data = e.Data.GetData(HeuristicLab.Common.Constants.DragDropDataFormat);
399      if (data != null) {
400        var solution = data as IRegressionSolution;
401        if (!Solutions.Contains(solution))
402          AddSolution(solution);
403      }
404    }
405
406    private void GradientChart_DragEnter(object sender, DragEventArgs e) {
407      if (!e.Data.GetDataPresent(HeuristicLab.Common.Constants.DragDropDataFormat)) return;
408      e.Effect = DragDropEffects.None;
409
410      var data = e.Data.GetData(HeuristicLab.Common.Constants.DragDropDataFormat);
411      var regressionSolution = data as IRegressionSolution;
412      if (regressionSolution != null) {
413        e.Effect = DragDropEffects.Copy;
414      }
415    }
416    #endregion
417  }
418}
Note: See TracBrowser for help on using the repository browser.