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