Free cookie consent management tool by TermsFeed Policy Generator

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

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

Transformations on shapes are possible outside of the Draw method. (#424)

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