Free cookie consent management tool by TermsFeed Policy Generator

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

Last change on this file since 1282 was 1282, checked in by bspisic, 15 years ago

#424 Resolved a bug in the zoomToFullView implementation.

File size: 15.2 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 int maxDataRowCount;
15    private double minDataValue;
16    private double maxDataValue;
17
18    private readonly TextShape titleShape;
19    private readonly LinesShape linesShape;
20    private readonly LegendShape legendShape;
21
22    private readonly XAxis xAxis;
23    private readonly YAxis yAxis;
24    private readonly Grid grid;
25
26    private readonly Stack<RectangleD> clippingAreaHistory = new Stack<RectangleD>();
27    private readonly WorldShape userInteractionShape;
28    private readonly RectangleShape rectangleShape;
29    private IMouseEventListener mouseEventListener;
30    private bool zoomToFullView;
31
32    /// <summary>
33    /// This constructor shouldn't be called. Only required for the designer.
34    /// </summary>
35    public LineChart() {
36      InitializeComponent();
37    }
38
39    /// <summary>
40    /// Initializes the chart.
41    /// </summary>
42    /// <param name="model">Referenz to the model, for data</param>
43    public LineChart(IChartDataRowsModel model) : this() {
44      if (model == null) {
45        throw new NullReferenceException("Model cannot be null.");
46      }
47
48      canvas = canvasUI.Canvas;
49
50      grid = new Grid();
51      canvas.AddShape(grid);
52
53      linesShape = new LinesShape();
54      canvas.AddShape(linesShape);
55
56      xAxis = new XAxis();
57      canvas.AddShape(xAxis);
58
59      yAxis = new YAxis();
60      canvas.AddShape(yAxis);
61
62      titleShape = new TextShape(0, 0, model.Title, 15);
63      canvas.AddShape(titleShape);
64
65      //  horizontalLineShape = new HorizontalLineShape(this.maxDataValue, Color.Yellow, 4, DrawingStyle.Solid);
66      //  root.AddShape(horizontalLineShape);
67
68      legendShape = new LegendShape();
69      canvas.AddShape(legendShape);
70
71      userInteractionShape = new WorldShape();
72      canvas.AddShape(userInteractionShape);
73
74      rectangleShape = new RectangleShape(0, 0, 0, 0, Color.Blue);
75      rectangleShape.Opacity = 50;
76
77      maxDataRowCount = 0;
78      this.model = model;
79      Item = model;
80
81      UpdateLayout();
82      canvasUI.Resize += delegate { UpdateLayout(); };
83
84      //The whole data rows are shown per default
85      if (zoomToFullView) {
86        ZoomToFullView();
87      }
88    }
89
90    /// <summary>
91    /// Layout management - arranges the inner shapes.
92    /// </summary>
93    private void UpdateLayout() {
94      titleShape.X = 10;
95      titleShape.Y = canvasUI.Height - 10;
96
97      const int yAxisWidth = 100;
98      const int xAxisHeight = 20;
99
100      linesShape.BoundingBox = new RectangleD(yAxisWidth,
101                                              xAxisHeight,
102                                              canvasUI.Width,
103                                              canvasUI.Height);
104
105      userInteractionShape.BoundingBox = linesShape.BoundingBox;
106      userInteractionShape.ClippingArea = new RectangleD(0, 0, userInteractionShape.BoundingBox.Width, userInteractionShape.BoundingBox.Height);
107
108      grid.BoundingBox = linesShape.BoundingBox;
109
110
111      yAxis.BoundingBox = new RectangleD(0,
112                                         linesShape.BoundingBox.Y1,
113                                         linesShape.BoundingBox.X1,
114                                         linesShape.BoundingBox.Y2);
115
116      xAxis.BoundingBox = new RectangleD(linesShape.BoundingBox.X1,
117                                         0,
118                                         linesShape.BoundingBox.X2,
119                                         linesShape.BoundingBox.Y1);
120
121      legendShape.BoundingBox = new RectangleD(10, 10, 110, canvasUI.Height - 50);
122      legendShape.ClippingArea = new RectangleD(0, 0, legendShape.BoundingBox.Width,
123                                                legendShape.BoundingBox.Height);
124    }
125
126    private void optionsToolStripMenuItem_Click(object sender, EventArgs e) {
127      var optionsdlg = new OptionsDialog(this.model);
128      optionsdlg.ShowDialog(this);
129    }
130
131    public void OnDataRowChanged(IDataRow row) {
132      foreach (LineShape ls in rowToLineShapes[row]) {
133        ls.LSColor = row.Color;
134        ls.LSThickness = row.Thickness;
135        ls.LSDrawingStyle = row.Style;
136      }
137      canvasUI.Invalidate();
138    }
139
140    #region Add-/RemoveItemEvents
141
142    private readonly IDictionary<IDataRow, List<LineShape>> rowToLineShapes =
143      new Dictionary<IDataRow, List<LineShape>>();
144
145    protected override void AddItemEvents() {
146      base.AddItemEvents();
147
148      model.DataRowAdded += OnDataRowAdded;
149      model.DataRowRemoved += OnDataRowRemoved;
150      model.ModelChanged += OnModelChanged;
151
152      foreach (IDataRow row in model.Rows) {
153        OnDataRowAdded(row);
154      }
155    }
156
157    protected override void RemoveItemEvents() {
158      base.RemoveItemEvents();
159
160      model.DataRowAdded -= OnDataRowAdded;
161      model.DataRowRemoved -= OnDataRowRemoved;
162      model.ModelChanged -= OnModelChanged;
163    }
164
165    private void OnDataRowAdded(IDataRow row) {
166      row.ValueChanged += OnRowValueChanged;
167      row.ValuesChanged += OnRowValuesChanged;
168      row.DataRowChanged += OnDataRowChanged;
169
170      if (row.Count > maxDataRowCount) {
171        maxDataRowCount = row.Count;
172        //   UpdateSingleValueRows();
173      }
174
175      legendShape.AddLegendItem(new LegendItem(row.Label, row.Color, row.Thickness));
176      legendShape.CreateLegend();
177      InitLineShapes(row);
178    }
179
180    private void OnDataRowRemoved(IDataRow row) {
181      row.ValueChanged -= OnRowValueChanged;
182      row.ValuesChanged -= OnRowValuesChanged;
183      row.DataRowChanged -= OnDataRowChanged;
184    }
185
186    #endregion
187
188    public void ZoomToFullView() {
189      RectangleD newClippingArea = new RectangleD(-0.1,
190                                                  minDataValue - ((maxDataValue - minDataValue)*0.05),
191                                                  maxDataRowCount - 0.9,
192                                                  maxDataValue + ((maxDataValue - minDataValue)*0.05));
193
194      SetLineClippingArea(newClippingArea, true);
195
196      zoomToFullView = true;
197    }
198
199    /// <summary>
200    /// Sets the clipping area of the data to display.
201    /// </summary>
202    /// <param name="clippingArea"></param>
203    /// <param name="pushToHistoryStack"></param>
204    private void SetLineClippingArea(RectangleD clippingArea, bool pushToHistoryStack) {
205      zoomToFullView = false;
206
207      if (pushToHistoryStack) {
208        int count = clippingAreaHistory.Count;
209
210        if (count > 40) {
211          RectangleD[] clippingAreas = clippingAreaHistory.ToArray();
212          clippingAreaHistory.Clear();
213
214          for (int i = count - 20; i < count; i++) {
215            clippingAreaHistory.Push(clippingAreas[i]);
216          }
217        }
218
219        clippingAreaHistory.Push(clippingArea);
220      }
221
222      linesShape.ClippingArea = clippingArea;
223
224      grid.ClippingArea = linesShape.ClippingArea;
225
226      // horizontalLineShape.ClippingArea = linesShape.ClippingArea;
227
228
229      xAxis.ClippingArea = new RectangleD(linesShape.ClippingArea.X1,
230                                          xAxis.BoundingBox.Y1,
231                                          linesShape.ClippingArea.X2,
232                                          xAxis.BoundingBox.Y2);
233
234      yAxis.ClippingArea = new RectangleD(yAxis.BoundingBox.X1,
235                                          linesShape.ClippingArea.Y1,
236                                          yAxis.BoundingBox.X2,
237                                          linesShape.ClippingArea.Y2);
238
239      canvasUI.Invalidate();
240    }
241
242    private void InitLineShapes(IDataRow row) {
243      var lineShapes = new List<LineShape>();
244      if (rowToLineShapes.Count == 0) {
245        minDataValue = Double.PositiveInfinity;
246        maxDataValue = Double.NegativeInfinity;
247      }
248      if ((row.Count > 0)) {
249        maxDataValue = Math.Max(row[0], maxDataValue);
250        minDataValue = Math.Min(row[0], minDataValue);
251      }
252      if ((row.LineType == DataRowType.SingleValue)) {
253        if (row.Count > 0) {
254          LineShape lineShape = new HorizontalLineShape(0, row[0], double.MaxValue, row[0], row.Color, row.Thickness,
255                                                        row.Style);
256          lineShapes.Add(lineShape);
257          // TODO each DataRow needs its own WorldShape so Y Axes can be zoomed independently.
258          linesShape.AddShape(lineShape);
259        }
260      } else {
261        for (int i = 1; i < row.Count; i++) {
262          var lineShape = new LineShape(i - 1, row[i - 1], i, row[i], row.Color, row.Thickness, row.Style);
263          lineShapes.Add(lineShape);
264          // TODO each DataRow needs its own WorldShape so Y Axes can be zoomed independently.
265          linesShape.AddShape(lineShape);
266          maxDataValue = Math.Max(row[i], maxDataValue);
267          minDataValue = Math.Min(row[i], minDataValue);
268        }
269      }
270      //horizontalLineShape.YVal = maxDataValue;
271      rowToLineShapes[row] = lineShapes;
272
273      ZoomToFullView();
274    }
275
276    // TODO use action parameter
277    private void OnRowValueChanged(IDataRow row, double value, int index, Action action) {
278      List<LineShape> lineShapes = rowToLineShapes[row];
279      maxDataValue = Math.Max(value, maxDataValue);
280      minDataValue = Math.Min(value, minDataValue);
281      if (row.LineType == DataRowType.SingleValue) {
282        if (action == Action.Added) {
283          LineShape lineShape = new HorizontalLineShape(0, row[0], double.MaxValue, row[0], row.Color, row.Thickness,
284                                                        row.Style);
285          lineShapes.Add(lineShape);
286          // TODO each DataRow needs its own WorldShape so Y Axes can be zoomed independently.
287          linesShape.AddShape(lineShape);
288        } else {
289          // lineShapes[0].X2 = maxDataRowCount;
290          lineShapes[0].Y1 = value;
291          lineShapes[0].Y2 = value;
292        }
293      } else {
294        //  horizontalLineShape.YVal = maxDataValue;
295        if (index > lineShapes.Count + 1) {
296          throw new NotImplementedException();
297        }
298
299        // new value was added
300        if (index > 0 && index == lineShapes.Count + 1) {
301          if (maxDataRowCount < row.Count) {
302            maxDataRowCount = row.Count;
303            //  UpdateSingleValueRows();
304          }
305          var lineShape = new LineShape(index - 1, row[index - 1], index, row[index], row.Color, row.Thickness,
306                                        row.Style);
307          lineShapes.Add(lineShape);
308          // TODO each DataRow needs its own WorldShape so Y Axes can be zoomed independently.
309          linesShape.AddShape(lineShape);
310        }
311
312        // not the first value
313        if (index > 0) {
314          lineShapes[index - 1].Y2 = value;
315        }
316
317        // not the last value
318        if (index > 0 && index < row.Count - 1) {
319          lineShapes[index].Y1 = value;
320        }
321      }
322
323      ZoomToFullView();
324    }
325
326    // TODO remove (see ticket #501)
327    public IList<IDataRow> GetRows() {
328      return model.Rows;
329    }
330
331
332    // TODO use action parameter
333    private void OnRowValuesChanged(IDataRow row, double[] values, int index, Action action) {
334      foreach (double value in values) {
335        OnRowValueChanged(row, value, index++, action);
336      }
337    }
338
339    private void OnModelChanged() {
340      titleShape.Text = model.Title;
341
342      canvasUI.Invalidate();
343    }
344
345    #region Begin-/EndUpdate
346
347    private int beginUpdateCount;
348
349    public void BeginUpdate() {
350      beginUpdateCount++;
351    }
352
353    public void EndUpdate() {
354      if (beginUpdateCount == 0) {
355        throw new InvalidOperationException("Too many EndUpdates.");
356      }
357
358      beginUpdateCount--;
359
360      if (beginUpdateCount == 0) {
361        canvasUI.Invalidate();
362      }
363    }
364
365    #endregion
366
367    #region Zooming / Panning
368
369    private void Pan(Point startPoint, Point endPoint) {
370      RectangleD clippingArea = CalcPanClippingArea(startPoint, endPoint);
371      SetLineClippingArea(clippingArea, false);
372    }
373
374    private void PanEnd(Point startPoint, Point endPoint) {
375      RectangleD clippingArea = CalcPanClippingArea(startPoint, endPoint);
376      SetLineClippingArea(clippingArea, true);
377    }
378
379    private RectangleD CalcPanClippingArea(Point startPoint, Point endPoint) {
380      return Translate.ClippingArea(startPoint, endPoint, linesShape.ClippingArea, linesShape.Viewport);
381    }
382
383    private void SetClippingArea(Rectangle rectangle) {
384      RectangleD clippingArea = Transform.ToWorld(rectangle, linesShape.Viewport, linesShape.ClippingArea);
385
386      SetLineClippingArea(clippingArea, true);
387      userInteractionShape.RemoveShape(rectangleShape);
388    }
389
390    private void DrawRectangle(Rectangle rectangle) {
391      rectangleShape.Rectangle = Transform.ToWorld(rectangle, userInteractionShape.Viewport, userInteractionShape.ClippingArea);
392      canvasUI.Invalidate();
393    }
394
395    private void canvasUI1_KeyDown(object sender, KeyEventArgs e) {
396      if (e.KeyCode == Keys.Back && clippingAreaHistory.Count > 1) {
397        clippingAreaHistory.Pop();
398
399        RectangleD clippingArea = clippingAreaHistory.Peek();
400
401        SetLineClippingArea(clippingArea, false);
402      }
403    }
404
405    private void canvasUI1_MouseDown(object sender, MouseEventArgs e) {
406      Focus();
407
408      if (e.Button == MouseButtons.Right) {
409        contextMenuStrip1.Show(PointToScreen(e.Location));
410      } else if (e.Button == MouseButtons.Left) {
411        if (ModifierKeys == Keys.None) {
412          PanListener panListener = new PanListener(e.Location);
413          panListener.Pan += Pan;
414          panListener.PanEnd += PanEnd;
415
416          mouseEventListener = panListener;
417        } else if (ModifierKeys == Keys.Control) {
418          ZoomListener zoomListener = new ZoomListener(e.Location);
419          zoomListener.DrawRectangle += DrawRectangle;
420          zoomListener.SetClippingArea += SetClippingArea;
421
422          rectangleShape.Rectangle = RectangleD.Empty;
423          userInteractionShape.AddShape(rectangleShape);
424
425          mouseEventListener = zoomListener;
426        }
427      }
428    }
429
430    private void canvasUI_MouseMove(object sender, MouseEventArgs e) {
431      if (mouseEventListener != null) {
432        mouseEventListener.MouseMove(sender, e);
433      }
434    }
435
436    private void canvasUI_MouseUp(object sender, MouseEventArgs e) {
437      if (mouseEventListener != null) {
438        mouseEventListener.MouseUp(sender, e);
439      }
440
441      mouseEventListener = null;
442    }
443
444    private void canvasUI1_MouseWheel(object sender, MouseEventArgs e) {
445      if (ModifierKeys == Keys.Control) {
446        double zoomFactor = (e.Delta > 0) ? 0.9 : 1.1;
447
448        RectangleD clippingArea = ZoomListener.ZoomClippingArea(linesShape.ClippingArea, zoomFactor);
449
450        SetLineClippingArea(clippingArea, true);
451      }
452    }
453
454    #endregion
455
456    #region Nested type: LinesShape
457
458    internal class LinesShape : WorldShape {}
459
460    #endregion
461  }
462}
Note: See TracBrowser for help on using the repository browser.