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

Last change on this file since 1058 was 1058, checked in by bspisic, 12 years ago

#424
Implemented zooming by holding the control key + scrolling

File size: 10.6 KB
Line 
1using System;
2using System.Collections.Generic;
3using System.Drawing;
4using System.Windows.Forms;
5using HeuristicLab.Core;
6
7namespace HeuristicLab.Visualization {
8  public class LinesShape : WorldShape {
9    private readonly RectangleShape background = new RectangleShape(0, 0, 1, 1, Color.FromArgb(240, 240, 240));
10
11    public LinesShape(RectangleD clippingArea, RectangleD boundingBox)
12      : base(clippingArea, boundingBox) {
13      AddShape(background);
14    }
15
16    public override void Draw(Graphics graphics, Rectangle viewport, RectangleD clippingArea) {
17      UpdateLayout();
18      base.Draw(graphics, viewport, clippingArea);
19    }
20
21    private void UpdateLayout() {
22      background.Rectangle = ClippingArea;
23    }
24  }
25
26  public partial class LineChart : ViewBase {
27    private readonly IChartDataRowsModel model;
28    private int maxDataRowCount;
29    private Boolean zoomFullView;
30    private double minDataValue;
31    private double maxDataValue;
32
33    private readonly WorldShape root;
34    private readonly TextShape titleShape;
35    private readonly LinesShape linesShape;
36    private readonly LegendShape legendShape;
37
38    private readonly XAxis xAxis;
39
40    /// <summary>
41    /// This constructor shouldn't be called. Only required for the designer.
42    /// </summary>
43    public LineChart() {
44      InitializeComponent();
45    }
46
47    /// <summary>
48    /// Initializes the chart.
49    /// </summary>
50    /// <param name="model">Referenz to the model, for data</param>
51    public LineChart(IChartDataRowsModel model) : this() {
52      if (model == null) {
53        throw new NullReferenceException("Model cannot be null.");
54      }
55
56      //TODO: correct Rectangle to fit
57
58      RectangleD dummy = new RectangleD(0, 0, 1, 1);
59
60      root = new WorldShape(dummy, dummy);
61
62      linesShape = new LinesShape(dummy, dummy);
63      root.AddShape(linesShape);
64
65      legendShape = new LegendShape(0, 0, 0, 0, 0, Color.Black);
66      //legendShape.AddLegendItem(new LegendItem("test", Color.Red, 5));
67      //legendShape.AddLegendItem(new LegendItem("test1", Color.Blue, 5));
68      //legendShape.AddLegendItem(new LegendItem("test2", Color.Pink, 5));
69      root.AddShape(legendShape);
70
71      xAxis = new XAxis(dummy, dummy);
72      root.AddShape(xAxis);
73
74      titleShape = new TextShape(0, 0, "Title", 15);
75      root.AddShape(titleShape);
76
77
78      canvas.MainCanvas.WorldShape = root;
79      canvas.Resize += delegate { UpdateLayout(); };
80
81      UpdateLayout();
82
83      this.model = model;
84      Item = model;
85
86      maxDataRowCount = 0;
87      //The whole data rows are shown per default
88      zoomFullView = true;
89      minDataValue = Double.PositiveInfinity;
90      maxDataValue = Double.NegativeInfinity;
91    }
92
93    /// <summary>
94    /// Layout management - arranges the inner shapes.
95    /// </summary>
96    private void UpdateLayout() {
97      root.ClippingArea = new RectangleD(0, 0, canvas.Width, canvas.Height);
98
99      titleShape.X = 10;
100      titleShape.Y = canvas.Height - 10;
101
102      linesShape.BoundingBox = new RectangleD(0, 20, canvas.Width, canvas.Height);
103
104      xAxis.BoundingBox = new RectangleD(linesShape.BoundingBox.X1,
105                                         0,
106                                         linesShape.BoundingBox.X2,
107                                         linesShape.BoundingBox.Y1);
108
109      legendShape.BoundingBox = new RectangleD(10, 10, 110, canvas.Height - 50);
110    }
111
112    public void ResetView() {
113      zoomFullView = true;
114      ZoomToFullView();
115
116      canvas.Invalidate();
117    }
118
119    #region Add-/RemoveItemEvents
120
121    protected override void AddItemEvents() {
122      base.AddItemEvents();
123
124      model.DataRowAdded += OnDataRowAdded;
125      model.DataRowRemoved += OnDataRowRemoved;
126      model.ModelChanged += OnModelChanged;
127
128      foreach (IDataRow row in model.Rows) {
129        OnDataRowAdded(row);
130      }
131    }
132
133    protected override void RemoveItemEvents() {
134      base.RemoveItemEvents();
135
136      model.DataRowAdded -= OnDataRowAdded;
137      model.DataRowRemoved -= OnDataRowRemoved;
138      model.ModelChanged -= OnModelChanged;
139    }
140
141    private void OnDataRowAdded(IDataRow row) {
142      row.ValueChanged += OnRowValueChanged;
143      row.ValuesChanged += OnRowValuesChanged;
144      if (row.Count > maxDataRowCount) {
145        maxDataRowCount = row.Count;
146      }
147
148      legendShape.AddLegendItem(new LegendItem(row.Label, row.Color, row.Thickness));
149      legendShape.CreateLegend();
150      InitLineShapes(row);
151    }
152
153    private void ZoomToFullView() {
154      if (!zoomFullView) {
155        return;
156      }
157      RectangleD newClippingArea = new RectangleD(-0.1,
158                                                  minDataValue - ((maxDataValue - minDataValue)*0.05),
159                                                  maxDataRowCount - 0.9,
160                                                  maxDataValue + ((maxDataValue - minDataValue)*0.05));
161
162      SetLineClippingArea(newClippingArea);
163    }
164
165    /// <summary>
166    /// Sets the clipping area of the data to display.
167    /// </summary>
168    /// <param name="clippingArea"></param>
169    private void SetLineClippingArea(RectangleD clippingArea) {
170      linesShape.ClippingArea = clippingArea;
171      xAxis.ClippingArea = new RectangleD(linesShape.ClippingArea.X1,
172                                          xAxis.BoundingBox.Y1,
173                                          linesShape.ClippingArea.X2,
174                                          xAxis.BoundingBox.Y2);
175    }
176
177    private void InitLineShapes(IDataRow row) {
178      List<LineShape> lineShapes = new List<LineShape>();
179      if (row.Count > 0) {
180        maxDataValue = Math.Max(row[0], maxDataValue);
181        minDataValue = Math.Min(row[0], minDataValue);
182      }
183      for (int i = 1; i < row.Count; i++) {
184        LineShape lineShape = new LineShape(i - 1, row[i - 1], i, row[i], 0, row.Color, row.Thickness, row.Style);
185        lineShapes.Add(lineShape);
186        // TODO each DataRow needs its own WorldShape so Y Axes can be zoomed independently.
187        linesShape.AddShape(lineShape);
188        maxDataValue = Math.Max(row[i], maxDataValue);
189        minDataValue = Math.Min(row[i], minDataValue);
190      }
191
192      rowToLineShapes[row] = lineShapes;
193      ZoomToFullView();
194
195      canvas.Invalidate();
196    }
197
198    private void OnDataRowRemoved(IDataRow row) {
199      row.ValueChanged -= OnRowValueChanged;
200      row.ValuesChanged -= OnRowValuesChanged;
201    }
202
203    private readonly IDictionary<IDataRow, List<LineShape>> rowToLineShapes = new Dictionary<IDataRow, List<LineShape>>();
204
205    // TODO use action parameter
206    private void OnRowValueChanged(IDataRow row, double value, int index, Action action) {
207      xAxis.SetLabel(index, index.ToString());
208
209      List<LineShape> lineShapes = rowToLineShapes[row];
210      maxDataValue = Math.Max(value, maxDataValue);
211      minDataValue = Math.Min(value, minDataValue);
212
213      if (index > lineShapes.Count + 1) {
214        throw new NotImplementedException();
215      }
216
217      // new value was added
218      if (index > 0 && index == lineShapes.Count + 1) {
219        if (maxDataRowCount < row.Count) {
220          maxDataRowCount = row.Count;
221        }
222        LineShape lineShape = new LineShape(index - 1, row[index - 1], index, row[index], 0, row.Color, row.Thickness, row.Style);
223        lineShapes.Add(lineShape);
224        // TODO each DataRow needs its own WorldShape so Y Axes can be zoomed independently.
225        linesShape.AddShape(lineShape);
226      }
227
228      // not the first value
229      if (index > 0) {
230        lineShapes[index - 1].Y2 = value;
231      }
232
233      // not the last value
234      if (index > 0 && index < row.Count - 1) {
235        lineShapes[index].Y1 = value;
236      }
237      ZoomToFullView();
238
239      canvas.Invalidate();
240    }
241
242    // TODO use action parameter
243    private void OnRowValuesChanged(IDataRow row, double[] values, int index, Action action) {
244      foreach (double value in values) {
245        OnRowValueChanged(row, value, index++, action);
246      }
247    }
248
249    private void OnModelChanged() {}
250
251    #endregion
252
253    #region Begin-/EndUpdate
254
255    private int beginUpdateCount = 0;
256
257    public void BeginUpdate() {
258      beginUpdateCount++;
259    }
260
261    public void EndUpdate() {
262      if (beginUpdateCount == 0) {
263        throw new InvalidOperationException("Too many EndUpdates.");
264      }
265
266      beginUpdateCount--;
267
268      if (beginUpdateCount == 0) {
269        canvas.Invalidate();
270      }
271    }
272
273    #endregion
274
275    private RectangleShape rectangleShape;
276
277    private void canvasUI1_MouseDown(object sender, MouseEventArgs e) {
278      Focus();
279
280      if (ModifierKeys == Keys.Control) {
281        CreateZoomListener(e);
282      } else {
283        CreatePanListener(e);
284      }
285    }
286
287    private void canvasUI1_MouseWheel(object sender, MouseEventArgs e) {
288      if (ModifierKeys == Keys.Control) {
289        double zoomFactor = (e.Delta > 0) ? 0.9 : 1.1;
290
291        RectangleD clippingArea = ZoomListener.ZoomClippingArea(linesShape.ClippingArea, zoomFactor);
292
293        SetLineClippingArea(clippingArea);
294        canvas.Invalidate();
295      }
296    }
297
298    private void CreateZoomListener(MouseEventArgs e) {
299      ZoomListener zoomListener = new ZoomListener(e.Location);
300      zoomListener.DrawRectangle += DrawRectangle;
301      zoomListener.OnMouseUp += OnZoom_MouseUp;
302
303      canvas.MouseEventListener = zoomListener;
304
305      rectangleShape = new RectangleShape(e.X, e.Y, e.X, e.Y, Color.Blue);
306      rectangleShape.Opacity = 50;
307
308      linesShape.AddShape(rectangleShape);
309    }
310
311    private void OnZoom_MouseUp(object sender, MouseEventArgs e) {
312      canvas.MouseEventListener = null;
313
314      SetLineClippingArea(rectangleShape.Rectangle);
315      linesShape.RemoveShape(rectangleShape);
316
317      zoomFullView = false; //user wants to zoom => no full view
318
319      canvas.Invalidate();
320    }
321
322    private void DrawRectangle(Rectangle rectangle) {
323      rectangleShape.Rectangle = Transform.ToWorld(rectangle, canvas.ClientRectangle, linesShape.ClippingArea);
324      canvas.Invalidate();
325    }
326
327    private void CreatePanListener(MouseEventArgs e) {
328      PanListener panListener = new PanListener(canvas.ClientRectangle, linesShape.ClippingArea, e.Location);
329
330      panListener.SetNewClippingArea += SetNewClippingArea;
331      panListener.OnMouseUp += delegate { canvas.MouseEventListener = null; };
332
333      canvas.MouseEventListener = panListener;
334    }
335
336    private void SetNewClippingArea(RectangleD newClippingArea) {
337      SetLineClippingArea(newClippingArea);
338
339      zoomFullView = false;
340      canvas.Invalidate();
341    }
342  }
343}
Note: See TracBrowser for help on using the repository browser.