Free cookie consent management tool by TermsFeed Policy Generator

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

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

General housekeeping (#498) Removed some old unused Z-Order values. Replaced some var types by concrete types. Renamed some LineShape properties. Added caching of Pens and Brushes in LineShape and RectangleShape. Put axis tick calculation algorithm into its own class.

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