base.dart 8.42 KB
Newer Older
Collin Jackson's avatar
Collin Jackson committed
1 2 3 4 5 6 7
// 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.

part of playfair;

class ChartData {
8 9 10 11 12 13 14 15 16 17 18
  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
19 20 21 22
  final double startX;
  final double endX;
  final double startY;
  final double endY;
23 24
  final int numHorizontalGridlines;
  final int roundToPlaces;
25 26
  final double indicatorLine;
  final String indicatorText;
27
  final List<Point> dataSet;
Collin Jackson's avatar
Collin Jackson committed
28 29
}

30 31 32 33 34 35 36
// 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;
37 38 39
const double kIndicatorStrokeWidth = 2.0;
const Color kIndicatorColor = const Color(0xFFFF4081);
const double kIndicatorMargin = 2.0;
40

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

  final ChartData data;

Adam Barth's avatar
Adam Barth committed
46 47 48 49 50 51 52 53 54 55 56 57
  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;

  RenderChart createRenderObject() => new RenderChart(textTheme: textTheme, data: data);
Collin Jackson's avatar
Collin Jackson committed
58

Adam Barth's avatar
Adam Barth committed
59 60
  void updateRenderObject(RenderChart renderObject, _ChartWrapper oldWidget) {
    renderObject.textTheme = textTheme;
61
    renderObject.data = data;
Collin Jackson's avatar
Collin Jackson committed
62 63 64 65 66 67
  }
}

class RenderChart extends RenderConstrainedBox {

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

  final ChartPainter _painter;

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

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

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

101 102
class Gridline {
  double value;
103
  TextPainter labelPainter;
104 105 106 107 108
  Point labelPosition;
  Point start;
  Point end;
}

109 110 111
class Indicator {
  Point start;
  Point end;
112
  TextPainter labelPainter;
113 114 115
  Point labelPosition;
}

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

119 120 121 122 123 124 125 126 127
  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
128

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

139 140 141 142 143 144 145 146 147 148 149 150 151 152
  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()
  List<Gridline> _horizontalGridlines;
  List<Point> _markers;
153
  Indicator _indicator;
154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169

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

    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
    for(Gridline gridline in _horizontalGridlines) {
      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,
193
        gridline.end.y - gridline.labelPainter.size.height / 2.0
194 195 196 197 198 199 200 201
      );
    }

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

    // Place the indicator line
    if (data.indicatorLine != null &&
        data.indicatorLine >= data.startY &&
        data.indicatorLine <= data.endY) {
      _indicator = new Indicator()
        ..start = _convertPointToRectSpace(new Point(data.startX, data.indicatorLine), markerRect)
        ..end = _convertPointToRectSpace(new Point(data.endX, data.indicatorLine), markerRect);
      if (data.indicatorText != null) {
        TextSpan text = new StyledTextSpan(
          _textTheme.body1,
          [new PlainTextSpan("${data.indicatorText}")]
        );
217
        _indicator.labelPainter = new TextPainter(text)
218 219 220
          ..maxWidth = markerRect.width
          ..layout();
        _indicator.labelPosition = new Point(
221
          ((_indicator.start.x + _indicator.end.x) / 2.0) - _indicator.labelPainter.maxIntrinsicWidth / 2.0,
222
          _indicator.start.y - _indicator.labelPainter.size.height - kIndicatorMargin
223 224 225 226
        );
      }
    } else {
      _indicator = null;
227 228 229 230 231
    }

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

233
  Point _convertPointToRectSpace(Point point, Rect rect) {
Collin Jackson's avatar
Collin Jackson committed
234 235 236 237 238
    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);
  }

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

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

263
  void _paintIndicator(ui.Canvas canvas) {
264 265 266 267 268 269 270 271 272 273
    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());
  }

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