source: trunk/sources/HeuristicLab.Problems.DataAnalysis.Views/3.4/Regression/RegressionSolutionLineChartView.cs @ 14008

Last change on this file since 14008 was 14008, checked in by bburlacu, 6 years ago

#2594: Added chart util method to calculate the "optimal" axis interval (reducing the number of fractional digits to make it look nice). Modified the regression solution line chart and scatter plot to use the new scaling method.

File size: 14.6 KB
Line 
1#region License Information
2/* HeuristicLab
3 * Copyright (C) 2002-2015 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
21using System;
22using System.Collections.Generic;
23using System.Drawing;
24using System.Linq;
25using System.Windows.Forms;
26using System.Windows.Forms.DataVisualization.Charting;
27using HeuristicLab.MainForm;
28using HeuristicLab.Visualization.ChartControlsExtensions;
29
30namespace HeuristicLab.Problems.DataAnalysis.Views {
31  [View("Line Chart")]
32  [Content(typeof(IRegressionSolution))]
33  public partial class RegressionSolutionLineChartView : DataAnalysisSolutionEvaluationView {
34    private const string TARGETVARIABLE_SERIES_NAME = "Target Variable";
35    private const string ESTIMATEDVALUES_TRAINING_SERIES_NAME = "Estimated Values (training)";
36    private const string ESTIMATEDVALUES_TEST_SERIES_NAME = "Estimated Values (test)";
37    private const string ESTIMATEDVALUES_ALL_SERIES_NAME = "Estimated Values (all samples)";
38
39    public new IRegressionSolution Content {
40      get { return (IRegressionSolution)base.Content; }
41      set { base.Content = value; }
42    }
43
44    public RegressionSolutionLineChartView()
45      : base() {
46      InitializeComponent();
47      //configure axis
48      this.chart.CustomizeAllChartAreas();
49      this.chart.ChartAreas[0].CursorX.IsUserSelectionEnabled = true;
50      this.chart.ChartAreas[0].AxisX.ScaleView.Zoomable = true;
51      this.chart.ChartAreas[0].AxisX.IsStartedFromZero = true;
52      this.chart.ChartAreas[0].CursorX.Interval = 1;
53
54      this.chart.ChartAreas[0].CursorY.IsUserSelectionEnabled = true;
55      this.chart.ChartAreas[0].AxisY.ScaleView.Zoomable = true;
56      this.chart.ChartAreas[0].CursorY.Interval = 0;
57    }
58
59    private void RedrawChart() {
60      this.chart.Series.Clear();
61      if (Content != null) {
62        this.chart.ChartAreas[0].AxisX.Minimum = 0;
63        this.chart.ChartAreas[0].AxisX.Maximum = Content.ProblemData.Dataset.Rows - 1;
64
65        this.chart.Series.Add(TARGETVARIABLE_SERIES_NAME);
66        this.chart.Series[TARGETVARIABLE_SERIES_NAME].LegendText = Content.ProblemData.TargetVariable;
67        this.chart.Series[TARGETVARIABLE_SERIES_NAME].ChartType = SeriesChartType.FastLine;
68        this.chart.Series[TARGETVARIABLE_SERIES_NAME].Points.DataBindXY(Enumerable.Range(0, Content.ProblemData.Dataset.Rows).ToArray(),
69          Content.ProblemData.Dataset.GetDoubleValues(Content.ProblemData.TargetVariable).ToArray());
70        // training series
71        this.chart.Series.Add(ESTIMATEDVALUES_TRAINING_SERIES_NAME);
72        this.chart.Series[ESTIMATEDVALUES_TRAINING_SERIES_NAME].LegendText = ESTIMATEDVALUES_TRAINING_SERIES_NAME;
73        this.chart.Series[ESTIMATEDVALUES_TRAINING_SERIES_NAME].ChartType = SeriesChartType.FastLine;
74        this.chart.Series[ESTIMATEDVALUES_TRAINING_SERIES_NAME].EmptyPointStyle.Color = this.chart.Series[ESTIMATEDVALUES_TRAINING_SERIES_NAME].Color;
75        this.chart.Series[ESTIMATEDVALUES_TRAINING_SERIES_NAME].Points.DataBindXY(Content.ProblemData.TrainingIndices.ToArray(), Content.EstimatedTrainingValues.ToArray());
76        this.InsertEmptyPoints(this.chart.Series[ESTIMATEDVALUES_TRAINING_SERIES_NAME]);
77        this.chart.Series[ESTIMATEDVALUES_TRAINING_SERIES_NAME].Tag = Content;
78        // test series
79        this.chart.Series.Add(ESTIMATEDVALUES_TEST_SERIES_NAME);
80        this.chart.Series[ESTIMATEDVALUES_TEST_SERIES_NAME].LegendText = ESTIMATEDVALUES_TEST_SERIES_NAME;
81        this.chart.Series[ESTIMATEDVALUES_TEST_SERIES_NAME].ChartType = SeriesChartType.FastLine;
82        this.chart.Series[ESTIMATEDVALUES_TEST_SERIES_NAME].Points.DataBindXY(Content.ProblemData.TestIndices.ToArray(), Content.EstimatedTestValues.ToArray());
83        this.InsertEmptyPoints(this.chart.Series[ESTIMATEDVALUES_TEST_SERIES_NAME]);
84        this.chart.Series[ESTIMATEDVALUES_TEST_SERIES_NAME].Tag = Content;
85        // series of remaining points
86        int[] allIndices = Enumerable.Range(0, Content.ProblemData.Dataset.Rows).Except(Content.ProblemData.TrainingIndices).Except(Content.ProblemData.TestIndices).ToArray();
87        var estimatedValues = Content.EstimatedValues.ToArray();
88        List<double> allEstimatedValues = allIndices.Select(index => estimatedValues[index]).ToList();
89        this.chart.Series.Add(ESTIMATEDVALUES_ALL_SERIES_NAME);
90        this.chart.Series[ESTIMATEDVALUES_ALL_SERIES_NAME].LegendText = ESTIMATEDVALUES_ALL_SERIES_NAME;
91        this.chart.Series[ESTIMATEDVALUES_ALL_SERIES_NAME].ChartType = SeriesChartType.FastLine;
92        if (allEstimatedValues.Count > 0) {
93          this.chart.Series[ESTIMATEDVALUES_ALL_SERIES_NAME].Points.DataBindXY(allIndices, allEstimatedValues);
94          this.InsertEmptyPoints(this.chart.Series[ESTIMATEDVALUES_ALL_SERIES_NAME]);
95        }
96        this.chart.Series[ESTIMATEDVALUES_ALL_SERIES_NAME].Tag = Content;
97        this.ToggleSeriesData(this.chart.Series[ESTIMATEDVALUES_ALL_SERIES_NAME]);
98
99        // set the y-axis bounds
100        var axisY = this.chart.ChartAreas[0].AxisY;
101        double min = double.MaxValue, max = double.MinValue;
102        foreach (var point in chart.Series.SelectMany(x => x.Points)) {
103          if (!point.YValues.Any() || double.IsInfinity(point.YValues[0]) || double.IsNaN(point.YValues[0]))
104            continue;
105          var y = point.YValues[0];
106          if (y < min)
107            min = y;
108          if (y > max)
109            max = y;
110        }
111
112        double axisMin, axisMax, axisInterval;
113        ChartUtil.CalculateOptimalAxisInterval(min, max, out axisMin, out axisMax, out axisInterval);
114        axisY.Minimum = axisMin;
115        axisY.Maximum = axisMax;
116        axisY.Interval = axisInterval;
117
118        UpdateCursorInterval();
119        this.UpdateStripLines();
120      }
121    }
122
123    private void InsertEmptyPoints(Series series) {
124      int i = 0;
125      while (i < series.Points.Count - 1) {
126        if (series.Points[i].IsEmpty) {
127          ++i;
128          continue;
129        }
130
131        var p1 = series.Points[i];
132        var p2 = series.Points[i + 1];
133        // check for consecutive indices
134        if ((int)p2.XValue - (int)p1.XValue != 1) {
135          // insert an empty point between p1 and p2 so that the line will be invisible (transparent)
136          var p = new DataPoint((int)((p1.XValue + p2.XValue) / 2), 0.0) { IsEmpty = true };
137          series.Points.Insert(i + 1, p);
138        }
139        ++i;
140      }
141    }
142
143    private void UpdateCursorInterval() {
144      var estimatedValues = this.chart.Series[ESTIMATEDVALUES_TRAINING_SERIES_NAME].Points.Select(x => x.YValues[0]).DefaultIfEmpty(1.0);
145      var targetValues = this.chart.Series[TARGETVARIABLE_SERIES_NAME].Points.Select(x => x.YValues[0]).DefaultIfEmpty(1.0);
146      double estimatedValuesRange = estimatedValues.Max() - estimatedValues.Min();
147      double targetValuesRange = targetValues.Max() - targetValues.Min();
148      double interestingValuesRange = Math.Min(Math.Max(targetValuesRange, 1.0), Math.Max(estimatedValuesRange, 1.0));
149      double digits = (int)Math.Log10(interestingValuesRange) - 3;
150      double yZoomInterval = Math.Max(Math.Pow(10, digits), 10E-5);
151      this.chart.ChartAreas[0].CursorY.Interval = yZoomInterval;
152    }
153
154    #region events
155    protected override void RegisterContentEvents() {
156      base.RegisterContentEvents();
157      Content.ModelChanged += new EventHandler(Content_ModelChanged);
158      Content.ProblemDataChanged += new EventHandler(Content_ProblemDataChanged);
159    }
160    protected override void DeregisterContentEvents() {
161      base.DeregisterContentEvents();
162      Content.ModelChanged -= new EventHandler(Content_ModelChanged);
163      Content.ProblemDataChanged -= new EventHandler(Content_ProblemDataChanged);
164    }
165
166    protected override void OnContentChanged() {
167      base.OnContentChanged();
168      RedrawChart();
169    }
170    private void Content_ProblemDataChanged(object sender, EventArgs e) {
171      RedrawChart();
172    }
173    private void Content_ModelChanged(object sender, EventArgs e) {
174      RedrawChart();
175    }
176
177
178
179    private void Chart_MouseDoubleClick(object sender, MouseEventArgs e) {
180      HitTestResult result = chart.HitTest(e.X, e.Y);
181      if (result.ChartArea != null && (result.ChartElementType == ChartElementType.PlottingArea ||
182                                       result.ChartElementType == ChartElementType.Gridlines) ||
183                                       result.ChartElementType == ChartElementType.StripLines) {
184        foreach (var axis in result.ChartArea.Axes)
185          axis.ScaleView.ZoomReset(int.MaxValue);
186      }
187    }
188    #endregion
189
190    private void UpdateStripLines() {
191      this.chart.ChartAreas[0].AxisX.StripLines.Clear();
192
193      int[] attr = new int[Content.ProblemData.Dataset.Rows + 1]; // add a virtual last row that is again empty to simplify loop further down
194      foreach (var row in Content.ProblemData.TrainingIndices) {
195        attr[row] += 1;
196      }
197      foreach (var row in Content.ProblemData.TestIndices) {
198        attr[row] += 2;
199      }
200      int start = 0;
201      int curAttr = attr[start];
202      for (int row = 0; row < attr.Length; row++) {
203        if (attr[row] != curAttr) {
204          switch (curAttr) {
205            case 0: break;
206            case 1:
207              this.CreateAndAddStripLine("Training", start, row, Color.FromArgb(40, Color.Green), Color.Transparent);
208              break;
209            case 2:
210              this.CreateAndAddStripLine("Test", start, row, Color.FromArgb(40, Color.Red), Color.Transparent);
211              break;
212            case 3:
213              this.CreateAndAddStripLine("Training and Test", start, row, Color.FromArgb(40, Color.Green), Color.FromArgb(40, Color.Red), ChartHatchStyle.WideUpwardDiagonal);
214              break;
215            default:
216              // should not happen
217              break;
218          }
219          curAttr = attr[row];
220          start = row;
221        }
222      }
223    }
224
225    private void CreateAndAddStripLine(string title, int start, int end, Color color, Color secondColor, ChartHatchStyle hatchStyle = ChartHatchStyle.None) {
226      StripLine stripLine = new StripLine();
227      stripLine.BackColor = color;
228      stripLine.BackSecondaryColor = secondColor;
229      stripLine.BackHatchStyle = hatchStyle;
230      stripLine.Text = title;
231      stripLine.Font = new Font("Times New Roman", 12, FontStyle.Bold);
232      // strip range is [start .. end] inclusive, but we evaluate [start..end[ (end is exclusive)
233      // the strip should be by one longer (starting at start - 0.5 and ending at end + 0.5)
234      stripLine.StripWidth = end - start;
235      stripLine.IntervalOffset = start - 0.5; // start slightly to the left of the first point to clearly indicate the first point in the partition
236      this.chart.ChartAreas[0].AxisX.StripLines.Add(stripLine);
237    }
238
239    private void ToggleSeriesData(Series series) {
240      if (series.Points.Count > 0) {  //checks if series is shown
241        if (this.chart.Series.Any(s => s != series && s.Points.Count > 0)) {
242          ClearPointsQuick(series.Points);
243        }
244      } else if (Content != null) {
245        string targetVariableName = Content.ProblemData.TargetVariable;
246
247        IEnumerable<int> indices = null;
248        double[] predictedValues = null;
249        switch (series.Name) {
250          case ESTIMATEDVALUES_ALL_SERIES_NAME:
251            indices = Enumerable.Range(0, Content.ProblemData.Dataset.Rows).Except(Content.ProblemData.TrainingIndices).Except(Content.ProblemData.TestIndices).ToArray();
252            var estimatedValues = Content.EstimatedValues.ToArray();
253            predictedValues = indices.Select(index => estimatedValues[index]).ToArray();
254            break;
255          case ESTIMATEDVALUES_TRAINING_SERIES_NAME:
256            indices = Content.ProblemData.TrainingIndices.ToArray();
257            predictedValues = Content.EstimatedTrainingValues.ToArray();
258            break;
259          case ESTIMATEDVALUES_TEST_SERIES_NAME:
260            indices = Content.ProblemData.TestIndices.ToArray();
261            predictedValues = Content.EstimatedTestValues.ToArray();
262            break;
263        }
264        if (predictedValues.Length > 0) {
265          series.Points.DataBindXY(indices, predictedValues);
266          this.InsertEmptyPoints(series);
267        }
268        chart.Legends[series.Legend].ForeColor = Color.Black;
269        UpdateCursorInterval();
270        chart.Refresh();
271      }
272    }
273
274    // workaround as per http://stackoverflow.com/questions/5744930/datapointcollection-clear-performance
275    private static void ClearPointsQuick(DataPointCollection points) {
276      points.SuspendUpdates();
277      while (points.Count > 0)
278        points.RemoveAt(points.Count - 1);
279      points.ResumeUpdates();
280    }
281
282    private void chart_MouseMove(object sender, MouseEventArgs e) {
283      HitTestResult result = chart.HitTest(e.X, e.Y);
284      if (result.ChartElementType == ChartElementType.LegendItem && result.Series.Name != TARGETVARIABLE_SERIES_NAME)
285        Cursor = Cursors.Hand;
286      else
287        Cursor = Cursors.Default;
288    }
289    private void chart_MouseDown(object sender, MouseEventArgs e) {
290      HitTestResult result = chart.HitTest(e.X, e.Y);
291      if (result.ChartElementType == ChartElementType.LegendItem && result.Series.Name != TARGETVARIABLE_SERIES_NAME) {
292        ToggleSeriesData(result.Series);
293      }
294    }
295
296    private void chart_CustomizeLegend(object sender, CustomizeLegendEventArgs e) {
297      if (chart.Series.Count != 4) return;
298      e.LegendItems[0].Cells[1].ForeColor = this.chart.Series[TARGETVARIABLE_SERIES_NAME].Points.Count == 0 ? Color.Gray : Color.Black;
299      e.LegendItems[1].Cells[1].ForeColor = this.chart.Series[ESTIMATEDVALUES_TRAINING_SERIES_NAME].Points.Count == 0 ? Color.Gray : Color.Black;
300      e.LegendItems[2].Cells[1].ForeColor = this.chart.Series[ESTIMATEDVALUES_TEST_SERIES_NAME].Points.Count == 0 ? Color.Gray : Color.Black;
301      e.LegendItems[3].Cells[1].ForeColor = this.chart.Series[ESTIMATEDVALUES_ALL_SERIES_NAME].Points.Count == 0 ? Color.Gray : Color.Black;
302    }
303  }
304}
Note: See TracBrowser for help on using the repository browser.