Free cookie consent management tool by TermsFeed Policy Generator

source: trunk/sources/HeuristicLab.Analysis.Views/3.3/ScatterPlotControl.cs @ 14895

Last change on this file since 14895 was 14860, checked in by mkommend, 8 years ago

#2765: Implemented tags for data points in ScatterPlot.

File size: 34.3 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.Linq;
26using System.Text;
27using System.Windows.Forms;
28using System.Windows.Forms.DataVisualization.Charting;
29using HeuristicLab.Collections;
30using HeuristicLab.Common;
31using HeuristicLab.MainForm;
32using HeuristicLab.MainForm.WindowsForms;
33
34namespace HeuristicLab.Analysis.Views {
35  public partial class ScatterPlotControl : AsynchronousContentView {
36    protected List<Series> invisibleSeries;
37    protected Dictionary<IObservableList<Point2D<double>>, ScatterPlotDataRow> pointsRowsTable;
38    protected Dictionary<Series, Series> seriesToRegressionSeriesTable;
39    private double xMin, xMax, yMin, yMax;
40
41    public new ScatterPlot Content {
42      get { return (ScatterPlot)base.Content; }
43      set { base.Content = value; }
44    }
45
46    public ScatterPlotControl() {
47      InitializeComponent();
48      pointsRowsTable = new Dictionary<IObservableList<Point2D<double>>, ScatterPlotDataRow>();
49      seriesToRegressionSeriesTable = new Dictionary<Series, Series>();
50      invisibleSeries = new List<Series>();
51      chart.CustomizeAllChartAreas();
52      chart.ChartAreas[0].CursorX.Interval = 1;
53      chart.ContextMenuStrip.Items.Add(configureToolStripMenuItem);
54    }
55
56    #region Event Handler Registration
57    protected override void DeregisterContentEvents() {
58      foreach (ScatterPlotDataRow row in Content.Rows)
59        DeregisterScatterPlotDataRowEvents(row);
60      Content.VisualPropertiesChanged -= new EventHandler(Content_VisualPropertiesChanged);
61      Content.Rows.ItemsAdded -= new CollectionItemsChangedEventHandler<ScatterPlotDataRow>(Rows_ItemsAdded);
62      Content.Rows.ItemsRemoved -= new CollectionItemsChangedEventHandler<ScatterPlotDataRow>(Rows_ItemsRemoved);
63      Content.Rows.ItemsReplaced -= new CollectionItemsChangedEventHandler<ScatterPlotDataRow>(Rows_ItemsReplaced);
64      Content.Rows.CollectionReset -= new CollectionItemsChangedEventHandler<ScatterPlotDataRow>(Rows_CollectionReset);
65    }
66    protected override void RegisterContentEvents() {
67      Content.VisualPropertiesChanged += new EventHandler(Content_VisualPropertiesChanged);
68      Content.Rows.ItemsAdded += new CollectionItemsChangedEventHandler<ScatterPlotDataRow>(Rows_ItemsAdded);
69      Content.Rows.ItemsRemoved += new CollectionItemsChangedEventHandler<ScatterPlotDataRow>(Rows_ItemsRemoved);
70      Content.Rows.ItemsReplaced += new CollectionItemsChangedEventHandler<ScatterPlotDataRow>(Rows_ItemsReplaced);
71      Content.Rows.CollectionReset += new CollectionItemsChangedEventHandler<ScatterPlotDataRow>(Rows_CollectionReset);
72    }
73
74    protected virtual void RegisterScatterPlotDataRowEvents(ScatterPlotDataRow row) {
75      row.NameChanged += new EventHandler(Row_NameChanged);
76      row.VisualPropertiesChanged += new EventHandler(Row_VisualPropertiesChanged);
77      pointsRowsTable.Add(row.Points, row);
78      row.Points.ItemsAdded += new CollectionItemsChangedEventHandler<IndexedItem<Point2D<double>>>(Points_ItemsAdded);
79      row.Points.ItemsRemoved += new CollectionItemsChangedEventHandler<IndexedItem<Point2D<double>>>(Points_ItemsRemoved);
80      row.Points.ItemsReplaced += new CollectionItemsChangedEventHandler<IndexedItem<Point2D<double>>>(Points_ItemsReplaced);
81      row.Points.CollectionReset += new CollectionItemsChangedEventHandler<IndexedItem<Point2D<double>>>(Points_CollectionReset);
82    }
83    protected virtual void DeregisterScatterPlotDataRowEvents(ScatterPlotDataRow row) {
84      row.Points.ItemsAdded -= new CollectionItemsChangedEventHandler<IndexedItem<Point2D<double>>>(Points_ItemsAdded);
85      row.Points.ItemsRemoved -= new CollectionItemsChangedEventHandler<IndexedItem<Point2D<double>>>(Points_ItemsRemoved);
86      row.Points.ItemsReplaced -= new CollectionItemsChangedEventHandler<IndexedItem<Point2D<double>>>(Points_ItemsReplaced);
87      row.Points.CollectionReset -= new CollectionItemsChangedEventHandler<IndexedItem<Point2D<double>>>(Points_CollectionReset);
88      pointsRowsTable.Remove(row.Points);
89      row.VisualPropertiesChanged -= new EventHandler(Row_VisualPropertiesChanged);
90      row.NameChanged -= new EventHandler(Row_NameChanged);
91    }
92    #endregion
93
94    protected override void OnContentChanged() {
95      invisibleSeries.Clear();
96      chart.Titles[0].Text = string.Empty;
97      chart.ChartAreas[0].AxisX.Title = string.Empty;
98      chart.ChartAreas[0].AxisY.Title = string.Empty;
99      chart.Series.Clear();
100      if (Content != null) {
101        chart.Titles[0].Text = Content.Name;
102        chart.Titles[0].Visible = !string.IsNullOrEmpty(Content.Name);
103        AddScatterPlotDataRows(Content.Rows);
104        ConfigureChartArea(chart.ChartAreas[0]);
105        RecalculateMinMaxPointValues();
106        RecalculateAxesScale(chart.ChartAreas[0]);
107      }
108    }
109
110    protected override void SetEnabledStateOfControls() {
111      chart.Enabled = Content != null;
112    }
113
114    public void ShowConfiguration() {
115      if (Content != null) {
116        using (ScatterPlotVisualPropertiesDialog dialog = new ScatterPlotVisualPropertiesDialog(Content)) {
117          dialog.ShowDialog(this);
118        }
119      } else MessageBox.Show("Nothing to configure.");
120    }
121
122    protected virtual void AddScatterPlotDataRows(IEnumerable<ScatterPlotDataRow> rows) {
123      foreach (var row in rows) {
124        RegisterScatterPlotDataRowEvents(row);
125        Series series = new Series(row.Name) { Tag = row };
126
127        if (row.VisualProperties.DisplayName.Trim() != String.Empty) series.LegendText = row.VisualProperties.DisplayName;
128        else series.LegendText = row.Name;
129
130        var regressionSeries = new Series(row.Name + "_Regression") {
131          Tag = row,
132          ChartType = SeriesChartType.Line,
133          BorderDashStyle = ChartDashStyle.Dot,
134          IsVisibleInLegend = false,
135          Color = Color.Transparent // to avoid auto color assignment via color palette
136        };
137
138        seriesToRegressionSeriesTable.Add(series, regressionSeries);
139        ConfigureSeries(series, regressionSeries, row);
140        FillSeriesWithRowValues(series, row);
141
142        chart.Series.Add(series);
143        chart.Series.Add(regressionSeries);
144        FillRegressionSeries(regressionSeries, row);
145      }
146      ConfigureChartArea(chart.ChartAreas[0]);
147      RecalculateMinMaxPointValues();
148      RecalculateAxesScale(chart.ChartAreas[0]);
149      UpdateYCursorInterval();
150      UpdateRegressionSeriesColors();
151    }
152
153    protected virtual void RemoveScatterPlotDataRows(IEnumerable<ScatterPlotDataRow> rows) {
154      foreach (var row in rows) {
155        DeregisterScatterPlotDataRowEvents(row);
156        Series series = chart.Series[row.Name];
157        chart.Series.Remove(series);
158        if (invisibleSeries.Contains(series))
159          invisibleSeries.Remove(series);
160        chart.Series.Remove(seriesToRegressionSeriesTable[series]);
161        seriesToRegressionSeriesTable.Remove(series);
162      }
163      RecalculateMinMaxPointValues();
164      RecalculateAxesScale(chart.ChartAreas[0]);
165    }
166
167    private void ConfigureSeries(Series series, Series regressionSeries, ScatterPlotDataRow row) {
168      series.BorderWidth = 1;
169      series.BorderDashStyle = ChartDashStyle.Solid;
170      series.BorderColor = Color.Empty;
171
172      if (row.VisualProperties.Color != Color.Empty)
173        series.Color = row.VisualProperties.Color;
174      else series.Color = Color.Empty;
175      series.IsVisibleInLegend = row.VisualProperties.IsVisibleInLegend;
176      series.ChartType = SeriesChartType.FastPoint;
177      series.MarkerSize = row.VisualProperties.PointSize;
178      series.MarkerStyle = ConvertPointStyle(row.VisualProperties.PointStyle);
179      series.XAxisType = AxisType.Primary;
180      series.YAxisType = AxisType.Primary;
181
182      if (row.VisualProperties.DisplayName.Trim() != String.Empty) series.LegendText = row.VisualProperties.DisplayName;
183      else series.LegendText = row.Name;
184
185      string xAxisTitle = string.IsNullOrEmpty(Content.VisualProperties.XAxisTitle)
186                      ? "X"
187                      : Content.VisualProperties.XAxisTitle;
188      string yAxisTitle = string.IsNullOrEmpty(Content.VisualProperties.YAxisTitle)
189                            ? "Y"
190                            : Content.VisualProperties.YAxisTitle;
191      series.ToolTip =
192        series.LegendText + Environment.NewLine +
193        xAxisTitle + " = " + "#VALX," + Environment.NewLine +
194        yAxisTitle + " = " + "#VAL";
195
196      regressionSeries.BorderWidth = Math.Max(1, row.VisualProperties.PointSize / 2);
197      regressionSeries.IsVisibleInLegend = row.VisualProperties.IsRegressionVisibleInLegend &&
198        row.VisualProperties.RegressionType != ScatterPlotDataRowVisualProperties.ScatterPlotDataRowRegressionType.None;
199      regressionSeries.LegendText = string.IsNullOrEmpty(row.VisualProperties.RegressionDisplayName)
200        ? string.Format("{0}({1})", row.VisualProperties.RegressionType, row.Name)
201        : row.VisualProperties.RegressionDisplayName;
202    }
203
204    private void ConfigureChartArea(ChartArea area) {
205      if (Content.VisualProperties.TitleFont != null) chart.Titles[0].Font = Content.VisualProperties.TitleFont;
206      if (!Content.VisualProperties.TitleColor.IsEmpty) chart.Titles[0].ForeColor = Content.VisualProperties.TitleColor;
207      chart.Titles[0].Text = Content.VisualProperties.Title;
208      chart.Titles[0].Visible = !string.IsNullOrEmpty(Content.VisualProperties.Title);
209
210      if (Content.VisualProperties.AxisTitleFont != null) area.AxisX.TitleFont = Content.VisualProperties.AxisTitleFont;
211      if (!Content.VisualProperties.AxisTitleColor.IsEmpty) area.AxisX.TitleForeColor = Content.VisualProperties.AxisTitleColor;
212      area.AxisX.Title = Content.VisualProperties.XAxisTitle;
213      area.AxisX.MajorGrid.Enabled = Content.VisualProperties.XAxisGrid;
214
215      if (Content.VisualProperties.AxisTitleFont != null) area.AxisY.TitleFont = Content.VisualProperties.AxisTitleFont;
216      if (!Content.VisualProperties.AxisTitleColor.IsEmpty) area.AxisY.TitleForeColor = Content.VisualProperties.AxisTitleColor;
217      area.AxisY.Title = Content.VisualProperties.YAxisTitle;
218      area.AxisY.MajorGrid.Enabled = Content.VisualProperties.YAxisGrid;
219    }
220
221    private void RecalculateAxesScale(ChartArea area) {
222      area.AxisX.Minimum = CalculateMinBound(xMin);
223      area.AxisX.Maximum = CalculateMaxBound(xMax);
224      if (area.AxisX.Minimum == area.AxisX.Maximum) {
225        area.AxisX.Minimum = xMin - 0.5;
226        area.AxisX.Maximum = xMax + 0.5;
227      }
228      area.AxisY.Minimum = CalculateMinBound(yMin);
229      area.AxisY.Maximum = CalculateMaxBound(yMax);
230      if (area.AxisY.Minimum == area.AxisY.Maximum) {
231        area.AxisY.Minimum = yMin - 0.5;
232        area.AxisY.Maximum = yMax + 0.5;
233      }
234      if (xMax - xMin > 0) area.CursorX.Interval = Math.Pow(10, Math.Floor(Math.Log10(area.AxisX.Maximum - area.AxisX.Minimum) - 3));
235      else area.CursorX.Interval = 1;
236      area.AxisX.IsMarginVisible = false;
237
238      if (!Content.VisualProperties.XAxisMinimumAuto && !double.IsNaN(Content.VisualProperties.XAxisMinimumFixedValue)) area.AxisX.Minimum = Content.VisualProperties.XAxisMinimumFixedValue;
239      if (!Content.VisualProperties.XAxisMaximumAuto && !double.IsNaN(Content.VisualProperties.XAxisMaximumFixedValue)) area.AxisX.Maximum = Content.VisualProperties.XAxisMaximumFixedValue;
240      if (!Content.VisualProperties.YAxisMinimumAuto && !double.IsNaN(Content.VisualProperties.YAxisMinimumFixedValue)) area.AxisY.Minimum = Content.VisualProperties.YAxisMinimumFixedValue;
241      if (!Content.VisualProperties.YAxisMaximumAuto && !double.IsNaN(Content.VisualProperties.YAxisMaximumFixedValue)) area.AxisY.Maximum = Content.VisualProperties.YAxisMaximumFixedValue;
242    }
243
244    private static double CalculateMinBound(double min) {
245      if (min == 0) return 0;
246      var scale = Math.Pow(10, Math.Floor(Math.Log10(Math.Abs(min))));
247      return scale * (Math.Floor(min / scale));
248    }
249
250    private static double CalculateMaxBound(double max) {
251      if (max == 0) return 0;
252      var scale = Math.Pow(10, Math.Floor(Math.Log10(Math.Abs(max))));
253      return scale * (Math.Ceiling(max / scale));
254    }
255
256    protected virtual void UpdateYCursorInterval() {
257      double interestingValuesRange = (
258        from series in chart.Series
259        where series.Enabled
260        let values = (from point in series.Points
261                      where !point.IsEmpty
262                      select point.YValues[0]).DefaultIfEmpty(1.0)
263        let range = values.Max() - values.Min()
264        where range > 0.0
265        select range
266        ).DefaultIfEmpty(1.0).Min();
267
268      double digits = (int)Math.Log10(interestingValuesRange) - 3;
269      double yZoomInterval = Math.Pow(10, digits);
270      this.chart.ChartAreas[0].CursorY.Interval = yZoomInterval;
271    }
272
273    protected void UpdateRegressionSeriesColors() {
274      chart.ApplyPaletteColors();
275      foreach (var row in Content.Rows) {
276        var series = chart.Series[row.Name];
277        var regressionSeries = seriesToRegressionSeriesTable[series];
278        regressionSeries.Color = series.Color;
279      }
280    }
281
282    #region Event Handlers
283    #region Content Event Handlers
284    private void Content_VisualPropertiesChanged(object sender, EventArgs e) {
285      if (InvokeRequired)
286        Invoke(new EventHandler(Content_VisualPropertiesChanged), sender, e);
287      else {
288        ConfigureChartArea(chart.ChartAreas[0]);
289        RecalculateAxesScale(chart.ChartAreas[0]); // axes min/max could have changed
290      }
291    }
292    #endregion
293    #region Rows Event Handlers
294    private void Rows_ItemsAdded(object sender, CollectionItemsChangedEventArgs<ScatterPlotDataRow> e) {
295      if (InvokeRequired)
296        Invoke(new CollectionItemsChangedEventHandler<ScatterPlotDataRow>(Rows_ItemsAdded), sender, e);
297      else {
298        AddScatterPlotDataRows(e.Items);
299      }
300    }
301    private void Rows_ItemsRemoved(object sender, CollectionItemsChangedEventArgs<ScatterPlotDataRow> e) {
302      if (InvokeRequired)
303        Invoke(new CollectionItemsChangedEventHandler<ScatterPlotDataRow>(Rows_ItemsRemoved), sender, e);
304      else {
305        RemoveScatterPlotDataRows(e.Items);
306      }
307    }
308    private void Rows_ItemsReplaced(object sender, CollectionItemsChangedEventArgs<ScatterPlotDataRow> e) {
309      if (InvokeRequired)
310        Invoke(new CollectionItemsChangedEventHandler<ScatterPlotDataRow>(Rows_ItemsReplaced), sender, e);
311      else {
312        RemoveScatterPlotDataRows(e.OldItems);
313        AddScatterPlotDataRows(e.Items);
314      }
315    }
316    private void Rows_CollectionReset(object sender, CollectionItemsChangedEventArgs<ScatterPlotDataRow> e) {
317      if (InvokeRequired)
318        Invoke(new CollectionItemsChangedEventHandler<ScatterPlotDataRow>(Rows_CollectionReset), sender, e);
319      else {
320        RemoveScatterPlotDataRows(e.OldItems);
321        AddScatterPlotDataRows(e.Items);
322      }
323    }
324    #endregion
325    #region Row Event Handlers
326    private void Row_VisualPropertiesChanged(object sender, EventArgs e) {
327      if (InvokeRequired)
328        Invoke(new EventHandler(Row_VisualPropertiesChanged), sender, e);
329      else {
330        ScatterPlotDataRow row = (ScatterPlotDataRow)sender;
331        Series series = chart.Series[row.Name];
332        Series regressionSeries = seriesToRegressionSeriesTable[series];
333        series.Points.Clear();
334        regressionSeries.Points.Clear();
335        ConfigureSeries(series, regressionSeries, row);
336        FillSeriesWithRowValues(series, row);
337        FillRegressionSeries(regressionSeries, row);
338        RecalculateMinMaxPointValues();
339        RecalculateAxesScale(chart.ChartAreas[0]);
340        UpdateRegressionSeriesColors();
341      }
342    }
343    private void Row_NameChanged(object sender, EventArgs e) {
344      if (InvokeRequired)
345        Invoke(new EventHandler(Row_NameChanged), sender, e);
346      else {
347        ScatterPlotDataRow row = (ScatterPlotDataRow)sender;
348        chart.Series[row.Name].Name = row.Name;
349      }
350    }
351    #endregion
352    #region Points Event Handlers
353    private void Points_ItemsAdded(object sender, CollectionItemsChangedEventArgs<IndexedItem<Point2D<double>>> e) {
354      if (InvokeRequired)
355        Invoke(new CollectionItemsChangedEventHandler<IndexedItem<Point2D<double>>>(Points_ItemsAdded), sender, e);
356      else {
357        ScatterPlotDataRow row = null;
358        pointsRowsTable.TryGetValue((IObservableList<Point2D<double>>)sender, out row);
359        if (row != null) {
360          Series rowSeries = chart.Series[row.Name];
361          Series regressionSeries = seriesToRegressionSeriesTable[rowSeries];
362          if (!invisibleSeries.Contains(rowSeries)) {
363            rowSeries.Points.Clear();
364            regressionSeries.Points.Clear();
365            FillSeriesWithRowValues(rowSeries, row);
366            FillRegressionSeries(regressionSeries, row);
367            RecalculateMinMaxPointValues();
368            RecalculateAxesScale(chart.ChartAreas[0]);
369            UpdateYCursorInterval();
370          }
371        }
372      }
373    }
374    private void Points_ItemsRemoved(object sender, CollectionItemsChangedEventArgs<IndexedItem<Point2D<double>>> e) {
375      if (InvokeRequired)
376        Invoke(new CollectionItemsChangedEventHandler<IndexedItem<Point2D<double>>>(Points_ItemsRemoved), sender, e);
377      else {
378        ScatterPlotDataRow row = null;
379        pointsRowsTable.TryGetValue((IObservableList<Point2D<double>>)sender, out row);
380        if (row != null) {
381          Series rowSeries = chart.Series[row.Name];
382          Series regressionSeries = seriesToRegressionSeriesTable[rowSeries];
383          if (!invisibleSeries.Contains(rowSeries)) {
384            rowSeries.Points.Clear();
385            regressionSeries.Points.Clear();
386            FillSeriesWithRowValues(rowSeries, row);
387            FillRegressionSeries(regressionSeries, row);
388            RecalculateMinMaxPointValues();
389            RecalculateAxesScale(chart.ChartAreas[0]);
390            UpdateYCursorInterval();
391          }
392        }
393      }
394    }
395    private void Points_ItemsReplaced(object sender, CollectionItemsChangedEventArgs<IndexedItem<Point2D<double>>> e) {
396      if (InvokeRequired)
397        Invoke(new CollectionItemsChangedEventHandler<IndexedItem<Point2D<double>>>(Points_ItemsReplaced), sender, e);
398      else {
399        ScatterPlotDataRow row = null;
400        pointsRowsTable.TryGetValue((IObservableList<Point2D<double>>)sender, out row);
401        if (row != null) {
402          Series rowSeries = chart.Series[row.Name];
403          Series regressionSeries = seriesToRegressionSeriesTable[rowSeries];
404          if (!invisibleSeries.Contains(rowSeries)) {
405            rowSeries.Points.Clear();
406            regressionSeries.Points.Clear();
407            FillSeriesWithRowValues(rowSeries, row);
408            FillRegressionSeries(regressionSeries, row);
409            RecalculateMinMaxPointValues();
410            RecalculateAxesScale(chart.ChartAreas[0]);
411            UpdateYCursorInterval();
412          }
413        }
414      }
415    }
416    private void Points_CollectionReset(object sender, CollectionItemsChangedEventArgs<IndexedItem<Point2D<double>>> e) {
417      if (InvokeRequired)
418        Invoke(new CollectionItemsChangedEventHandler<IndexedItem<Point2D<double>>>(Points_CollectionReset), sender, e);
419      else {
420        ScatterPlotDataRow row = null;
421        pointsRowsTable.TryGetValue((IObservableList<Point2D<double>>)sender, out row);
422        if (row != null) {
423          Series rowSeries = chart.Series[row.Name];
424          Series regressionSeries = seriesToRegressionSeriesTable[rowSeries];
425          if (!invisibleSeries.Contains(rowSeries)) {
426            rowSeries.Points.Clear();
427            regressionSeries.Points.Clear();
428            FillSeriesWithRowValues(rowSeries, row);
429            FillRegressionSeries(regressionSeries, row);
430            RecalculateMinMaxPointValues();
431            RecalculateAxesScale(chart.ChartAreas[0]);
432            UpdateYCursorInterval();
433          }
434        }
435      }
436    }
437    #endregion
438    private void configureToolStripMenuItem_Click(object sender, System.EventArgs e) {
439      ShowConfiguration();
440    }
441    #endregion
442
443    #region Chart Event Handlers
444    private void chart_MouseDoubleClick(object sender, MouseEventArgs e) {
445      HitTestResult result = chart.HitTest(e.X, e.Y,ChartElementType.DataPoint);
446      if (result.ChartElementType != ChartElementType.DataPoint) return;
447
448      var series = result.Series;
449      var dataPoint = series.Points[result.PointIndex];
450      var tag = dataPoint.Tag;
451      var content = tag as IContent;
452
453      if (tag == null) return;
454      if (content == null) return;
455
456      MainFormManager.MainForm.ShowContent(content);
457    }
458
459    private void chart_MouseDown(object sender, MouseEventArgs e) {
460      HitTestResult result = chart.HitTest(e.X, e.Y);
461      if (result.ChartElementType == ChartElementType.LegendItem) {
462        ToggleSeriesVisible(result.Series);
463      }
464    }
465    private void chart_MouseMove(object sender, MouseEventArgs e) {
466      HitTestResult result = chart.HitTest(e.X, e.Y);
467      if (result.ChartElementType == ChartElementType.LegendItem)
468        this.Cursor = Cursors.Hand;
469      else
470        this.Cursor = Cursors.Default;
471    }
472    private void chart_CustomizeLegend(object sender, CustomizeLegendEventArgs e) {
473      foreach (LegendItem legendItem in e.LegendItems) {
474        var series = chart.Series[legendItem.SeriesName];
475        if (series != null) {
476          bool seriesIsInvisible = invisibleSeries.Contains(series);
477          foreach (LegendCell cell in legendItem.Cells) {
478            cell.ForeColor = seriesIsInvisible ? Color.Gray : Color.Black;
479          }
480        }
481      }
482    }
483    #endregion
484
485    private void ToggleSeriesVisible(Series series) {
486      if (!invisibleSeries.Contains(series)) {
487        series.Points.Clear();
488        invisibleSeries.Add(series);
489        RecalculateMinMaxPointValues();
490      } else {
491        invisibleSeries.Remove(series);
492        if (Content != null) {
493          var row = (ScatterPlotDataRow)series.Tag;
494          if (seriesToRegressionSeriesTable.ContainsKey(series))
495            FillSeriesWithRowValues(series, row);
496          else
497            FillRegressionSeries(series, row);
498          RecalculateMinMaxPointValues();
499          this.chart.Legends[series.Legend].ForeColor = Color.Black;
500          RecalculateAxesScale(chart.ChartAreas[0]);
501          UpdateYCursorInterval();
502        }
503      }
504    }
505
506    private void RecalculateMinMaxPointValues() {
507      yMin = xMin = double.MaxValue;
508      yMax = xMax = double.MinValue;
509      foreach (var s in chart.Series.Where(x => x.Enabled)) {
510        foreach (var p in s.Points) {
511          double x = p.XValue, y = p.YValues[0];
512          if (xMin > x) xMin = x;
513          if (xMax < x) xMax = x;
514          if (yMin > y) yMin = y;
515          if (yMax < y) yMax = y;
516        }
517      }
518    }
519
520    private void FillSeriesWithRowValues(Series series, ScatterPlotDataRow row) {
521      bool zerosOnly = true;
522      for (int i = 0; i < row.Points.Count; i++) {
523        var value = row.Points[i];
524        DataPoint point = new DataPoint();
525        if (IsInvalidValue(value.X) || IsInvalidValue(value.Y))
526          point.IsEmpty = true;
527        else {
528          point.XValue = value.X;
529          point.YValues = new double[] { value.Y };
530        }
531        point.Tag = value.Tag;
532        series.Points.Add(point);
533        if (value.X != 0.0f)
534          zerosOnly = false;
535      }
536      if (zerosOnly) // if all x-values are zero, the x-values are interpreted as 1, 2, 3, ...
537        series.Points.Add(new DataPoint(1, 1) { IsEmpty = true });
538      double correlation = Correlation(row.Points);
539      series.LegendToolTip = string.Format("Correlation (R²) = {0:G4}", correlation * correlation);
540    }
541
542    private void FillRegressionSeries(Series regressionSeries, ScatterPlotDataRow row) {
543      if (row.VisualProperties.RegressionType == ScatterPlotDataRowVisualProperties.ScatterPlotDataRowRegressionType.None
544        || invisibleSeries.Contains(regressionSeries))
545        return;
546
547      double[] coefficients;
548      if (!Fitting(row, out coefficients))
549        return;
550
551      // Fill regrssion series
552      var validPoints = row.Points.Where(p => !IsInvalidValue(p.X));
553      double min = validPoints.Min(p => p.X), max = validPoints.Max(p => p.X);
554      double range = max - min, delta = range / row.Points.Count;
555      for (double x = min; x < max; x += delta) {
556        regressionSeries.Points.AddXY(x, Estimate(x, row, coefficients));
557      }
558
559      // Correlation
560      var data = row.Points.Select(p => new Point2D<double>(p.Y, Estimate(p.X, row, coefficients)));
561      double correlation = Correlation(data.ToList());
562      regressionSeries.LegendToolTip = GetStringFormula(row, coefficients) + Environment.NewLine +
563                                       string.Format("Correlation (R²) = {0:G4}", correlation * correlation);
564      regressionSeries.ToolTip = GetStringFormula(row, coefficients);
565    }
566
567    #region Helpers
568    private MarkerStyle ConvertPointStyle(ScatterPlotDataRowVisualProperties.ScatterPlotDataRowPointStyle pointStyle) {
569      switch (pointStyle) {
570        case ScatterPlotDataRowVisualProperties.ScatterPlotDataRowPointStyle.Circle:
571          return MarkerStyle.Circle;
572        case ScatterPlotDataRowVisualProperties.ScatterPlotDataRowPointStyle.Cross:
573          return MarkerStyle.Cross;
574        case ScatterPlotDataRowVisualProperties.ScatterPlotDataRowPointStyle.Diamond:
575          return MarkerStyle.Diamond;
576        case ScatterPlotDataRowVisualProperties.ScatterPlotDataRowPointStyle.Square:
577          return MarkerStyle.Square;
578        case ScatterPlotDataRowVisualProperties.ScatterPlotDataRowPointStyle.Star4:
579          return MarkerStyle.Star4;
580        case ScatterPlotDataRowVisualProperties.ScatterPlotDataRowPointStyle.Star5:
581          return MarkerStyle.Star5;
582        case ScatterPlotDataRowVisualProperties.ScatterPlotDataRowPointStyle.Star6:
583          return MarkerStyle.Star6;
584        case ScatterPlotDataRowVisualProperties.ScatterPlotDataRowPointStyle.Star10:
585          return MarkerStyle.Star10;
586        case ScatterPlotDataRowVisualProperties.ScatterPlotDataRowPointStyle.Triangle:
587          return MarkerStyle.Triangle;
588        default:
589          return MarkerStyle.None;
590      }
591    }
592
593    protected static bool IsInvalidValue(double x) {
594      return double.IsNaN(x) || x < (double)decimal.MinValue || x > (double)decimal.MaxValue;
595    }
596    #endregion
597
598    #region Correlation and Fitting Helper
599    protected static double Correlation(IList<Point2D<double>> values) {
600      // sums of x, y, x squared etc.
601      double sx = 0.0;
602      double sy = 0.0;
603      double sxx = 0.0;
604      double syy = 0.0;
605      double sxy = 0.0;
606
607      int n = 0;
608      for (int i = 0; i < values.Count; i++) {
609        double x = values[i].X;
610        double y = values[i].Y;
611        if (IsInvalidValue(x) || IsInvalidValue(y))
612          continue;
613
614        sx += x;
615        sy += y;
616        sxx += x * x;
617        syy += y * y;
618        sxy += x * y;
619        n++;
620      }
621
622      // covariation
623      double cov = sxy / n - sx * sy / n / n;
624      // standard error of x
625      double sigmaX = Math.Sqrt(sxx / n - sx * sx / n / n);
626      // standard error of y
627      double sigmaY = Math.Sqrt(syy / n - sy * sy / n / n);
628
629      // correlation
630      return cov / sigmaX / sigmaY;
631    }
632
633    protected static bool Fitting(ScatterPlotDataRow row, out double[] coefficients) {
634      var xs = row.Points.Select(p => p.X).ToList();
635      var ys = row.Points.Select(p => p.Y).ToList();
636
637      // Input transformations
638      double[,] matrix;
639      int nRows;
640      switch (row.VisualProperties.RegressionType) {
641        case ScatterPlotDataRowVisualProperties.ScatterPlotDataRowRegressionType.Linear:
642          matrix = CreateMatrix(out nRows, ys, xs);
643          break;
644        case ScatterPlotDataRowVisualProperties.ScatterPlotDataRowRegressionType.Polynomial:
645          var xss = Enumerable.Range(1, row.VisualProperties.PolynomialRegressionOrder)
646            .Select(o => xs.Select(x => Math.Pow(x, o)).ToList())
647            .Reverse(); // higher order first
648          matrix = CreateMatrix(out nRows, ys, xss.ToArray());
649          break;
650        case ScatterPlotDataRowVisualProperties.ScatterPlotDataRowRegressionType.Exponential:
651          matrix = CreateMatrix(out nRows, ys.Select(y => Math.Log(y)).ToList(), xs);
652          break;
653        case ScatterPlotDataRowVisualProperties.ScatterPlotDataRowRegressionType.Power:
654          matrix = CreateMatrix(out nRows, ys.Select(y => Math.Log(y)).ToList(), xs.Select(x => Math.Log(x)).ToList());
655          break;
656        case ScatterPlotDataRowVisualProperties.ScatterPlotDataRowRegressionType.Logarithmic:
657          matrix = CreateMatrix(out nRows, ys, xs.Select(x => Math.Log(x)).ToList());
658          break;
659        default:
660          throw new ArgumentException("Unknown RegressionType: " + row.VisualProperties.RegressionType);
661      }
662
663      // Linear fitting
664      bool success = LinearFitting(matrix, nRows, out coefficients);
665      if (!success) return success;
666
667      // Output transformation
668      switch (row.VisualProperties.RegressionType) {
669        case ScatterPlotDataRowVisualProperties.ScatterPlotDataRowRegressionType.Exponential:
670        case ScatterPlotDataRowVisualProperties.ScatterPlotDataRowRegressionType.Power:
671          coefficients[1] = Math.Exp(coefficients[1]);
672          break;
673      }
674
675      return true;
676    }
677    protected static double[,] CreateMatrix(out int nRows, IList<double> ys, params IList<double>[] xss) {
678      var matrix = new double[ys.Count, xss.Length + 1];
679      int rowIdx = 0;
680      for (int i = 0; i < ys.Count; i++) {
681        if (IsInvalidValue(ys[i]) || xss.Any(xs => IsInvalidValue(xs[i])))
682          continue;
683        for (int j = 0; j < xss.Length; j++) {
684          matrix[rowIdx, j] = xss[j][i];
685        }
686        matrix[rowIdx, xss.Length] = ys[i];
687        rowIdx++;
688      }
689      nRows = rowIdx;
690      return matrix;
691    }
692
693    protected static bool LinearFitting(double[,] xsy, int nRows, out double[] coefficients) {
694      int nFeatures = xsy.GetLength(1) - 1;
695
696      alglib.linearmodel lm;
697      alglib.lrreport ar;
698      int retVal;
699      alglib.lrbuild(xsy, nRows, nFeatures, out retVal, out lm, out ar);
700      if (retVal != 1) {
701        coefficients = new double[0];
702        return false;
703      }
704
705      alglib.lrunpack(lm, out coefficients, out nFeatures);
706      return true;
707    }
708
709    protected static double Estimate(double x, ScatterPlotDataRow row, double[] coefficients) {
710      switch (row.VisualProperties.RegressionType) {
711        case ScatterPlotDataRowVisualProperties.ScatterPlotDataRowRegressionType.Linear:
712          return coefficients[0] * x + coefficients[1];
713        case ScatterPlotDataRowVisualProperties.ScatterPlotDataRowRegressionType.Polynomial:
714          return coefficients
715            .Reverse() // to match index and order
716            .Select((c, o) => c * Math.Pow(x, o))
717            .Sum();
718        case ScatterPlotDataRowVisualProperties.ScatterPlotDataRowRegressionType.Exponential:
719          return coefficients[1] * Math.Exp(coefficients[0] * x);
720        case ScatterPlotDataRowVisualProperties.ScatterPlotDataRowRegressionType.Power:
721          return coefficients[1] * Math.Pow(x, coefficients[0]);
722        case ScatterPlotDataRowVisualProperties.ScatterPlotDataRowRegressionType.Logarithmic:
723          return coefficients[0] * Math.Log(x) + coefficients[1];
724        default:
725          throw new ArgumentException("Unknown RegressionType: " + row.VisualProperties.RegressionType);
726      }
727    }
728
729    protected static string GetStringFormula(ScatterPlotDataRow row, double[] coefficients) {
730      switch (row.VisualProperties.RegressionType) {
731        case ScatterPlotDataRowVisualProperties.ScatterPlotDataRowRegressionType.Linear:
732          return string.Format("{0:G4} x {1} {2:G4}", coefficients[0], Sign(coefficients[1]), Math.Abs(coefficients[1]));
733        case ScatterPlotDataRowVisualProperties.ScatterPlotDataRowRegressionType.Polynomial:
734          var sb = new StringBuilder();
735          sb.AppendFormat("{0:G4}{1}", coefficients[0], PolyFactor(coefficients.Length - 1));
736          foreach (var x in coefficients
737            .Reverse() // match index and order
738            .Select((c, o) => new { c, o })
739            .Reverse() // higher order first
740            .Skip(1)) // highest order poly already added
741            sb.AppendFormat(" {0} {1:G4}{2}", Sign(x.c), Math.Abs(x.c), PolyFactor(x.o));
742          return sb.ToString();
743        case ScatterPlotDataRowVisualProperties.ScatterPlotDataRowRegressionType.Exponential:
744          return string.Format("{0:G4} e^({1:G4} x)", coefficients[1], coefficients[0]);
745        case ScatterPlotDataRowVisualProperties.ScatterPlotDataRowRegressionType.Power:
746          return string.Format("{0:G4} x^({1:G4})", coefficients[1], coefficients[0]);
747        case ScatterPlotDataRowVisualProperties.ScatterPlotDataRowRegressionType.Logarithmic:
748          return string.Format("{0:G4} ln(x) {1} {2:G4}", coefficients[0], Sign(coefficients[1]), Math.Abs(coefficients[1]));
749        default:
750          throw new ArgumentException("Unknown RegressionType: " + row.VisualProperties.RegressionType);
751      }
752    }
753    private static string Sign(double value) {
754      return value >= 0 ? "+" : "-";
755    }
756    private static string PolyFactor(int order) {
757      if (order == 0) return "";
758      if (order == 1) return " x";
759      if (order == 2) return " x²";
760      if (order == 3) return " x³";
761      return " x^" + order;
762    }
763    #endregion
764  }
765}
Note: See TracBrowser for help on using the repository browser.