source: branches/symbreg-factors-2650/HeuristicLab.Analysis.Views/3.3/ScatterPlotControl.cs @ 14542

Last change on this file since 14542 was 14542, checked in by gkronber, 9 months ago

#2650: merged r14504:14533 from trunk to branch

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