Free cookie consent management tool by TermsFeed Policy Generator

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

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

Modified the XAxis labels so they are always visible at the bottom. (#433)
Added simple layout management to the line chart's root shape containing a title-, lines- and xaxis-area. (#345)

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