source: branches/BubbleChart/HeuristicLab.Optimization.BubbleChart/3.3/BubbleChartView.cs @ 12881

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

#2379 Added jitter for x and y axis.

File size: 22.9 KB
Line 
1#region License Information
2/* HeuristicLab
3 * Copyright (C) 2002-2015 Heuristic and Evolutionary Algorithms Laboratory (HEAL)
4 *
5 * This file is part of HeuristicLab.
6 *
7 * HeuristicLab is free software: you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation, either version 3 of the License, or
10 * (at your option) any later version.
11 *
12 * HeuristicLab is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with HeuristicLab. If not, see <http://www.gnu.org/licenses/>.
19 */
20#endregion
21
22using System;
23using System.Collections.Generic;
24using System.Linq;
25using System.Windows.Forms;
26using System.Windows.Forms.DataVisualization.Charting;
27using HeuristicLab.Common;
28using HeuristicLab.Core;
29using HeuristicLab.Core.Views;
30using HeuristicLab.Data;
31using HeuristicLab.MainForm;
32
33namespace HeuristicLab.Optimization.BubbleChart {
34  [View("Bubble Chart (Recursive)")]
35  [Content(typeof(RecursiveDataItem), true)]
36  public partial class BubbleChartView : ItemView {
37    private const string Separator = ":";
38
39    private enum SizeDimension { Constant = 0 }
40    private enum AxisDimension { Index = 0 }
41
42    private readonly Dictionary<RecursiveDataItem, int> itemToIndexMapping = new Dictionary<RecursiveDataItem, int>();
43    private readonly Dictionary<string, Dictionary<object, double>> categoricalMapping = new Dictionary<string, Dictionary<object, double>>();
44
45    private readonly Dictionary<RecursiveDataItem, double> xJitter = new Dictionary<RecursiveDataItem, double>();
46    private readonly Dictionary<RecursiveDataItem, double> yJitter = new Dictionary<RecursiveDataItem, double>();
47    private readonly Random random = new Random();
48
49
50    private string XAxisValue { get { return (string)xAxisComboBox.SelectedItem; } }
51    private string YAxisValue { get { return (string)yAxisComboBox.SelectedItem; } }
52    private string SizeAxisValue { get { return (string)sizeComboBox.SelectedItem; } }
53
54    private bool updating = false;
55
56    private double xJitterFactor = 0.0;
57    private double yJitterFactor = 0.0;
58
59    public new RecursiveDataItem Content {
60      get { return (RecursiveDataItem)base.Content; }
61      set { base.Content = value; }
62    }
63
64    public BubbleChartView() {
65      InitializeComponent();
66
67      chart.CustomizeAllChartAreas();
68      chart.ChartAreas[0].CursorX.Interval = 1;
69      chart.ChartAreas[0].CursorY.Interval = 1;
70      chart.ChartAreas[0].AxisX.ScaleView.Zoomable = true;
71      chart.ChartAreas[0].AxisY.ScaleView.Zoomable = true;
72    }
73
74    protected override void OnContentChanged() {
75      base.OnContentChanged();
76
77      UpdateTreeView();
78      UpdateLevelControl();
79
80      UpdateComboBoxes();
81      UpdateDataPoints();
82      //UpdateCaption();
83    }
84    protected override void SetEnabledStateOfControls() {
85      base.SetEnabledStateOfControls();
86      levelNumericUpDown.Enabled = Content != null;
87      xJitterTrackBar.Enabled = Content != null;
88      yJitterTrackBar.Enabled = Content != null;
89      xAxisComboBox.Enabled = Content != null;
90      yAxisComboBox.Enabled = Content != null;
91    }
92
93    #region Tree Queries
94    private IEnumerable<RecursiveDataItem> GetAvailableItems() {
95      return IterateCheckedNodes()
96        .Select(n => (RecursiveDataItem)n.Tag);
97    }
98    private IEnumerable<string> GetAvailableKeys() {
99      var collector = new List<string>();
100      GetAvailableKeys(Content, collector);
101      return collector.Distinct();
102    }
103    private void GetAvailableKeys(RecursiveDataItem node, List<string> collector) {
104      collector.AddRange(node.Data.Keys);
105
106      foreach (var child in node.Children)
107        GetAvailableKeys(child, collector);
108    }
109    #endregion
110
111    #region Update Controls
112    private void UpdateLevelControl() {
113      if (Content == null) return;
114      if (treeView.Nodes.Count > 0)
115        levelNumericUpDown.Maximum = IterateAllNodes().Max(t => t.Level);
116      else
117        levelNumericUpDown.Maximum = 0;
118    }
119
120    private void UpdateTreeView() {
121      treeView.Nodes.Clear();
122      if (Content != null)
123        treeView.Nodes.Add(CreateTreeNode(Content));
124      treeView.ExpandAll();
125      splitContainer.Panel1Collapsed = treeView.Nodes.Count == 0;
126      if (treeView.Nodes.Count > 0)
127        treeView.SelectedNode = treeView.Nodes[0];
128    }
129    private TreeNode CreateTreeNode(RecursiveDataItem item) {
130      var node = new TreeNode(item.Name) {
131        Tag = item,
132        Checked = true
133      };
134      foreach (var child in item.Children)
135        node.Nodes.Add(CreateTreeNode(child));
136      return node;
137    }
138
139    private void UpdateTreeViewCheckBoxes() {
140      int level = (int)levelNumericUpDown.Value;
141      bool includeChildren = includeChildrenCheckBox.Checked;
142      foreach (var node in IterateAllNodes()) {
143        bool @checked = includeChildren ? node.Level >= level : node.Level == level;
144        if (node.Checked != @checked)
145          node.Checked = @checked;
146      }
147    }
148
149    private void UpdateComboBoxes() {
150      var selectedXAxis = (string)xAxisComboBox.SelectedItem;
151      var selectedYAxis = (string)yAxisComboBox.SelectedItem;
152      var selectedSizeAxis = (string)sizeComboBox.SelectedItem;
153      xAxisComboBox.Items.Clear();
154      yAxisComboBox.Items.Clear();
155      sizeComboBox.Items.Clear();
156
157      if (Content != null) {
158        var axisNames = GetAvailableKeys().ToArray();
159        var additionalAxisDimension = Enum.GetNames(typeof(AxisDimension));
160        var additionalSizeDimension = Enum.GetNames(typeof(SizeDimension));
161
162        //xAxisComboBox.Items.AddRange(additionalAxisDimension);
163        xAxisComboBox.Items.AddRange(axisNames);
164        //yAxisComboBox.Items.AddRange(additionalAxisDimension);
165        yAxisComboBox.Items.AddRange(axisNames);
166        sizeComboBox.Items.AddRange(additionalSizeDimension);
167        sizeComboBox.Items.AddRange(axisNames);
168
169        bool changed = false;
170        if (selectedXAxis != null && xAxisComboBox.Items.Contains(selectedXAxis)) {
171          xAxisComboBox.SelectedItem = selectedXAxis;
172          changed = true;
173        }
174        if (selectedYAxis != null && yAxisComboBox.Items.Contains(selectedYAxis)) {
175          yAxisComboBox.SelectedItem = selectedYAxis;
176          changed = true;
177        }
178        if (selectedSizeAxis != null && sizeComboBox.Items.Contains(selectedSizeAxis)) {
179          sizeComboBox.SelectedItem = selectedSizeAxis;
180          changed = true;
181        } else sizeComboBox.SelectedItem = SizeDimension.Constant.ToString();
182        if (changed) {
183          UpdateDataPoints();
184          UpdateAxisLabels();
185        }
186      }
187    }
188
189    private void UpdateDataPoints() {
190      var series = chart.Series[0];
191      series.Points.Clear();
192      itemToIndexMapping.Clear();
193      categoricalMapping.Clear();
194      RebuildInverseIndex();
195
196      chart.ChartAreas[0].AxisX.IsMarginVisible = !(XAxisValue != null && XAxisValue.Contains(AxisDimension.Index.ToString()));
197      chart.ChartAreas[0].AxisY.IsMarginVisible = !(YAxisValue != null && YAxisValue.Contains(AxisDimension.Index.ToString()));
198
199      if (Content == null || string.IsNullOrEmpty(XAxisValue) || string.IsNullOrEmpty(YAxisValue)) return;
200
201      var xss = GetValues(XAxisValue);
202      var yss = GetValues(YAxisValue);
203
204      if (xss.Count == yss.Count) {
205        for (int i = 0; i < xss.Count; i++)
206          AddPoints(xss[i], yss[i], series);
207      } else if (xss.Count == 1 || yss.Count == 1) {
208        for (int i = 0; i < xss.Count; i++)
209          for (int j = 0; j < yss.Count; j++)
210            AddPoints(xss[i], yss[j], series);
211      }
212
213      xJitterTrackBar.Value = 0;
214      yJitterTrackBar.Value = 0;
215
216      //needed to set axis back to automatic and refresh them, otherwise their values may remain NaN
217      var xAxis = chart.ChartAreas[0].AxisX;
218      var yAxis = chart.ChartAreas[0].AxisY;
219      SetAutomaticUpdateOfAxis(xAxis, true);
220      SetAutomaticUpdateOfAxis(yAxis, true);
221      chart.Refresh();
222    }
223    void AddPoints(Tuple<double[], RecursiveDataItem> xs, Tuple<double[], RecursiveDataItem> ys, Series series) {
224      if (xs.Item1.Length != ys.Item1.Length) return;
225      for (int k = 0; k < xs.Item1.Length; k++) {
226        series.Points.Add(new DataPoint(xs.Item1[k], new[] { ys.Item1[k], 1.0 }) {
227          Tag = Tuple.Create(xs.Item1[k], ys.Item1[k], xs.Item2, ys.Item2)
228        });
229      }
230    }
231
232    private List<Tuple<double[], RecursiveDataItem>> GetValues(string key) {
233      var collector = new List<Tuple<double[], RecursiveDataItem>>();
234      GetValues(Content, key, collector);
235      return collector;
236    }
237    private void GetValues(RecursiveDataItem node, string key, List<Tuple<double[], RecursiveDataItem>> collector) {
238      IItem item;
239      if (node.Data.TryGetValue(key, out item)) {
240        var value = ConvertToDoubles(node, key);
241        collector.Add(Tuple.Create(value, node));
242      }
243
244      foreach (var child in node.Children)
245        GetValues(child, key, collector);
246    }
247
248    private double[] ConvertToDoubles(RecursiveDataItem item, string key) {
249      IItem value = item.Data[key];
250      var doubleValue = value as DoubleValue;
251      var doubleArray = value as DoubleArray;
252      var intValue = value as IntValue;
253      var intArray = value as IntArray;
254      var timeSpanValue = value as TimeSpanValue;
255      if (doubleValue != null) return new double[1] { doubleValue.Value };
256      if (intValue != null) return new double[1] { intValue.Value };
257      if (timeSpanValue != null) return new double[1] { timeSpanValue.Value.TotalSeconds };
258      if (doubleArray != null) return doubleArray.ToArray();
259      if (intArray != null) return intArray.Select(v => (double)v).ToArray();
260      return new double[1] { GetCategoricalValue(key, value.ToString()).Value };
261    }
262
263    private IEnumerable<IEnumerable<double>> GetValues(IEnumerable<RecursiveDataItem> items, string key) {
264      if (key == null) yield break;
265      var keyTokens = key.Split(new[] { Separator }, StringSplitOptions.RemoveEmptyEntries);
266      if (keyTokens.Length == 1) {
267        if (Enum.IsDefined(typeof(SizeDimension), key)) {
268          var sizeDimension = (SizeDimension)Enum.Parse(typeof(SizeDimension), key);
269          yield return GetValue(null, sizeDimension).ToEnumerable();
270        } else
271          foreach (var item in items) {
272            if (Enum.IsDefined(typeof(AxisDimension), key)) {
273              var axisDimension = (AxisDimension)Enum.Parse(typeof(AxisDimension), key);
274              yield return GetValue(item, axisDimension).ToEnumerable();
275            } else if (item.Data.ContainsKey(key)) {
276              yield return item.Data[key] as IEnumerable<double> ?? ConvertToDouble(item, key).ToEnumerable();
277            }
278          }
279      } else if (keyTokens.Length == 2) {
280        string parentName = keyTokens[0];
281        key = keyTokens[1];
282        foreach (var item in items.Where(item => item.Name == parentName)) {
283          foreach (var child in item.Children.Where(child => child.Data.ContainsKey(key))) {
284            yield return child.Data[key] as IEnumerable<double> ?? ConvertToDouble(child, key).ToEnumerable();
285          }
286        }
287      } else throw new InvalidOperationException("key either contains a single key or a child/data key");
288
289    }
290
291    private IEnumerable<double> GetValues(RecursiveDataItem parent, string key) {
292      if (parent == null || string.IsNullOrEmpty(key))
293        return Enumerable.Empty<double>();
294      var tokens = key.Split(new[] { Separator }, StringSplitOptions.RemoveEmptyEntries);
295      if (tokens.Length == 2) {
296        var parentKey = tokens[0];
297        var dataKey = tokens[1];
298        if (parent.Name == parentKey) {
299          return parent.Children
300            .Where(child => child.Data.ContainsKey(dataKey))
301            .Select(child => ConvertToDouble(child, dataKey));
302        }
303        return Enumerable.Empty<double>();
304      } else {
305        var item = parent;
306        if (item == null || string.IsNullOrEmpty(key))
307          return Enumerable.Empty<double>();
308
309        if (Enum.IsDefined(typeof(AxisDimension), key)) {
310          var axisDimension = (AxisDimension)Enum.Parse(typeof(AxisDimension), key);
311          return GetValue(item, axisDimension).ToEnumerable();
312        } else if (Enum.IsDefined(typeof(SizeDimension), key)) {
313          var sizeDimension = (SizeDimension)Enum.Parse(typeof(SizeDimension), key);
314          return GetValue(item, sizeDimension).ToEnumerable();
315        } else if (item.Data.ContainsKey(key)) {
316
317          return ConvertToDouble(item, key).ToEnumerable();
318        } else {
319          return Enumerable.Empty<double>();
320        }
321      }
322    }
323
324    private double ConvertToDouble(RecursiveDataItem item, string key) {
325      IItem value = item.Data[key];
326      var doubleValue = value as DoubleValue;
327      var intValue = value as IntValue;
328      var timeSpanValue = value as TimeSpanValue;
329      double? ret = null;
330      if (doubleValue != null) {
331        if (!double.IsNaN(doubleValue.Value) && !double.IsInfinity(doubleValue.Value))
332          ret = doubleValue.Value;
333      } else if (intValue != null)
334        ret = intValue.Value;
335      else if (timeSpanValue != null)
336        ret = timeSpanValue.Value.TotalSeconds;
337      else
338        ret = GetCategoricalValue(key, value.ToString());
339      return ret.Value;
340    }
341
342    private double? GetCategoricalValue(string key, string value) {
343      if (!categoricalMapping.ContainsKey(key)) {
344        categoricalMapping[key] = new Dictionary<object, double>();
345        var orderedCategories =
346          GetAvailableItems().Where(x => x.Data.ContainsKey(key)).Select(x => x.Data[key].ToString())
347            .Distinct().OrderBy(x => x, new NaturalStringComparer());
348        int count = 1;
349        foreach (var category in orderedCategories) {
350          categoricalMapping[key].Add(category, count);
351          count++;
352        }
353      }
354      if (!this.categoricalMapping[key].ContainsKey(value)) return null;
355      return this.categoricalMapping[key][value];
356    }
357    private double GetValue(RecursiveDataItem item, AxisDimension axisDimension) {
358      double value = double.NaN;
359      switch (axisDimension) {
360        case AxisDimension.Index:
361          value = itemToIndexMapping[item];
362          break;
363        default:
364          throw new ArgumentException("No handling strategy for " + axisDimension.ToString() + " is defined.");
365      }
366      return value;
367    }
368    private double GetValue(RecursiveDataItem item, SizeDimension sizeDimension) {
369      double value = double.NaN;
370      switch (sizeDimension) {
371        case SizeDimension.Constant:
372          value = 5;
373          break;
374        default:
375          throw new ArgumentException("No handling strategy for " + sizeDimension.ToString() + " is defined.");
376      }
377      return value;
378    }
379    private void RebuildInverseIndex() {
380      if (Content != null) {
381        itemToIndexMapping.Clear();
382        int i = 0;
383        foreach (var item in GetAvailableItems()) {
384          // ToDo: do not add if key (which one?) is not present within item
385          itemToIndexMapping.Add(item, i);
386          i++;
387        }
388      }
389    }
390
391    private void UpdateAxisLabels() {
392      Axis xAxis = chart.ChartAreas[0].AxisX;
393      Axis yAxis = chart.ChartAreas[0].AxisY;
394      //mkommend: combobox.SelectedIndex could not be used as this changes during hovering over possible values
395      var xSAxisSelected = XAxisValue == null ? null : (string)xAxisComboBox.SelectedItem;
396      var ySAxisSelected = YAxisValue == null ? null : (string)yAxisComboBox.SelectedItem;
397      SetCustomAxisLabels(xAxis, xSAxisSelected);
398      SetCustomAxisLabels(yAxis, ySAxisSelected);
399      if (XAxisValue != null)
400        xAxis.Title = XAxisValue;
401      if (YAxisValue != null)
402        yAxis.Title = YAxisValue;
403    }
404    private void chart_AxisViewChanged(object sender, System.Windows.Forms.DataVisualization.Charting.ViewEventArgs e) {
405      this.UpdateAxisLabels();
406    }
407    private void SetCustomAxisLabels(Axis axis, string key) {
408      if (key == null) return;
409      var tokens = key.Split(new[] { Separator }, StringSplitOptions.RemoveEmptyEntries);
410      if (tokens.Length == 2)
411        key = tokens[1];
412      axis.CustomLabels.Clear();
413      if (categoricalMapping.ContainsKey(key)) {
414        foreach (var pair in categoricalMapping[key]) {
415          string labelText = pair.Key.ToString();
416          CustomLabel label = new CustomLabel();
417          label.ToolTip = labelText;
418          if (labelText.Length > 25)
419            labelText = labelText.Substring(0, 25) + " ... ";
420          label.Text = labelText;
421          label.GridTicks = GridTickTypes.TickMark;
422          label.FromPosition = pair.Value - 0.5;
423          label.ToPosition = pair.Value + 0.5;
424          axis.CustomLabels.Add(label);
425        }
426      } else if (Content.Data.ContainsKey(key) && Content.Data[key] is TimeSpanValue) { // TODO
427        chart.ChartAreas[0].RecalculateAxesScale();
428        for (double i = axis.Minimum; i <= axis.Maximum; i += axis.LabelStyle.Interval) {
429          TimeSpan time = TimeSpan.FromSeconds(i);
430          string x = string.Format("{0:00}:{1:00}:{2:00}", time.Hours, time.Minutes, time.Seconds);
431          axis.CustomLabels.Add(i - axis.LabelStyle.Interval / 2, i + axis.LabelStyle.Interval / 2, x);
432        }
433      }
434    }
435
436    private void UpdateMarkerSizes() {
437      var series = chart.Series[0];
438      if (series.Points.Count <= 0) return;
439
440      var sizeValues = series.Points.Select(p => p.YValues[1]);
441      double minSizeValue = sizeValues.Min();
442      double maxSizeValue = sizeValues.Max();
443      double sizeRange = maxSizeValue - minSizeValue;
444
445      const int smallestBubbleSize = 5;
446
447      foreach (DataPoint point in series.Points) {
448        //calculates the relative size of the data point  0 <= relativeSize <= 1
449        double relativeSize = (point.YValues[1] - minSizeValue);
450        if (sizeRange > double.Epsilon) {
451          relativeSize /= sizeRange;
452
453          //invert bubble sizes if the value of the trackbar is negative
454          if (sizeTrackBar.Value < 0) relativeSize = Math.Abs(relativeSize - 1);
455        } else relativeSize = 1;
456
457        double sizeChange = Math.Abs(sizeTrackBar.Value) * relativeSize;
458        point.MarkerSize = (int)Math.Round(sizeChange + smallestBubbleSize);
459      }
460    }
461
462    private void UpdateDataPointJitter() {
463      var xAxis = this.chart.ChartAreas[0].AxisX;
464      var yAxis = this.chart.ChartAreas[0].AxisY;
465
466      double xAxisRange = xAxis.Maximum - xAxis.Minimum;
467      double yAxisRange = yAxis.Maximum - yAxis.Minimum;
468
469      foreach (DataPoint point in chart.Series[0].Points) {
470        var tag = (Tuple<double, double, RecursiveDataItem, RecursiveDataItem>)point.Tag;
471        var xItem = tag.Item3;
472        var yItem = tag.Item4;
473        double xValue = tag.Item1;
474        double yValue = tag.Item2;
475
476        if (!xJitterFactor.IsAlmost(0.0))
477          xValue += 0.1 * GetXJitter(xItem) * xJitterFactor * (xAxisRange);
478        if (!yJitterFactor.IsAlmost(0.0))
479          yValue += 0.1 * GetYJitter(yItem) * yJitterFactor * (yAxisRange);
480
481        point.XValue = xValue;
482        point.YValues[0] = yValue;
483      }
484
485      if (xJitterFactor.IsAlmost(0.0) && yJitterFactor.IsAlmost(0.0)) {
486        SetAutomaticUpdateOfAxis(xAxis, true);
487        SetAutomaticUpdateOfAxis(yAxis, true);
488        chart.ChartAreas[0].RecalculateAxesScale();
489      } else {
490        SetAutomaticUpdateOfAxis(xAxis, false);
491        SetAutomaticUpdateOfAxis(yAxis, false);
492      }
493    }
494    private double GetXJitter(RecursiveDataItem item) {
495      if (!this.xJitter.ContainsKey(item))
496        this.xJitter[item] = random.NextDouble() * 2.0 - 1.0;
497      return this.xJitter[item];
498    }
499    private double GetYJitter(RecursiveDataItem item) {
500      if (!this.yJitter.ContainsKey(item))
501        this.yJitter[item] = random.NextDouble() * 2.0 - 1.0;
502      return this.yJitter[item];
503    }
504
505    // sets an axis to automatic or restrains it to its current values
506    // this is used that none of the set values is changed when jitter is applied, so that the chart stays the same
507    private void SetAutomaticUpdateOfAxis(Axis axis, bool enabled) {
508      if (enabled) {
509        axis.Maximum = double.NaN;
510        axis.Minimum = double.NaN;
511        axis.MajorGrid.Interval = double.NaN;
512        axis.MajorTickMark.Interval = double.NaN;
513        axis.LabelStyle.Interval = double.NaN;
514      } else {
515        axis.Minimum = axis.Minimum;
516        axis.Maximum = axis.Maximum;
517        axis.MajorGrid.Interval = axis.MajorGrid.Interval;
518        axis.MajorTickMark.Interval = axis.MajorTickMark.Interval;
519        axis.LabelStyle.Interval = axis.LabelStyle.Interval;
520      }
521    }
522    #endregion
523
524    #region Event Handlers
525    private void treeView_AfterCheck(object sender, TreeViewEventArgs e) {
526      if (updating) return;
527      updating = true;
528      UpdateComboBoxes();
529      UpdateDataPoints();
530      updating = false;
531    }
532    private void treeView_AfterSelect(object sender, TreeViewEventArgs e) {
533      //UpdateNodeColors();
534      //UpdateComboBoxes();
535      //UpdateDataPoints();
536    }
537
538    private void axisComboBox_SelectedValueChanged(object sender, EventArgs e) {
539      if (updating) return;
540      updating = true;
541      UpdateDataPoints();
542      UpdateAxisLabels();
543      updating = false;
544    }
545
546    private void includeChildrenCheckBox_CheckedChanged(object sender, EventArgs e) {
547      if (updating) return;
548      updating = true;
549      UpdateTreeViewCheckBoxes();
550      UpdateComboBoxes();
551      updating = false;
552    }
553
554    private void levelNumericUpDown_ValueChanged(object sender, EventArgs e) {
555      if (updating) return;
556      updating = true;
557      UpdateTreeViewCheckBoxes();
558      UpdateComboBoxes();
559      updating = false;
560    }
561
562    private void sizeTrackBar_ValueChanged(object sender, EventArgs e) {
563      UpdateMarkerSizes();
564    }
565
566    private void jitterTrackBar_ValueChanged(object sender, EventArgs e) {
567      this.xJitterFactor = xJitterTrackBar.Value / 100.0;
568      this.yJitterFactor = yJitterTrackBar.Value / 100.0;
569      UpdateDataPointJitter();
570    }
571    #endregion
572
573    #region Helpers
574    private IEnumerable<TreeNode> IterateAllNodes() {
575      return Collect(treeView.Nodes);
576    }
577    private IEnumerable<TreeNode> IterateCheckedNodes() {
578      return IterateAllNodes().Where(t => t.Checked);
579    }
580    private IEnumerable<TreeNode> Collect(TreeNodeCollection nodes) {
581      foreach (TreeNode node in nodes) {
582        yield return node;
583        foreach (var child in Collect(node.Nodes))
584          yield return child;
585      }
586    }
587    #endregion
588  }
589}
Note: See TracBrowser for help on using the repository browser.