Free cookie consent management tool by TermsFeed Policy Generator

source: branches/DataPreprocessing Enhancements/HeuristicLab.DataPreprocessing.Views/3.4/PreprocessingScatterPlotView.cs @ 14445

Last change on this file since 14445 was 14381, checked in by pfleck, 7 years ago

#2698

  • Refactored CheckedVariablesView out of the ChartView to allow reuse of the checked variables list.
    • The new list visualizes the non-input/target variables in gray.
    • Added context menu to quickly (un)check all variables or only the inputs+target variables.
  • In the Multi-Scatterplot
    • New structure and layout of the single charts to support fixed header rows and columns (for the variable names). Instead, removed the legend of each plot for better usage of plot area.
    • Adapted the new CheckedVariablesView (but hidden until (un)checking is implemented).
File size: 21.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.Windows.Forms;
27using System.Windows.Forms.DataVisualization.Charting;
28using HeuristicLab.Analysis;
29using HeuristicLab.Analysis.Views;
30using HeuristicLab.Collections;
31using HeuristicLab.Common;
32using HeuristicLab.Core.Views;
33using HeuristicLab.MainForm;
34
35namespace HeuristicLab.DataPreprocessing.Views {
36  [View("Preprocessing ScatterPlot View")]
37  [Content(typeof(ScatterPlot), false)]
38  public partial class PreprocessingScatterPlotView : ItemView, IConfigureableView {
39    protected List<Series> invisibleSeries;
40    protected Dictionary<IObservableList<Point2D<double>>, ScatterPlotDataRow> pointsRowsTable;
41    private event EventHandler chartDoubleClick;
42
43    public new ScatterPlot Content {
44      get { return (ScatterPlot)base.Content; }
45      set { base.Content = value; }
46    }
47
48    public bool ShowLegend {
49      get { return chart.Legends[0].Enabled; }
50      set { chart.Legends[0].Enabled = value; }
51    }
52
53    public string XAxisFormat {
54      get { return chart.ChartAreas[0].AxisX.LabelStyle.Format; }
55      set { chart.ChartAreas[0].AxisX.LabelStyle.Format = value; }
56    }
57    public string YAxisFormat {
58      get { return chart.ChartAreas[0].AxisY.LabelStyle.Format; }
59      set { chart.ChartAreas[0].AxisY.LabelStyle.Format = value; }
60    }
61
62    public PreprocessingScatterPlotView() {
63      InitializeComponent();
64      pointsRowsTable = new Dictionary<IObservableList<Point2D<double>>, ScatterPlotDataRow>();
65      invisibleSeries = new List<Series>();
66      chart.CustomizeAllChartAreas();
67      chart.ChartAreas[0].CursorX.Interval = 1;
68    }
69
70    #region Event Handler Registration
71    protected override void DeregisterContentEvents() {
72      foreach (ScatterPlotDataRow row in Content.Rows)
73        DeregisterScatterPlotDataRowEvents(row);
74      Content.VisualPropertiesChanged -= new EventHandler(Content_VisualPropertiesChanged);
75      Content.Rows.ItemsAdded -= new CollectionItemsChangedEventHandler<ScatterPlotDataRow>(Rows_ItemsAdded);
76      Content.Rows.ItemsRemoved -= new CollectionItemsChangedEventHandler<ScatterPlotDataRow>(Rows_ItemsRemoved);
77      Content.Rows.ItemsReplaced -= new CollectionItemsChangedEventHandler<ScatterPlotDataRow>(Rows_ItemsReplaced);
78      Content.Rows.CollectionReset -= new CollectionItemsChangedEventHandler<ScatterPlotDataRow>(Rows_CollectionReset);
79      base.DeregisterContentEvents();
80    }
81    protected override void RegisterContentEvents() {
82      base.RegisterContentEvents();
83      Content.VisualPropertiesChanged += new EventHandler(Content_VisualPropertiesChanged);
84      Content.Rows.ItemsAdded += new CollectionItemsChangedEventHandler<ScatterPlotDataRow>(Rows_ItemsAdded);
85      Content.Rows.ItemsRemoved += new CollectionItemsChangedEventHandler<ScatterPlotDataRow>(Rows_ItemsRemoved);
86      Content.Rows.ItemsReplaced += new CollectionItemsChangedEventHandler<ScatterPlotDataRow>(Rows_ItemsReplaced);
87      Content.Rows.CollectionReset += new CollectionItemsChangedEventHandler<ScatterPlotDataRow>(Rows_CollectionReset);
88    }
89
90    protected virtual void RegisterScatterPlotDataRowEvents(ScatterPlotDataRow row) {
91      row.NameChanged += new EventHandler(Row_NameChanged);
92      row.VisualPropertiesChanged += new EventHandler(Row_VisualPropertiesChanged);
93      pointsRowsTable.Add(row.Points, row);
94      row.Points.ItemsAdded += new CollectionItemsChangedEventHandler<IndexedItem<Point2D<double>>>(Points_ItemsAdded);
95      row.Points.ItemsRemoved += new CollectionItemsChangedEventHandler<IndexedItem<Point2D<double>>>(Points_ItemsRemoved);
96      row.Points.ItemsReplaced += new CollectionItemsChangedEventHandler<IndexedItem<Point2D<double>>>(Points_ItemsReplaced);
97      row.Points.CollectionReset += new CollectionItemsChangedEventHandler<IndexedItem<Point2D<double>>>(Points_CollectionReset);
98    }
99    protected virtual void DeregisterScatterPlotDataRowEvents(ScatterPlotDataRow row) {
100      row.Points.ItemsAdded -= new CollectionItemsChangedEventHandler<IndexedItem<Point2D<double>>>(Points_ItemsAdded);
101      row.Points.ItemsRemoved -= new CollectionItemsChangedEventHandler<IndexedItem<Point2D<double>>>(Points_ItemsRemoved);
102      row.Points.ItemsReplaced -= new CollectionItemsChangedEventHandler<IndexedItem<Point2D<double>>>(Points_ItemsReplaced);
103      row.Points.CollectionReset -= new CollectionItemsChangedEventHandler<IndexedItem<Point2D<double>>>(Points_CollectionReset);
104      pointsRowsTable.Remove(row.Points);
105      row.VisualPropertiesChanged -= new EventHandler(Row_VisualPropertiesChanged);
106      row.NameChanged -= new EventHandler(Row_NameChanged);
107    }
108    #endregion
109
110    protected override void OnContentChanged() {
111      base.OnContentChanged();
112      invisibleSeries.Clear();
113      chart.ChartAreas[0].AxisX.Title = string.Empty;
114      chart.ChartAreas[0].AxisY.Title = string.Empty;
115      chart.Series.Clear();
116      if (Content != null) {
117        AddScatterPlotDataRows(Content.Rows);
118        ConfigureChartArea(chart.ChartAreas[0]);
119        RecalculateAxesScale(chart.ChartAreas[0]);
120      }
121    }
122
123    protected override void SetEnabledStateOfControls() {
124      base.SetEnabledStateOfControls();
125      chart.Enabled = Content != null;
126    }
127
128    public void ShowConfiguration() {
129      if (Content != null) {
130        using (ScatterPlotVisualPropertiesDialog dialog = new ScatterPlotVisualPropertiesDialog(Content)) {
131          dialog.ShowDialog(this);
132        }
133      } else MessageBox.Show("Nothing to configure.");
134    }
135
136    protected virtual void AddScatterPlotDataRows(IEnumerable<ScatterPlotDataRow> rows) {
137      foreach (var row in rows) {
138        RegisterScatterPlotDataRowEvents(row);
139        Series series = new Series(row.Name);
140        if (row.VisualProperties.DisplayName.Trim() != String.Empty) series.LegendText = row.VisualProperties.DisplayName;
141        else series.LegendText = row.Name;
142        ConfigureSeries(series, row);
143        FillSeriesWithRowValues(series, row);
144        chart.Series.Add(series);
145      }
146      ConfigureChartArea(chart.ChartAreas[0]);
147      RecalculateAxesScale(chart.ChartAreas[0]);
148      UpdateYCursorInterval();
149    }
150
151    protected virtual void RemoveScatterPlotDataRows(IEnumerable<ScatterPlotDataRow> rows) {
152      foreach (var row in rows) {
153        DeregisterScatterPlotDataRowEvents(row);
154        Series series = chart.Series[row.Name];
155        chart.Series.Remove(series);
156        if (invisibleSeries.Contains(series))
157          invisibleSeries.Remove(series);
158      }
159      RecalculateAxesScale(chart.ChartAreas[0]);
160    }
161
162    private void ConfigureSeries(Series series, ScatterPlotDataRow row) {
163      series.BorderWidth = 1;
164      series.BorderDashStyle = ChartDashStyle.Solid;
165      series.BorderColor = Color.Empty;
166
167      if (row.VisualProperties.Color != Color.Empty)
168        series.Color = row.VisualProperties.Color;
169      else series.Color = Color.Empty;
170      series.IsVisibleInLegend = row.VisualProperties.IsVisibleInLegend;
171      series.ChartType = SeriesChartType.FastPoint;
172      series.MarkerSize = row.VisualProperties.PointSize;
173      series.MarkerStyle = ConvertPointStyle(row.VisualProperties.PointStyle);
174      series.XAxisType = AxisType.Primary;
175      series.YAxisType = AxisType.Primary;
176
177      if (row.VisualProperties.DisplayName.Trim() != String.Empty) series.LegendText = row.VisualProperties.DisplayName;
178      else series.LegendText = row.Name;
179
180      string xAxisTitle = string.IsNullOrEmpty(Content.VisualProperties.XAxisTitle)
181                      ? "X"
182                      : Content.VisualProperties.XAxisTitle;
183      string yAxisTitle = string.IsNullOrEmpty(Content.VisualProperties.YAxisTitle)
184                            ? "Y"
185                            : Content.VisualProperties.YAxisTitle;
186      series.ToolTip =
187        series.LegendText + Environment.NewLine +
188        xAxisTitle + " = " + "#VALX," + Environment.NewLine +
189        yAxisTitle + " = " + "#VAL";
190    }
191
192    private void ConfigureChartArea(ChartArea area) {
193
194      if (Content.VisualProperties.AxisTitleFont != null) area.AxisX.TitleFont = Content.VisualProperties.AxisTitleFont;
195      if (!Content.VisualProperties.AxisTitleColor.IsEmpty) area.AxisX.TitleForeColor = Content.VisualProperties.AxisTitleColor;
196      area.AxisX.Title = Content.VisualProperties.XAxisTitle;
197      area.AxisX.MajorGrid.Enabled = Content.VisualProperties.XAxisGrid;
198
199      if (Content.VisualProperties.AxisTitleFont != null) area.AxisY.TitleFont = Content.VisualProperties.AxisTitleFont;
200      if (!Content.VisualProperties.AxisTitleColor.IsEmpty) area.AxisY.TitleForeColor = Content.VisualProperties.AxisTitleColor;
201      area.AxisY.Title = Content.VisualProperties.YAxisTitle;
202      area.AxisY.MajorGrid.Enabled = Content.VisualProperties.YAxisGrid;
203    }
204
205    private void RecalculateAxesScale(ChartArea area) {
206      // Reset the axes bounds so that RecalculateAxesScale() will assign new bounds
207      foreach (Axis a in area.Axes) {
208        a.Minimum = double.NaN;
209        a.Maximum = double.NaN;
210      }
211      area.RecalculateAxesScale();
212      area.AxisX.IsMarginVisible = false;
213
214      if (!Content.VisualProperties.XAxisMinimumAuto && !double.IsNaN(Content.VisualProperties.XAxisMinimumFixedValue)) area.AxisX.Minimum = Content.VisualProperties.XAxisMinimumFixedValue;
215      if (!Content.VisualProperties.XAxisMaximumAuto && !double.IsNaN(Content.VisualProperties.XAxisMaximumFixedValue)) area.AxisX.Maximum = Content.VisualProperties.XAxisMaximumFixedValue;
216      if (!Content.VisualProperties.YAxisMinimumAuto && !double.IsNaN(Content.VisualProperties.YAxisMinimumFixedValue)) area.AxisY.Minimum = Content.VisualProperties.YAxisMinimumFixedValue;
217      if (!Content.VisualProperties.YAxisMaximumAuto && !double.IsNaN(Content.VisualProperties.YAxisMaximumFixedValue)) area.AxisY.Maximum = Content.VisualProperties.YAxisMaximumFixedValue;
218      if (area.AxisX.Minimum >= area.AxisX.Maximum) area.AxisX.Maximum = area.AxisX.Minimum + 1;
219      if (area.AxisY.Minimum >= area.AxisY.Maximum) area.AxisY.Maximum = area.AxisY.Minimum + 1;
220    }
221
222    protected virtual void UpdateYCursorInterval() {
223      double interestingValuesRange = (
224        from series in chart.Series
225        where series.Enabled
226        let values = (from point in series.Points
227                      where !point.IsEmpty
228                      select point.YValues[0]).DefaultIfEmpty(1.0)
229        let range = values.Max() - values.Min()
230        where range > 0.0
231        select range
232        ).DefaultIfEmpty(1.0).Min();
233
234      double digits = (int)Math.Log10(interestingValuesRange) - 3;
235      double yZoomInterval = Math.Pow(10, digits);
236      this.chart.ChartAreas[0].CursorY.Interval = yZoomInterval;
237    }
238
239
240    public event EventHandler ChartDoubleClick {
241      add { chartDoubleClick += value; }
242      remove { chartDoubleClick -= value; }
243    }
244
245    #region Event Handlers
246    #region Content Event Handlers
247
248    private void Content_VisualPropertiesChanged(object sender, EventArgs e) {
249      if (InvokeRequired)
250        Invoke(new EventHandler(Content_VisualPropertiesChanged), sender, e);
251      else {
252        ConfigureChartArea(chart.ChartAreas[0]);
253        RecalculateAxesScale(chart.ChartAreas[0]); // axes min/max could have changed
254      }
255    }
256    #endregion
257    #region Rows Event Handlers
258    private void Rows_ItemsAdded(object sender, CollectionItemsChangedEventArgs<ScatterPlotDataRow> e) {
259      if (InvokeRequired)
260        Invoke(new CollectionItemsChangedEventHandler<ScatterPlotDataRow>(Rows_ItemsAdded), sender, e);
261      else {
262        AddScatterPlotDataRows(e.Items);
263      }
264    }
265    private void Rows_ItemsRemoved(object sender, CollectionItemsChangedEventArgs<ScatterPlotDataRow> e) {
266      if (InvokeRequired)
267        Invoke(new CollectionItemsChangedEventHandler<ScatterPlotDataRow>(Rows_ItemsRemoved), sender, e);
268      else {
269        RemoveScatterPlotDataRows(e.Items);
270      }
271    }
272    private void Rows_ItemsReplaced(object sender, CollectionItemsChangedEventArgs<ScatterPlotDataRow> e) {
273      if (InvokeRequired)
274        Invoke(new CollectionItemsChangedEventHandler<ScatterPlotDataRow>(Rows_ItemsReplaced), sender, e);
275      else {
276        RemoveScatterPlotDataRows(e.OldItems);
277        AddScatterPlotDataRows(e.Items);
278      }
279    }
280    private void Rows_CollectionReset(object sender, CollectionItemsChangedEventArgs<ScatterPlotDataRow> e) {
281      if (InvokeRequired)
282        Invoke(new CollectionItemsChangedEventHandler<ScatterPlotDataRow>(Rows_CollectionReset), sender, e);
283      else {
284        RemoveScatterPlotDataRows(e.OldItems);
285        AddScatterPlotDataRows(e.Items);
286      }
287    }
288    #endregion
289    #region Row Event Handlers
290    private void Row_VisualPropertiesChanged(object sender, EventArgs e) {
291      if (InvokeRequired)
292        Invoke(new EventHandler(Row_VisualPropertiesChanged), sender, e);
293      else {
294        ScatterPlotDataRow row = (ScatterPlotDataRow)sender;
295        Series series = chart.Series[row.Name];
296        series.Points.Clear();
297        ConfigureSeries(series, row);
298        FillSeriesWithRowValues(series, row);
299        RecalculateAxesScale(chart.ChartAreas[0]);
300      }
301    }
302    private void Row_NameChanged(object sender, EventArgs e) {
303      if (InvokeRequired)
304        Invoke(new EventHandler(Row_NameChanged), sender, e);
305      else {
306        ScatterPlotDataRow row = (ScatterPlotDataRow)sender;
307        chart.Series[row.Name].Name = row.Name;
308      }
309    }
310    #endregion
311    #region Points Event Handlers
312    private void Points_ItemsAdded(object sender, CollectionItemsChangedEventArgs<IndexedItem<Point2D<double>>> e) {
313      if (InvokeRequired)
314        Invoke(new CollectionItemsChangedEventHandler<IndexedItem<Point2D<double>>>(Points_ItemsAdded), sender, e);
315      else {
316        ScatterPlotDataRow row = null;
317        pointsRowsTable.TryGetValue((IObservableList<Point2D<double>>)sender, out row);
318        if (row != null) {
319          Series rowSeries = chart.Series[row.Name];
320          if (!invisibleSeries.Contains(rowSeries)) {
321            rowSeries.Points.Clear();
322            FillSeriesWithRowValues(rowSeries, row);
323            RecalculateAxesScale(chart.ChartAreas[0]);
324            UpdateYCursorInterval();
325          }
326        }
327      }
328    }
329    private void Points_ItemsRemoved(object sender, CollectionItemsChangedEventArgs<IndexedItem<Point2D<double>>> e) {
330      if (InvokeRequired)
331        Invoke(new CollectionItemsChangedEventHandler<IndexedItem<Point2D<double>>>(Points_ItemsRemoved), sender, e);
332      else {
333        ScatterPlotDataRow row = null;
334        pointsRowsTable.TryGetValue((IObservableList<Point2D<double>>)sender, out row);
335        if (row != null) {
336          Series rowSeries = chart.Series[row.Name];
337          if (!invisibleSeries.Contains(rowSeries)) {
338            rowSeries.Points.Clear();
339            FillSeriesWithRowValues(rowSeries, row);
340            RecalculateAxesScale(chart.ChartAreas[0]);
341            UpdateYCursorInterval();
342          }
343        }
344      }
345    }
346    private void Points_ItemsReplaced(object sender, CollectionItemsChangedEventArgs<IndexedItem<Point2D<double>>> e) {
347      if (InvokeRequired)
348        Invoke(new CollectionItemsChangedEventHandler<IndexedItem<Point2D<double>>>(Points_ItemsReplaced), sender, e);
349      else {
350        ScatterPlotDataRow row = null;
351        pointsRowsTable.TryGetValue((IObservableList<Point2D<double>>)sender, out row);
352        if (row != null) {
353          Series rowSeries = chart.Series[row.Name];
354          if (!invisibleSeries.Contains(rowSeries)) {
355            rowSeries.Points.Clear();
356            FillSeriesWithRowValues(rowSeries, row);
357            RecalculateAxesScale(chart.ChartAreas[0]);
358            UpdateYCursorInterval();
359          }
360        }
361      }
362    }
363    private void Points_CollectionReset(object sender, CollectionItemsChangedEventArgs<IndexedItem<Point2D<double>>> e) {
364      if (InvokeRequired)
365        Invoke(new CollectionItemsChangedEventHandler<IndexedItem<Point2D<double>>>(Points_CollectionReset), sender, e);
366      else {
367        ScatterPlotDataRow row = null;
368        pointsRowsTable.TryGetValue((IObservableList<Point2D<double>>)sender, out row);
369        if (row != null) {
370          Series rowSeries = chart.Series[row.Name];
371          if (!invisibleSeries.Contains(rowSeries)) {
372            rowSeries.Points.Clear();
373            FillSeriesWithRowValues(rowSeries, row);
374            RecalculateAxesScale(chart.ChartAreas[0]);
375            UpdateYCursorInterval();
376          }
377        }
378      }
379    }
380    #endregion
381    #endregion
382
383    #region Chart Event Handlers
384    private void chart_MouseDown(object sender, MouseEventArgs e) {
385      HitTestResult result = chart.HitTest(e.X, e.Y);
386      if (result.ChartElementType == ChartElementType.LegendItem) {
387        ToggleSeriesVisible(result.Series);
388      }
389    }
390    private void chart_MouseMove(object sender, MouseEventArgs e) {
391      HitTestResult result = chart.HitTest(e.X, e.Y);
392      if (result.ChartElementType == ChartElementType.LegendItem)
393        this.Cursor = Cursors.Hand;
394      else
395        this.Cursor = Cursors.Default;
396    }
397    private void chart_CustomizeLegend(object sender, CustomizeLegendEventArgs e) {
398      foreach (LegendItem legendItem in e.LegendItems) {
399        var series = chart.Series[legendItem.SeriesName];
400        if (series != null) {
401          bool seriesIsInvisible = invisibleSeries.Contains(series);
402          foreach (LegendCell cell in legendItem.Cells) {
403            cell.ForeColor = seriesIsInvisible ? Color.Gray : Color.Black;
404          }
405        }
406      }
407    }
408    #endregion
409
410    private void ToggleSeriesVisible(Series series) {
411      if (!invisibleSeries.Contains(series)) {
412        series.Points.Clear();
413        invisibleSeries.Add(series);
414      } else {
415        invisibleSeries.Remove(series);
416        if (Content != null) {
417
418          var row = (from r in Content.Rows
419                     where r.Name == series.Name
420                     select r).Single();
421          FillSeriesWithRowValues(series, row);
422          this.chart.Legends[series.Legend].ForeColor = Color.Black;
423          RecalculateAxesScale(chart.ChartAreas[0]);
424          UpdateYCursorInterval();
425        }
426      }
427    }
428
429    private void FillSeriesWithRowValues(Series series, ScatterPlotDataRow row) {
430      for (int i = 0; i < row.Points.Count; i++) {
431        var value = row.Points[i];
432        DataPoint point = new DataPoint();
433        if (IsInvalidValue(value.X) || IsInvalidValue(value.Y))
434          point.IsEmpty = true;
435        else {
436          point.XValue = value.X;
437          point.YValues = new double[] { value.Y };
438        }
439        series.Points.Add(point);
440      }
441    }
442
443    #region Helpers
444    private MarkerStyle ConvertPointStyle(ScatterPlotDataRowVisualProperties.ScatterPlotDataRowPointStyle pointStyle) {
445      switch (pointStyle) {
446        case ScatterPlotDataRowVisualProperties.ScatterPlotDataRowPointStyle.Circle:
447          return MarkerStyle.Circle;
448        case ScatterPlotDataRowVisualProperties.ScatterPlotDataRowPointStyle.Cross:
449          return MarkerStyle.Cross;
450        case ScatterPlotDataRowVisualProperties.ScatterPlotDataRowPointStyle.Diamond:
451          return MarkerStyle.Diamond;
452        case ScatterPlotDataRowVisualProperties.ScatterPlotDataRowPointStyle.Square:
453          return MarkerStyle.Square;
454        case ScatterPlotDataRowVisualProperties.ScatterPlotDataRowPointStyle.Star4:
455          return MarkerStyle.Star4;
456        case ScatterPlotDataRowVisualProperties.ScatterPlotDataRowPointStyle.Star5:
457          return MarkerStyle.Star5;
458        case ScatterPlotDataRowVisualProperties.ScatterPlotDataRowPointStyle.Star6:
459          return MarkerStyle.Star6;
460        case ScatterPlotDataRowVisualProperties.ScatterPlotDataRowPointStyle.Star10:
461          return MarkerStyle.Star10;
462        case ScatterPlotDataRowVisualProperties.ScatterPlotDataRowPointStyle.Triangle:
463          return MarkerStyle.Triangle;
464        default:
465          return MarkerStyle.None;
466      }
467    }
468
469    protected static bool IsInvalidValue(double x) {
470      return double.IsNaN(x) || x < (double)decimal.MinValue || x > (double)decimal.MaxValue;
471    }
472    #endregion
473
474    //bubble double click event with scatter plot view as sender
475    private void chart_DoubleClick(object sender, EventArgs e) {
476      if (chartDoubleClick != null)
477        chartDoubleClick(this, e);
478    }
479  }
480}
Note: See TracBrowser for help on using the repository browser.