base.dart 8.48 KB
Newer Older
Collin Jackson's avatar
Collin Jackson committed
1 2 3 4
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5 6 7 8
import 'dart:math' as math;

import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
Collin Jackson's avatar
Collin Jackson committed
9 10

class ChartData {
11 12 13 14 15 16 17 18 19 20 21
  const ChartData({
    this.startX,
    this.endX,
    this.startY,
    this.endY,
    this.dataSet,
    this.numHorizontalGridlines,
    this.roundToPlaces,
    this.indicatorLine,
    this.indicatorText
  });
Collin Jackson's avatar
Collin Jackson committed
22 23 24 25
  final double startX;
  final double endX;
  final double startY;
  final double endY;
26 27
  final int numHorizontalGridlines;
  final int roundToPlaces;
28 29
  final double indicatorLine;
  final String indicatorText;
30
  final List<Point> dataSet;
Collin Jackson's avatar
Collin Jackson committed
31 32
}

33 34 35 36 37 38 39
// TODO(jackson): Make these configurable
const double kGridStrokeWidth = 1.0;
const Color kGridColor = const Color(0xFFCCCCCC);
const Color kMarkerColor = const Color(0xFF000000);
const double kMarkerStrokeWidth = 2.0;
const double kMarkerRadius = 2.0;
const double kScaleMargin = 10.0;
40 41 42
const double kIndicatorStrokeWidth = 2.0;
const Color kIndicatorColor = const Color(0xFFFF4081);
const double kIndicatorMargin = 2.0;
43

Adam Barth's avatar
Adam Barth committed
44
class Chart extends StatelessComponent {
Collin Jackson's avatar
Collin Jackson committed
45 46 47 48
  Chart({ Key key, this.data }) : super(key: key);

  final ChartData data;

Adam Barth's avatar
Adam Barth committed
49 50 51 52 53 54 55 56 57 58 59
  Widget build(BuildContext context) {
    return new _ChartWrapper(textTheme: Theme.of(context).text, data: data);
  }
}

class _ChartWrapper extends LeafRenderObjectWidget {
  _ChartWrapper({ Key key, this.textTheme, this.data }) : super(key: key);

  final TextTheme textTheme;
  final ChartData data;

60
  _RenderChart createRenderObject() => new _RenderChart(textTheme: textTheme, data: data);
Collin Jackson's avatar
Collin Jackson committed
61

62 63 64 65
  void updateRenderObject(_RenderChart renderObject, _ChartWrapper oldWidget) {
    renderObject
      ..textTheme = textTheme
      ..data = data;
Collin Jackson's avatar
Collin Jackson committed
66 67 68
  }
}

69 70
class _RenderChart extends RenderConstrainedBox {
  _RenderChart({
Adam Barth's avatar
Adam Barth committed
71
    TextTheme textTheme,
Collin Jackson's avatar
Collin Jackson committed
72
    ChartData data
Adam Barth's avatar
Adam Barth committed
73 74
  }) : _painter = new ChartPainter(textTheme: textTheme, data: data),
       super(child: null, additionalConstraints: const BoxConstraints.expand());
Collin Jackson's avatar
Collin Jackson committed
75 76 77 78

  final ChartPainter _painter;

  ChartData get data => _painter.data;
Collin Jackson's avatar
Collin Jackson committed
79
  void set data(ChartData value) {
Collin Jackson's avatar
Collin Jackson committed
80 81 82 83 84 85 86
    assert(value != null);
    if (value == _painter.data)
      return;
    _painter.data = value;
    markNeedsPaint();
  }

Collin Jackson's avatar
Collin Jackson committed
87 88 89 90 91 92 93 94 95
  TextTheme get textTheme => _painter.textTheme;
  void set textTheme(TextTheme value) {
    assert(value != null);
    if (value == _painter.textTheme)
      return;
    _painter.textTheme = value;
    markNeedsPaint();
  }

96
  void paint(PaintingContext context, Offset offset) {
Collin Jackson's avatar
Collin Jackson committed
97 98
    assert(size.width != null);
    assert(size.height != null);
99 100
    _painter.paint(context.canvas, offset & size);
    super.paint(context, offset);
Collin Jackson's avatar
Collin Jackson committed
101 102 103
  }
}

104
class _Gridline {
105
  double value;
106
  TextPainter labelPainter;
107 108 109 110 111
  Point labelPosition;
  Point start;
  Point end;
}

112
class _Indicator {
113 114
  Point start;
  Point end;
115
  TextPainter labelPainter;
116 117 118
  Point labelPosition;
}

Collin Jackson's avatar
Collin Jackson committed
119
class ChartPainter {
Adam Barth's avatar
Adam Barth committed
120
  ChartPainter({ TextTheme textTheme, ChartData data }) : _data = data, _textTheme = textTheme;
Collin Jackson's avatar
Collin Jackson committed
121

122 123 124 125 126 127 128 129 130
  ChartData _data;
  ChartData get data => _data;
  void set data(ChartData value) {
    assert(data != null);
    if (_data == value)
      return;
    _data = value;
    _needsLayout = true;
  }
Collin Jackson's avatar
Collin Jackson committed
131

Collin Jackson's avatar
Collin Jackson committed
132 133 134 135 136 137 138
  TextTheme _textTheme;
  TextTheme get textTheme => _textTheme;
  void set textTheme(TextTheme value) {
    assert(value != null);
    if (_textTheme == value)
      return;
    _textTheme = value;
139
    _needsLayout = true;
Collin Jackson's avatar
Collin Jackson committed
140 141
  }

142 143 144 145 146 147 148 149 150 151 152 153
  static double _roundToPlaces(double value, int places) {
    int multiplier = math.pow(10, places);
    return (value * multiplier).roundToDouble() / multiplier;
  }

  // If this is set to true we will _layout() the next time we paint()
  bool _needsLayout = true;

  // The last rectangle that we were drawn into. If it changes we will _layout()
  Rect _rect;

  // These are updated by _layout()
154
  List<_Gridline> _horizontalGridlines;
155
  List<Point> _markers;
156
  _Indicator _indicator;
157 158 159 160

  void _layout() {
    // Create the scale labels
    double yScaleWidth = 0.0;
161
    _horizontalGridlines = new List<_Gridline>();
162 163 164
    assert(data.numHorizontalGridlines > 1);
    double stepSize = (data.endY - data.startY) / (data.numHorizontalGridlines - 1);
    for(int i = 0; i < data.numHorizontalGridlines; i++) {
165
      _Gridline gridline = new _Gridline()
166 167 168
        ..value = _roundToPlaces(data.startY + stepSize * i, data.roundToPlaces);
      if (gridline.value < data.startY || gridline.value > data.endY)
        continue;  // TODO(jackson): Align things so this doesn't ever happen
Adam Barth's avatar
Adam Barth committed
169 170 171
      TextSpan text = new TextSpan(
        style: _textTheme.body1,
        text: '${gridline.value}'
172
      );
173
      gridline.labelPainter = new TextPainter(text)
174 175 176
        ..maxWidth = _rect.width
        ..layout();
      _horizontalGridlines.add(gridline);
177
      yScaleWidth = math.max(yScaleWidth, gridline.labelPainter.maxIntrinsicWidth);
178 179 180 181 182 183 184 185 186 187 188 189 190
    }

    yScaleWidth += kScaleMargin;

    // Leave room for the scale on the right side
    Rect markerRect = new Rect.fromLTWH(
      _rect.left,
      _rect.top,
      _rect.width - yScaleWidth,
      _rect.height
    );

    // Left align and vertically center the labels on the right side
191
    for(_Gridline gridline in _horizontalGridlines) {
192 193 194 195
      gridline.start = _convertPointToRectSpace(new Point(data.startX, gridline.value), markerRect);
      gridline.end = _convertPointToRectSpace(new Point(data.endX, gridline.value), markerRect);
      gridline.labelPosition = new Point(
        gridline.end.x + kScaleMargin,
196
        gridline.end.y - gridline.labelPainter.size.height / 2.0
197 198 199 200 201 202 203 204
      );
    }

    // Place the markers
    List<Point> dataSet = data.dataSet;
    assert(dataSet != null);
    assert(dataSet.length > 0);
    _markers = new List<Point>();
205
    for(int i = 0; i < dataSet.length; i++)
206
      _markers.add(_convertPointToRectSpace(dataSet[i], markerRect));
207 208 209 210 211

    // Place the indicator line
    if (data.indicatorLine != null &&
        data.indicatorLine >= data.startY &&
        data.indicatorLine <= data.endY) {
212
      _indicator = new _Indicator()
213 214 215
        ..start = _convertPointToRectSpace(new Point(data.startX, data.indicatorLine), markerRect)
        ..end = _convertPointToRectSpace(new Point(data.endX, data.indicatorLine), markerRect);
      if (data.indicatorText != null) {
Adam Barth's avatar
Adam Barth committed
216 217 218
        TextSpan text = new TextSpan(
          style: _textTheme.body1,
          text: '${data.indicatorText}'
219
        );
220
        _indicator.labelPainter = new TextPainter(text)
221 222 223
          ..maxWidth = markerRect.width
          ..layout();
        _indicator.labelPosition = new Point(
224
          ((_indicator.start.x + _indicator.end.x) / 2.0) - _indicator.labelPainter.maxIntrinsicWidth / 2.0,
225
          _indicator.start.y - _indicator.labelPainter.size.height - kIndicatorMargin
226 227 228 229
        );
      }
    } else {
      _indicator = null;
230 231 232 233 234
    }

    // we don't need to compute layout again unless something changes
    _needsLayout = false;
  }
Collin Jackson's avatar
Collin Jackson committed
235

236
  Point _convertPointToRectSpace(Point point, Rect rect) {
Collin Jackson's avatar
Collin Jackson committed
237 238 239 240 241
    double x = rect.left + ((point.x - data.startX) / (data.endX - data.startX)) * rect.width;
    double y = rect.bottom - ((point.y - data.startY) / (data.endY - data.startY)) * rect.height;
    return new Point(x, y);
  }

242
  void _paintGrid(Canvas canvas) {
Collin Jackson's avatar
Collin Jackson committed
243
    Paint paint = new Paint()
244 245
      ..strokeWidth = kGridStrokeWidth
      ..color = kGridColor;
246
    for(_Gridline gridline in _horizontalGridlines) {
247 248
      gridline.labelPainter.paint(canvas, gridline.labelPosition.toOffset());
      canvas.drawLine(gridline.start, gridline.end, paint);
Collin Jackson's avatar
Collin Jackson committed
249 250 251
    }
  }

252
  void _paintChart(Canvas canvas) {
253 254 255
    Paint paint = new Paint()
      ..strokeWidth = kMarkerStrokeWidth
      ..color = kMarkerColor;
256
    Path path = new Path();
257 258 259 260
    path.moveTo(_markers[0].x, _markers[0].y);
    for (Point marker in _markers) {
      canvas.drawCircle(marker, kMarkerRadius, paint);
      path.lineTo(marker.x, marker.y);
Collin Jackson's avatar
Collin Jackson committed
261
    }
262
    paint.style = PaintingStyle.stroke;
263
    canvas.drawPath(path, paint);
Collin Jackson's avatar
Collin Jackson committed
264 265
  }

266
  void _paintIndicator(Canvas canvas) {
267 268 269 270 271 272 273 274 275 276
    if (_indicator == null)
      return;
    Paint paint = new Paint()
      ..strokeWidth = kIndicatorStrokeWidth
      ..color = kIndicatorColor;
    canvas.drawLine(_indicator.start, _indicator.end, paint);
    if (_indicator.labelPainter != null)
      _indicator.labelPainter.paint(canvas, _indicator.labelPosition.toOffset());
  }

277
  void paint(Canvas canvas, Rect rect) {
278 279 280 281 282 283 284
    if (rect != _rect)
      _needsLayout = true;
    _rect = rect;
    if (_needsLayout)
      _layout();
    _paintGrid(canvas);
    _paintChart(canvas);
285
    _paintIndicator(canvas);
Collin Jackson's avatar
Collin Jackson committed
286 287
  }
}