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

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

Fixed Zoom & Pan. Improved Mouse-Wheel-Zoom. (#424)

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