Free cookie consent management tool by TermsFeed Policy Generator

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

Last change on this file since 1255 was 1249, checked in by bspisic, 16 years ago

reimplemented panning and zooming (#424)

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