source: trunk/sources/HeuristicLab.Visualization/LineChart.cs @ 1350

Last change on this file since 1350 was 1350, checked in by mstoeger, 12 years ago

Implemented multiple Y-Axes. A LineChart has several Y-Axes and each Y-Axis has several data rows. The same clipping area is set for all data rows belonging to a Y-Axis. (#433)

File size: 19.8 KB
Line 
1using System;
2using System.Collections.Generic;
3using System.Drawing;
4using System.Windows.Forms;
5using HeuristicLab.Core;
6using HeuristicLab.Visualization.Legend;
7using HeuristicLab.Visualization.Options;
8
9namespace HeuristicLab.Visualization {
10  public partial class LineChart : ViewBase {
11    private readonly IChartDataRowsModel model;
12    private readonly Canvas canvas;
13
14    private readonly TextShape titleShape = new TextShape("Title");
15    private readonly LegendShape legendShape = new LegendShape();
16    private readonly XAxis xAxis = new XAxis();
17    private readonly List<RowEntry> rowEntries = new List<RowEntry>();
18
19    private readonly Dictionary<IDataRow, RowEntry> rowToRowEntry = new Dictionary<IDataRow, RowEntry>();
20
21    private readonly ViewSettings viewSettings;
22
23//    private readonly Stack<RectangleD> clippingAreaHistory = new Stack<RectangleD>();
24    private readonly WorldShape userInteractionShape = new WorldShape();
25    private readonly RectangleShape rectangleShape = new RectangleShape(0, 0, 0, 0, Color.FromArgb(50, 0, 0, 255));
26    private IMouseEventListener mouseEventListener;
27
28    private const int YAxisWidth = 100;
29    private const int XAxisHeight = 20;
30
31    /// <summary>
32    /// This constructor shouldn't be called. Only required for the designer.
33    /// </summary>
34    public LineChart() {
35      InitializeComponent();
36    }
37
38    /// <summary>
39    /// Initializes the chart.
40    /// </summary>
41    /// <param name="model">Referenz to the model, for data</param>
42    public LineChart(IChartDataRowsModel model) : this() {
43      if (model == null) {
44        throw new NullReferenceException("Model cannot be null.");
45      }
46
47      canvas = canvasUI.Canvas;
48
49      this.model = model;
50      viewSettings = model.ViewSettings;
51      viewSettings.OnUpdateSettings += UpdateViewSettings;
52
53      Item = model;
54
55      UpdateLayout();
56      canvasUI.Resize += delegate { UpdateLayout(); };
57
58      ZoomToFullView();
59    }
60
61    /// <summary>
62    /// updates the view settings
63    /// </summary>
64    private void UpdateViewSettings() {
65      titleShape.Font = viewSettings.TitleFont;
66      titleShape.Color = viewSettings.TitleColor;
67
68      legendShape.Font = viewSettings.LegendFont;
69      legendShape.Color = viewSettings.LegendColor;
70
71      xAxis.Font = viewSettings.XAxisFont;
72      xAxis.Color = viewSettings.XAxisColor;
73
74      SetLegendPosition();
75
76      canvasUI.Invalidate();
77    }
78
79    /// <summary>
80    /// Layout management - arranges the inner shapes.
81    /// </summary>
82    private void UpdateLayout() {
83      canvas.ClearShapes();
84
85      foreach (YAxisDescriptor yAxisDescriptor in model.YAxes) {
86        YAxisInfo info = GetYAxisInfo(yAxisDescriptor);
87        canvas.AddShape(info.Grid);
88      }
89
90      foreach (RowEntry rowEntry in rowEntries) {
91        canvas.AddShape(rowEntry.LinesShape);
92      }
93
94      canvas.AddShape(xAxis);
95
96      int yAxesWidth = 0;
97
98      foreach (YAxisDescriptor yAxisDescriptor in model.YAxes) {
99        YAxisInfo info = GetYAxisInfo(yAxisDescriptor);
100        if (yAxisDescriptor.ShowYAxis) {
101          canvas.AddShape(info.YAxis);
102          yAxesWidth += YAxisWidth;
103        }
104      }
105
106      canvas.AddShape(titleShape);
107      canvas.AddShape(legendShape);
108
109      canvas.AddShape(userInteractionShape);
110
111      titleShape.X = 10;
112      titleShape.Y = canvasUI.Height - 10;
113
114      RectangleD linesAreaBoundingBox = new RectangleD(yAxesWidth,
115                                                       XAxisHeight,
116                                                       canvasUI.Width,
117                                                       canvasUI.Height);
118
119      foreach (RowEntry rowEntry in rowEntries) {
120        rowEntry.LinesShape.BoundingBox = linesAreaBoundingBox;
121      }
122
123      foreach (YAxisDescriptor yAxisDescriptor in model.YAxes) {
124        YAxisInfo info = GetYAxisInfo(yAxisDescriptor);
125        info.Grid.BoundingBox = linesAreaBoundingBox;
126      }
127
128      int yAxisLeft = 0;
129      foreach (YAxisDescriptor yAxisDescriptor in model.YAxes) {
130        YAxisInfo info = GetYAxisInfo(yAxisDescriptor);
131        if (yAxisDescriptor.ShowYAxis) {
132          info.YAxis.BoundingBox = new RectangleD(yAxisLeft,
133                                                 linesAreaBoundingBox.Y1,
134                                                 yAxisLeft + YAxisWidth,
135                                                 linesAreaBoundingBox.Y2);
136          yAxisLeft += YAxisWidth;
137        }
138      }
139
140      userInteractionShape.BoundingBox = linesAreaBoundingBox;
141      userInteractionShape.ClippingArea = new RectangleD(0, 0, userInteractionShape.BoundingBox.Width, userInteractionShape.BoundingBox.Height);
142
143      xAxis.BoundingBox = new RectangleD(linesAreaBoundingBox.X1,
144                                         0,
145                                         linesAreaBoundingBox.X2,
146                                         linesAreaBoundingBox.Y1);
147
148      SetLegendPosition();
149    }
150
151    private readonly Dictionary<YAxisDescriptor, YAxisInfo> yAxisInfos = new Dictionary<YAxisDescriptor, YAxisInfo>();
152
153    private YAxisInfo GetYAxisInfo(YAxisDescriptor yAxisDescriptor) {
154      YAxisInfo info;
155
156      if (!yAxisInfos.TryGetValue(yAxisDescriptor, out info)) {
157        info = new YAxisInfo();
158        yAxisInfos[yAxisDescriptor] = info;
159      }
160
161      return info;
162    }
163
164    private void UpdateYAxisInfo(YAxisDescriptor yAxisDescriptor) {
165//      if (yAxisInfos.ContainsKey(yAxisDescriptor)) {
166//        yAxisInfos.Remove(yAxisDescriptor);
167//      }
168    }
169
170    /// <summary>
171    /// sets the legend position
172    /// </summary>
173    private void SetLegendPosition() {
174      switch (viewSettings.LegendPosition) {
175        case LegendPosition.Bottom:
176          setLegendBottom();
177          break;
178
179        case LegendPosition.Top:
180          setLegendTop();
181          break;
182
183        case LegendPosition.Left:
184          setLegendLeft();
185          break;
186
187        case LegendPosition.Right:
188          setLegendRight();
189          break;
190      }
191    }
192
193    public void setLegendRight() {
194      // legend right
195      legendShape.BoundingBox = new RectangleD(canvasUI.Width - 110, 10, canvasUI.Width, canvasUI.Height - 50);
196      legendShape.ClippingArea = new RectangleD(0, 0, legendShape.BoundingBox.Width, legendShape.BoundingBox.Height);
197      legendShape.Row = false;
198      legendShape.CreateLegend();
199    }
200
201    public void setLegendLeft() {
202      // legend left
203      legendShape.BoundingBox = new RectangleD(10, 10, 110, canvasUI.Height - 50);
204      legendShape.ClippingArea = new RectangleD(0, 0, legendShape.BoundingBox.Width, legendShape.BoundingBox.Height);
205      legendShape.Row = false;
206      legendShape.CreateLegend();
207
208      canvasUI.Invalidate();
209    }
210
211    public void setLegendTop() {
212      // legend top
213      legendShape.BoundingBox = new RectangleD(100, canvasUI.Height - canvasUI.Height, canvasUI.Width, canvasUI.Height - 10);
214      legendShape.ClippingArea = new RectangleD(0, 0, legendShape.BoundingBox.Width, legendShape.BoundingBox.Height);
215      legendShape.Row = true;
216      legendShape.Top = true;
217      legendShape.CreateLegend();
218    }
219
220    public void setLegendBottom() {
221      // legend bottom
222      legendShape.BoundingBox = new RectangleD(100, 10, canvasUI.Width, 200);
223      legendShape.ClippingArea = new RectangleD(0, 0, legendShape.BoundingBox.Width, legendShape.BoundingBox.Height);
224      legendShape.Row = true;
225      legendShape.Top = false;
226      legendShape.CreateLegend();
227    }
228
229    private void optionsToolStripMenuItem_Click(object sender, EventArgs e) {
230      OptionsDialog optionsdlg = new OptionsDialog(model);
231      optionsdlg.ShowDialog(this);
232      Invalidate();
233    }
234
235    public void OnDataRowChanged(IDataRow row) {
236      UpdateYAxisInfo(row.YAxis);
237
238      RowEntry rowEntry = rowToRowEntry[row];
239
240      rowEntry.LinesShape.UpdateStyle(row);
241
242      UpdateLayout();
243
244      canvasUI.Invalidate();
245    }
246
247    #region Add-/RemoveItemEvents
248
249    protected override void AddItemEvents() {
250      base.AddItemEvents();
251
252      model.DataRowAdded += OnDataRowAdded;
253      model.DataRowRemoved += OnDataRowRemoved;
254      model.ModelChanged += OnModelChanged;
255
256      foreach (IDataRow row in model.Rows) {
257        OnDataRowAdded(row);
258      }
259    }
260
261    protected override void RemoveItemEvents() {
262      base.RemoveItemEvents();
263
264      model.DataRowAdded -= OnDataRowAdded;
265      model.DataRowRemoved -= OnDataRowRemoved;
266      model.ModelChanged -= OnModelChanged;
267    }
268
269    private void OnDataRowAdded(IDataRow row) {
270      row.ValueChanged += OnRowValueChanged;
271      row.ValuesChanged += OnRowValuesChanged;
272      row.DataRowChanged += OnDataRowChanged;
273
274      legendShape.AddLegendItem(new LegendItem(row.Label, row.Color, row.Thickness));
275      legendShape.CreateLegend();
276
277      InitLineShapes(row);
278
279      UpdateLayout();
280    }
281
282    private void OnDataRowRemoved(IDataRow row) {
283      row.ValueChanged -= OnRowValueChanged;
284      row.ValuesChanged -= OnRowValuesChanged;
285      row.DataRowChanged -= OnDataRowChanged;
286
287      rowToRowEntry.Remove(row);
288      rowEntries.RemoveAll(delegate(RowEntry rowEntry) { return rowEntry.DataRow == row; });
289
290      UpdateLayout();
291    }
292
293    #endregion
294
295    public void ZoomToFullView() {
296      SetClipX(-0.1, model.MaxDataRowValues - 0.9);
297
298      foreach (RowEntry rowEntry in rowEntries) {
299        YAxisDescriptor yAxisDescriptor = rowEntry.DataRow.YAxis;
300
301        SetClipY(rowEntry,
302                 yAxisDescriptor.MinValue - ((yAxisDescriptor.MaxValue - yAxisDescriptor.MinValue)*0.05),
303                 yAxisDescriptor.MaxValue + ((yAxisDescriptor.MaxValue - yAxisDescriptor.MinValue)*0.05));
304      }
305
306      canvasUI.Invalidate();
307    }
308
309    private void SetClipX(double x1, double x2) {
310      xAxis.ClippingArea = new RectangleD(x1,
311                                          0,
312                                          x2,
313                                          XAxisHeight);
314
315      foreach (RowEntry rowEntry in rowEntries) {
316        rowEntry.LinesShape.ClippingArea = new RectangleD(x1,
317                                                          rowEntry.LinesShape.ClippingArea.Y1,
318                                                          x2,
319                                                          rowEntry.LinesShape.ClippingArea.Y2);
320      }
321
322      foreach (YAxisDescriptor yAxisDescriptor in model.YAxes) {
323        YAxisInfo info = GetYAxisInfo(yAxisDescriptor);
324        info.Grid.ClippingArea = new RectangleD(x1,
325                                                info.Grid.ClippingArea.Y1,
326                                                x2,
327                                                info.Grid.ClippingArea.Y2);
328        info.YAxis.ClippingArea = new RectangleD(0,
329                                                 info.YAxis.ClippingArea.Y1,
330                                                 YAxisWidth,
331                                                 info.YAxis.ClippingArea.Y2);
332      }
333    }
334
335    private void SetClipY(RowEntry rowEntry, double y1, double y2) {
336      rowEntry.LinesShape.ClippingArea = new RectangleD(rowEntry.LinesShape.ClippingArea.X1,
337                                                        y1,
338                                                        rowEntry.LinesShape.ClippingArea.X2,
339                                                        y2);
340
341      YAxisInfo info = GetYAxisInfo(rowEntry.DataRow.YAxis);
342
343      info.Grid.ClippingArea = new RectangleD(info.Grid.ClippingArea.X1,
344                                              y1,
345                                              info.Grid.ClippingArea.X2,
346                                              y2);
347      info.YAxis.ClippingArea = new RectangleD(info.YAxis.ClippingArea.X1,
348                                               y1,
349                                               info.YAxis.ClippingArea.X2,
350                                               y2);
351    }
352
353    private void InitLineShapes(IDataRow row) {
354      RowEntry rowEntry = new RowEntry(row);
355      rowEntries.Add(rowEntry);
356      rowToRowEntry[row] = rowEntry;
357
358      if ((row.LineType == DataRowType.SingleValue)) {
359        if (row.Count > 0) {
360          LineShape lineShape = new HorizontalLineShape(0, row[0], double.MaxValue, row[0], row.Color, row.Thickness,
361                                                        row.Style);
362          rowEntry.LinesShape.AddShape(lineShape);
363        }
364      } else {
365        for (int i = 1; i < row.Count; i++) {
366          LineShape lineShape = new LineShape(i - 1, row[i - 1], i, row[i], row.Color, row.Thickness, row.Style);
367          rowEntry.LinesShape.AddShape(lineShape);
368        }
369      }
370
371      ZoomToFullView();
372    }
373
374    private void OnRowValueChanged(IDataRow row, double value, int index, Action action) {
375      RowEntry rowEntry = rowToRowEntry[row];
376
377      if (row.LineType == DataRowType.SingleValue) {
378        if (action == Action.Added) {
379          LineShape lineShape = new HorizontalLineShape(0, row[0], double.MaxValue, row[0], row.Color, row.Thickness,
380                                                        row.Style);
381          rowEntry.LinesShape.AddShape(lineShape);
382        } else {
383          LineShape lineShape = rowEntry.LinesShape.GetShape(0);
384          lineShape.Y1 = value;
385          lineShape.Y2 = value;
386        }
387      } else {
388        if (index > rowEntry.LinesShape.Count + 1) {
389          throw new NotImplementedException();
390        }
391
392        // new value was added
393        if (index > 0 && index == rowEntry.LinesShape.Count + 1) {
394          LineShape lineShape = new LineShape(index - 1, row[index - 1], index, row[index], row.Color, row.Thickness, row.Style);
395          rowEntry.LinesShape.AddShape(lineShape);
396        }
397
398        // not the first value
399        if (index > 0) {
400          rowEntry.LinesShape.GetShape(index - 1).Y2 = value;
401        }
402
403        // not the last value
404        if (index > 0 && index < row.Count - 1) {
405          rowEntry.LinesShape.GetShape(index).Y1 = value;
406        }
407      }
408
409      ZoomToFullView();
410    }
411
412    private void OnRowValuesChanged(IDataRow row, double[] values, int index, Action action) {
413      foreach (double value in values) {
414        OnRowValueChanged(row, value, index++, action);
415      }
416    }
417
418    private void OnModelChanged() {
419      titleShape.Text = model.Title;
420
421      canvasUI.Invalidate();
422    }
423
424    #region Begin-/EndUpdate
425
426    private int beginUpdateCount;
427
428    public void BeginUpdate() {
429      beginUpdateCount++;
430    }
431
432    public void EndUpdate() {
433      if (beginUpdateCount == 0) {
434        throw new InvalidOperationException("Too many EndUpdates.");
435      }
436
437      beginUpdateCount--;
438
439      if (beginUpdateCount == 0) {
440        canvasUI.Invalidate();
441      }
442    }
443
444    #endregion
445
446    #region Zooming / Panning
447
448    private void Pan(Point startPoint, Point endPoint) {
449      foreach (RowEntry rowEntry in rowEntries) {
450        RectangleD clippingArea = CalcPanClippingArea(startPoint, endPoint, rowEntry.LinesShape);
451
452        SetClipX(clippingArea.X1, clippingArea.X1);
453        SetClipY(rowEntry, clippingArea.Y1, clippingArea.Y2);
454      }
455
456      canvasUI.Invalidate();
457    }
458
459    private void PanEnd(Point startPoint, Point endPoint) {
460      foreach (RowEntry rowEntry in rowEntries) {
461        RectangleD clippingArea = CalcPanClippingArea(startPoint, endPoint, rowEntry.LinesShape);
462
463        SetClipX(clippingArea.X1, clippingArea.X1);
464        SetClipY(rowEntry, clippingArea.Y1, clippingArea.Y2);
465      }
466
467      canvasUI.Invalidate();
468    }
469
470    private static RectangleD CalcPanClippingArea(Point startPoint, Point endPoint, LinesShape linesShape) {
471      return Translate.ClippingArea(startPoint, endPoint, linesShape.ClippingArea, linesShape.Viewport);
472    }
473
474    private void SetClippingArea(Rectangle rectangle) {
475      foreach (RowEntry rowEntry in rowEntries) {
476        RectangleD clippingArea = Transform.ToWorld(rectangle, rowEntry.LinesShape.Viewport, rowEntry.LinesShape.ClippingArea);
477
478        SetClipX(clippingArea.X1, clippingArea.X1);
479        SetClipY(rowEntry, clippingArea.Y1, clippingArea.Y2);
480      }
481
482      userInteractionShape.RemoveShape(rectangleShape);
483      canvasUI.Invalidate();
484    }
485
486    private void DrawRectangle(Rectangle rectangle) {
487      rectangleShape.Rectangle = Transform.ToWorld(rectangle, userInteractionShape.Viewport, userInteractionShape.ClippingArea);
488      canvasUI.Invalidate();
489    }
490
491    private void canvasUI1_KeyDown(object sender, KeyEventArgs e) {
492//      if (e.KeyCode == Keys.Back && clippingAreaHistory.Count > 1) {
493//        clippingAreaHistory.Pop();
494//
495//        RectangleD clippingArea = clippingAreaHistory.Peek();
496//
497//        SetLineClippingArea(clippingArea, false);
498//      }
499    }
500
501    private void canvasUI1_MouseDown(object sender, MouseEventArgs e) {
502      Focus();
503
504      if (e.Button == MouseButtons.Right) {
505        contextMenuStrip1.Show(PointToScreen(e.Location));
506      } else if (e.Button == MouseButtons.Left) {
507        if (ModifierKeys == Keys.None) {
508          PanListener panListener = new PanListener(e.Location);
509          panListener.Pan += Pan;
510          panListener.PanEnd += PanEnd;
511
512          mouseEventListener = panListener;
513        } else if (ModifierKeys == Keys.Control) {
514          ZoomListener zoomListener = new ZoomListener(e.Location);
515          zoomListener.DrawRectangle += DrawRectangle;
516          zoomListener.SetClippingArea += SetClippingArea;
517
518          rectangleShape.Rectangle = RectangleD.Empty;
519          userInteractionShape.AddShape(rectangleShape);
520
521          mouseEventListener = zoomListener;
522        }
523      }
524    }
525
526    private void canvasUI_MouseMove(object sender, MouseEventArgs e) {
527      if (mouseEventListener != null) {
528        mouseEventListener.MouseMove(sender, e);
529      }
530    }
531
532    private void canvasUI_MouseUp(object sender, MouseEventArgs e) {
533      if (mouseEventListener != null) {
534        mouseEventListener.MouseUp(sender, e);
535      }
536
537      mouseEventListener = null;
538    }
539
540    private void canvasUI1_MouseWheel(object sender, MouseEventArgs e) {
541      if (ModifierKeys == Keys.Control) {
542        double zoomFactor = (e.Delta > 0) ? 0.9 : 1.1;
543
544        foreach (RowEntry rowEntry in rowEntries) {
545          RectangleD clippingArea = ZoomListener.ZoomClippingArea(rowEntry.LinesShape.ClippingArea, zoomFactor);
546         
547          SetClipX(clippingArea.X1, clippingArea.X1);
548          SetClipY(rowEntry, clippingArea.Y1, clippingArea.Y2);
549        }
550      }
551    }
552
553    #endregion
554
555    private class LinesShape : WorldShape {
556      public void UpdateStyle(IDataRow row) {
557        foreach (IShape shape in shapes) {
558          LineShape lineShape = shape as LineShape;
559          if (lineShape != null) {
560            lineShape.LSColor = row.Color;
561            lineShape.LSDrawingStyle = row.Style;
562            lineShape.LSThickness = row.Thickness;
563          }
564        }
565      }
566
567      public int Count {
568        get { return shapes.Count; }
569      }
570
571      public LineShape GetShape(int index) {
572        return (LineShape)shapes[index];
573      }
574    }
575
576    private class RowEntry {
577      private readonly IDataRow dataRow;
578
579      private readonly LinesShape linesShape = new LinesShape();
580
581      public RowEntry(IDataRow dataRow) {
582        this.dataRow = dataRow;
583      }
584
585      public IDataRow DataRow {
586        get { return dataRow; }
587      }
588
589      public LinesShape LinesShape {
590        get { return linesShape; }
591      }
592    }
593
594    private class YAxisInfo {
595      private readonly Grid grid = new Grid();
596      private readonly YAxis yAxis = new YAxis();
597
598      public Grid Grid {
599        get { return grid; }
600      }
601
602      public YAxis YAxis {
603        get { return yAxis; }
604      }
605    }
606  }
607}
Note: See TracBrowser for help on using the repository browser.