Free cookie consent management tool by TermsFeed Policy Generator

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

Last change on this file since 1199 was 1195, checked in by shofstad, 16 years ago

Legend implementation changed (#407)

File size: 13.5 KB
Line 
1using System;
2using System.Collections.Generic;
3using System.Drawing;
4using System.Drawing.Drawing2D;
5using System.Windows.Forms;
6using HeuristicLab.Core;
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, 0, 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], 0, 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], 0, 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      var optionsdlg = new OptionsDialog(this);
415      optionsdlg.Show();
416    }
417
418    public void ApplyChangesToRow(IDataRow row) {
419      foreach (var ls in rowToLineShapes[row]) {
420        ls.LSColor = row.Color;
421        ls.LSThickness = row.Thickness;
422        if (row.Style == DrawingStyle.Dashed) {
423          ls.LSDashStyle = DashStyle.Dash;
424        }
425        else {
426          ls.LSDashStyle = DashStyle.Solid; //default
427        }
428      }
429      canvas.Invalidate();
430    }
431  }
432}
Note: See TracBrowser for help on using the repository browser.