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
RevLine 
[683]1using System;
[861]2using System.Collections.Generic;
[928]3using System.Drawing;
[1187]4using System.Drawing.Drawing2D;
[928]5using System.Windows.Forms;
[697]6using HeuristicLab.Core;
[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
[1187]67      minMaxLineShape = new MinMaxLineShape(this.minDataValue, this.maxDataValue, 0, Color.Yellow, 4, DrawingStyle.Solid);
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++) {
[980]209        LineShape lineShape = new LineShape(i - 1, row[i - 1], i, row[i], 0, 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        }
[1187]248        LineShape lineShape = new LineShape(index - 1, row[index - 1], index, row[index], 0, row.Color, row.Thickness,
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) {
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    }
[684]431  }
[1038]432}
Note: See TracBrowser for help on using the repository browser.