Free cookie consent management tool by TermsFeed Policy Generator

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

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