Free cookie consent management tool by TermsFeed Policy Generator

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

Last change on this file since 1248 was 1244, checked in by mstoeger, 16 years ago

Fixed MouseMove event bug with some mouse drivers (#503)

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