Free cookie consent management tool by TermsFeed Policy Generator

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

Last change on this file since 1285 was 1285, checked in by mstoeger, 15 years ago

Implemented multiple Y-Axes. (#433) Panning & Zooming is broken.

File size: 16.4 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 Stack<RectangleD> clippingAreaHistory = new Stack<RectangleD>();
22    private readonly WorldShape userInteractionShape = new WorldShape();
23    private readonly RectangleShape rectangleShape = new RectangleShape(0, 0, 0, 0, Color.FromArgb(50, 0, 0, 255));
24    private IMouseEventListener mouseEventListener;
25
26    private const int YAxisWidth = 100;
27    private const int XAxisHeight = 20;
28
29    private bool zoomToFullView;
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
51      Item = model;
52
53      UpdateLayout();
54      canvasUI.Resize += delegate { UpdateLayout(); };
55
56      ZoomToFullView();
57    }
58
59    /// <summary>
60    /// Layout management - arranges the inner shapes.
61    /// </summary>
62    private void UpdateLayout() {
63      canvas.ClearShapes();
64
65      foreach (RowEntry rowEntry in rowEntries) {
66        canvas.AddShape(rowEntry.Grid);
67      }
68
69      foreach (RowEntry rowEntry in rowEntries) {
70        canvas.AddShape(rowEntry.LinesShape);
71      }
72
73      canvas.AddShape(xAxis);
74
75      foreach (RowEntry rowEntry in rowEntries) {
76        canvas.AddShape(rowEntry.YAxis);
77      }
78
79      canvas.AddShape(titleShape);
80      canvas.AddShape(legendShape);
81
82      canvas.AddShape(userInteractionShape);
83
84      titleShape.X = 10;
85      titleShape.Y = canvasUI.Height - 10;
86
87      int yAxesWidth = 0;
88
89      foreach (RowEntry rowEntry in rowEntries) {
90        if (rowEntry.YAxis.Visible) {
91          yAxesWidth += YAxisWidth;
92        }
93      }
94
95      RectangleD linesAreaBoundingBox = new RectangleD(yAxesWidth,
96                                                       XAxisHeight,
97                                                       canvasUI.Width,
98                                                       canvasUI.Height);
99
100      foreach (RowEntry rowEntry in rowEntries) {
101        rowEntry.LinesShape.BoundingBox = linesAreaBoundingBox;
102        rowEntry.Grid.BoundingBox = linesAreaBoundingBox;
103      }
104
105      int yAxisLeft = 0;
106      foreach (RowEntry rowEntry in rowEntries) {
107        rowEntry.YAxis.BoundingBox = new RectangleD(yAxisLeft,
108                                                    linesAreaBoundingBox.Y1,
109                                                    yAxisLeft + YAxisWidth,
110                                                    linesAreaBoundingBox.Y2);
111        yAxisLeft += YAxisWidth;
112      }
113
114      userInteractionShape.BoundingBox = linesAreaBoundingBox;
115      userInteractionShape.ClippingArea = new RectangleD(0, 0, userInteractionShape.BoundingBox.Width, userInteractionShape.BoundingBox.Height);
116
117      xAxis.BoundingBox = new RectangleD(linesAreaBoundingBox.X1,
118                                         0,
119                                         linesAreaBoundingBox.X2,
120                                         linesAreaBoundingBox.Y1);
121
122      legendShape.BoundingBox = new RectangleD(10, 10, 110, canvasUI.Height - 50);
123      legendShape.ClippingArea = new RectangleD(0, 0, legendShape.BoundingBox.Width,
124                                                legendShape.BoundingBox.Height);
125
126      canvasUI.Invalidate();
127    }
128
129    private void optionsToolStripMenuItem_Click(object sender, EventArgs e) {
130      OptionsDialog optionsdlg = new OptionsDialog(this.model);
131      optionsdlg.ShowDialog(this);
132    }
133
134    public void OnDataRowChanged(IDataRow row) {
135      RowEntry rowEntry = rowToRowEntry[row];
136
137      rowEntry.LinesShape.UpdateStyle(row);
138
139      canvasUI.Invalidate();
140    }
141
142    #region Add-/RemoveItemEvents
143
144    protected override void AddItemEvents() {
145      base.AddItemEvents();
146
147      model.DataRowAdded += OnDataRowAdded;
148      model.DataRowRemoved += OnDataRowRemoved;
149      model.ModelChanged += OnModelChanged;
150
151      foreach (IDataRow row in model.Rows) {
152        OnDataRowAdded(row);
153      }
154    }
155
156    protected override void RemoveItemEvents() {
157      base.RemoveItemEvents();
158
159      model.DataRowAdded -= OnDataRowAdded;
160      model.DataRowRemoved -= OnDataRowRemoved;
161      model.ModelChanged -= OnModelChanged;
162    }
163
164    private void OnDataRowAdded(IDataRow row) {
165      row.ValueChanged += OnRowValueChanged;
166      row.ValuesChanged += OnRowValuesChanged;
167      row.DataRowChanged += OnDataRowChanged;
168
169      legendShape.AddLegendItem(new LegendItem(row.Label, row.Color, row.Thickness));
170      legendShape.CreateLegend();
171
172      InitLineShapes(row);
173
174      UpdateLayout();
175    }
176
177    private void OnDataRowRemoved(IDataRow row) {
178      row.ValueChanged -= OnRowValueChanged;
179      row.ValuesChanged -= OnRowValuesChanged;
180      row.DataRowChanged -= OnDataRowChanged;
181
182      rowToRowEntry.Remove(row);
183      rowEntries.RemoveAll(delegate(RowEntry rowEntry) { return rowEntry.DataRow == row; });
184
185      UpdateLayout();
186    }
187
188    #endregion
189
190    public void ZoomToFullView() {
191      SetClipX(-0.1, model.MaxDataRowValues - 0.9);
192
193      foreach (RowEntry rowEntry in rowEntries) {
194        IDataRow row = rowEntry.DataRow;
195
196        SetClipY(rowEntry,
197                 row.MinValue - ((row.MaxValue - row.MinValue)*0.05),
198                 row.MaxValue + ((row.MaxValue - row.MinValue)*0.05));
199      }
200
201      zoomToFullView = true;
202
203      canvasUI.Invalidate();
204    }
205
206    private void SetClipX(double x1, double x2) {
207      xAxis.ClippingArea = new RectangleD(x1,
208                                          0,
209                                          x2,
210                                          XAxisHeight);
211
212      foreach (RowEntry rowEntry in rowEntries) {
213        rowEntry.LinesShape.ClippingArea = new RectangleD(x1,
214                                                          rowEntry.LinesShape.ClippingArea.Y1,
215                                                          x2,
216                                                          rowEntry.LinesShape.ClippingArea.Y2);
217        rowEntry.Grid.ClippingArea = new RectangleD(x1,
218                                                    rowEntry.Grid.ClippingArea.Y1,
219                                                    x2,
220                                                    rowEntry.Grid.ClippingArea.Y2);
221        rowEntry.YAxis.ClippingArea = new RectangleD(0,
222                                                     rowEntry.YAxis.ClippingArea.Y1,
223                                                     YAxisWidth,
224                                                     rowEntry.YAxis.ClippingArea.Y2);
225      }
226    }
227
228    private static void SetClipY(RowEntry rowEntry, double y1, double y2) {
229      rowEntry.LinesShape.ClippingArea = new RectangleD(rowEntry.LinesShape.ClippingArea.X1,
230                                                        y1,
231                                                        rowEntry.LinesShape.ClippingArea.X2,
232                                                        y2);
233      rowEntry.Grid.ClippingArea = new RectangleD(rowEntry.Grid.ClippingArea.X1,
234                                                  y1,
235                                                  rowEntry.Grid.ClippingArea.X2,
236                                                  y2);
237      rowEntry.YAxis.ClippingArea = new RectangleD(rowEntry.YAxis.ClippingArea.X1,
238                                                   y1,
239                                                   rowEntry.YAxis.ClippingArea.X2,
240                                                   y2);
241    }
242
243    private void InitLineShapes(IDataRow row) {
244      RowEntry rowEntry = new RowEntry(row);
245      rowEntries.Add(rowEntry);
246      rowToRowEntry[row] = rowEntry;
247
248      if ((row.LineType == DataRowType.SingleValue)) {
249        if (row.Count > 0) {
250          LineShape lineShape = new HorizontalLineShape(0, row[0], double.MaxValue, row[0], row.Color, row.Thickness,
251                                                        row.Style);
252          rowEntry.LinesShape.AddShape(lineShape);
253        }
254      } else {
255        for (int i = 1; i < row.Count; i++) {
256          LineShape lineShape = new LineShape(i - 1, row[i - 1], i, row[i], row.Color, row.Thickness, row.Style);
257          rowEntry.LinesShape.AddShape(lineShape);
258        }
259      }
260
261      ZoomToFullView();
262    }
263
264    private void OnRowValueChanged(IDataRow row, double value, int index, Action action) {
265      RowEntry rowEntry = rowToRowEntry[row];
266
267      if (row.LineType == DataRowType.SingleValue) {
268        if (action == Action.Added) {
269          LineShape lineShape = new HorizontalLineShape(0, row[0], double.MaxValue, row[0], row.Color, row.Thickness,
270                                                        row.Style);
271          rowEntry.LinesShape.AddShape(lineShape);
272        } else {
273          LineShape lineShape = rowEntry.LinesShape.GetShape(0);
274          lineShape.Y1 = value;
275          lineShape.Y2 = value;
276        }
277      } else {
278        if (index > rowEntry.LinesShape.Count + 1) {
279          throw new NotImplementedException();
280        }
281
282        // new value was added
283        if (index > 0 && index == rowEntry.LinesShape.Count + 1) {
284          LineShape lineShape = new LineShape(index - 1, row[index - 1], index, row[index], row.Color, row.Thickness, row.Style);
285          rowEntry.LinesShape.AddShape(lineShape);
286        }
287
288        // not the first value
289        if (index > 0) {
290          rowEntry.LinesShape.GetShape(index - 1).Y2 = value;
291        }
292
293        // not the last value
294        if (index > 0 && index < row.Count - 1) {
295          rowEntry.LinesShape.GetShape(index).Y1 = value;
296        }
297      }
298
299      ZoomToFullView();
300    }
301
302    private void OnRowValuesChanged(IDataRow row, double[] values, int index, Action action) {
303      foreach (double value in values) {
304        OnRowValueChanged(row, value, index++, action);
305      }
306    }
307
308    private void OnModelChanged() {
309      titleShape.Text = model.Title;
310
311      canvasUI.Invalidate();
312    }
313
314    #region Begin-/EndUpdate
315
316    private int beginUpdateCount;
317
318    public void BeginUpdate() {
319      beginUpdateCount++;
320    }
321
322    public void EndUpdate() {
323      if (beginUpdateCount == 0) {
324        throw new InvalidOperationException("Too many EndUpdates.");
325      }
326
327      beginUpdateCount--;
328
329      if (beginUpdateCount == 0) {
330        canvasUI.Invalidate();
331      }
332    }
333
334    #endregion
335
336    #region Zooming / Panning
337
338    private void Pan(Point startPoint, Point endPoint) {
339      zoomToFullView = false;
340
341      foreach (RowEntry rowEntry in rowEntries) {
342        RectangleD clippingArea = CalcPanClippingArea(startPoint, endPoint, rowEntry.LinesShape);
343
344        SetClipX(clippingArea.X1, clippingArea.X1);
345        SetClipY(rowEntry, clippingArea.Y1, clippingArea.Y2);
346      }
347
348      canvasUI.Invalidate();
349    }
350
351    private void PanEnd(Point startPoint, Point endPoint) {
352      zoomToFullView = false;
353
354      foreach (RowEntry rowEntry in rowEntries) {
355        RectangleD clippingArea = CalcPanClippingArea(startPoint, endPoint, rowEntry.LinesShape);
356
357        SetClipX(clippingArea.X1, clippingArea.X1);
358        SetClipY(rowEntry, clippingArea.Y1, clippingArea.Y2);
359      }
360
361      canvasUI.Invalidate();
362    }
363
364    private static RectangleD CalcPanClippingArea(Point startPoint, Point endPoint, LinesShape linesShape) {
365      return Translate.ClippingArea(startPoint, endPoint, linesShape.ClippingArea, linesShape.Viewport);
366    }
367
368    private void SetClippingArea(Rectangle rectangle) {
369      foreach (RowEntry rowEntry in rowEntries) {
370        RectangleD clippingArea = Transform.ToWorld(rectangle, rowEntry.LinesShape.Viewport, rowEntry.LinesShape.ClippingArea);
371
372        SetClipX(clippingArea.X1, clippingArea.X1);
373        SetClipY(rowEntry, clippingArea.Y1, clippingArea.Y2);
374      }
375
376      userInteractionShape.RemoveShape(rectangleShape);
377      canvasUI.Invalidate();
378    }
379
380    private void DrawRectangle(Rectangle rectangle) {
381      rectangleShape.Rectangle = Transform.ToWorld(rectangle, userInteractionShape.Viewport, userInteractionShape.ClippingArea);
382      canvasUI.Invalidate();
383    }
384
385    private void canvasUI1_KeyDown(object sender, KeyEventArgs e) {
386//      if (e.KeyCode == Keys.Back && clippingAreaHistory.Count > 1) {
387//        clippingAreaHistory.Pop();
388//
389//        RectangleD clippingArea = clippingAreaHistory.Peek();
390//
391//        SetLineClippingArea(clippingArea, false);
392//      }
393    }
394
395    private void canvasUI1_MouseDown(object sender, MouseEventArgs e) {
396      Focus();
397
398      if (e.Button == MouseButtons.Right) {
399        contextMenuStrip1.Show(PointToScreen(e.Location));
400      } else if (e.Button == MouseButtons.Left) {
401        if (ModifierKeys == Keys.None) {
402          PanListener panListener = new PanListener(e.Location);
403          panListener.Pan += Pan;
404          panListener.PanEnd += PanEnd;
405
406          mouseEventListener = panListener;
407        } else if (ModifierKeys == Keys.Control) {
408          ZoomListener zoomListener = new ZoomListener(e.Location);
409          zoomListener.DrawRectangle += DrawRectangle;
410          zoomListener.SetClippingArea += SetClippingArea;
411
412          rectangleShape.Rectangle = RectangleD.Empty;
413          userInteractionShape.AddShape(rectangleShape);
414
415          mouseEventListener = zoomListener;
416        }
417      }
418    }
419
420    private void canvasUI_MouseMove(object sender, MouseEventArgs e) {
421      if (mouseEventListener != null) {
422        mouseEventListener.MouseMove(sender, e);
423      }
424    }
425
426    private void canvasUI_MouseUp(object sender, MouseEventArgs e) {
427      if (mouseEventListener != null) {
428        mouseEventListener.MouseUp(sender, e);
429      }
430
431      mouseEventListener = null;
432    }
433
434    private void canvasUI1_MouseWheel(object sender, MouseEventArgs e) {
435      if (ModifierKeys == Keys.Control) {
436        double zoomFactor = (e.Delta > 0) ? 0.9 : 1.1;
437
438        foreach (RowEntry rowEntry in rowEntries) {
439          RectangleD clippingArea = ZoomListener.ZoomClippingArea(rowEntry.LinesShape.ClippingArea, zoomFactor);
440         
441          SetClipX(clippingArea.X1, clippingArea.X1);
442          SetClipY(rowEntry, clippingArea.Y1, clippingArea.Y2);
443        }
444      }
445    }
446
447    #endregion
448
449    private class LinesShape : WorldShape {
450      public void UpdateStyle(IDataRow row) {
451        foreach (IShape shape in shapes) {
452          LineShape lineShape = shape as LineShape;
453          if (lineShape != null) {
454            lineShape.LSColor = row.Color;
455            lineShape.LSDrawingStyle = row.Style;
456            lineShape.LSThickness = row.Thickness;
457          }
458        }
459      }
460
461      public int Count {
462        get { return shapes.Count; }
463      }
464
465      public LineShape GetShape(int index) {
466        return (LineShape)shapes[index];
467      }
468    }
469
470    private class RowEntry {
471      private readonly IDataRow dataRow;
472
473      private readonly Grid grid = new Grid();
474      private readonly YAxis yAxis = new YAxis();
475      private readonly LinesShape linesShape = new LinesShape();
476
477      public RowEntry(IDataRow dataRow) {
478        this.dataRow = dataRow;
479      }
480
481      public IDataRow DataRow {
482        get { return dataRow; }
483      }
484
485      public Grid Grid {
486        get { return grid; }
487      }
488
489      public YAxis YAxis {
490        get { return yAxis; }
491      }
492
493      public LinesShape LinesShape {
494        get { return linesShape; }
495      }
496    }
497  }
498}
Note: See TracBrowser for help on using the repository browser.