base.dart 8.52 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

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

  final ChartData data;

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

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

  final TextTheme textTheme;
  final ChartData data;

61
  @override
62
  _RenderChart createRenderObject(BuildContext context) => new _RenderChart(textTheme: textTheme, data: data);
Collin Jackson's avatar
Collin Jackson committed
63

64
  @override
65
  void updateRenderObject(BuildContext context, _RenderChart renderObject) {
66 67 68
    renderObject
      ..textTheme = textTheme
      ..data = data;
Collin Jackson's avatar
Collin Jackson committed
69 70 71
  }
}

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

  final ChartPainter _painter;

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

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

99
  @override
100
  void paint(PaintingContext context, Offset offset) {
Collin Jackson's avatar
Collin Jackson committed
101 102
    assert(size.width != null);
    assert(size.height != null);
103 104
    _painter.paint(context.canvas, offset & size);
    super.paint(context, offset);
Collin Jackson's avatar
Collin Jackson committed
105 106 107
  }
}

108
class _Gridline {
109
  double value;
110
  TextPainter labelPainter;
111 112 113 114 115
  Point labelPosition;
  Point start;
  Point end;
}

116
class _Indicator {
117 118
  Point start;
  Point end;
119
  TextPainter labelPainter;
120 121 122
  Point labelPosition;
}

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

126 127
  ChartData _data;
  ChartData get data => _data;
128
  set data(ChartData value) {
129 130 131 132 133 134
    assert(data != null);
    if (_data == value)
      return;
    _data = value;
    _needsLayout = true;
  }
Collin Jackson's avatar
Collin Jackson committed
135

Collin Jackson's avatar
Collin Jackson committed
136 137
  TextTheme _textTheme;
  TextTheme get textTheme => _textTheme;
138
  set textTheme(TextTheme value) {
Collin Jackson's avatar
Collin Jackson committed
139 140 141 142
    assert(value != null);
    if (_textTheme == value)
      return;
    _textTheme = value;
143
    _needsLayout = true;
Collin Jackson's avatar
Collin Jackson committed
144 145
  }

146 147 148 149 150 151 152 153 154 155 156 157
  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()
158
  List<_Gridline> _horizontalGridlines;
159
  List<Point> _markers;
160
  _Indicator _indicator;
161 162 163 164

  void _layout() {
    // Create the scale labels
    double yScaleWidth = 0.0;
165
    _horizontalGridlines = new List<_Gridline>();
166 167 168
    assert(data.numHorizontalGridlines > 1);
    double stepSize = (data.endY - data.startY) / (data.numHorizontalGridlines - 1);
    for(int i = 0; i < data.numHorizontalGridlines; i++) {
169
      _Gridline gridline = new _Gridline()
170 171 172
        ..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
173 174 175
      TextSpan text = new TextSpan(
        style: _textTheme.body1,
        text: '${gridline.value}'
176
      );
177
      gridline.labelPainter = new TextPainter(text: text)
178
        ..layout(maxWidth: _rect.width);
179
      _horizontalGridlines.add(gridline);
180
      yScaleWidth = math.max(yScaleWidth, gridline.labelPainter.maxIntrinsicWidth);
181 182 183 184 185 186 187 188 189 190 191 192 193
    }

    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
194
    for(_Gridline gridline in _horizontalGridlines) {
195 196 197 198
      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,
199
        gridline.end.y - gridline.labelPainter.size.height / 2.0
200 201 202 203 204 205 206 207
      );
    }

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

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

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

238
  Point _convertPointToRectSpace(Point point, Rect rect) {
Collin Jackson's avatar
Collin Jackson committed
239 240 241 242 243
    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);
  }

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

254
  void _paintChart(Canvas canvas) {
255 256 257
    Paint paint = new Paint()
      ..strokeWidth = kMarkerStrokeWidth
      ..color = kMarkerColor;
258
    Path path = new Path();
259 260 261 262
    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
263
    }
264
    paint.style = PaintingStyle.stroke;
265
    canvas.drawPath(path, paint);
Collin Jackson's avatar
Collin Jackson committed
266 267
  }

268
  void _paintIndicator(Canvas canvas) {
269 270 271 272 273 274 275 276 277 278
    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());
  }

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