Free cookie consent management tool by TermsFeed Policy Generator

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

Last change on this file since 1187 was 1187, checked in by dwagner, 15 years ago

Added OptionsDialog, min and max lines and fixed bug in auto-adjust. (#478) (#345)

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