Free cookie consent management tool by TermsFeed Policy Generator

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

Last change on this file since 1182 was 1182, checked in by mstoeger, 16 years ago

Implemented X/Y-Axes and a Grid. (#433)

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