// 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. import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; class ChartData { const ChartData({ this.startX, this.endX, this.startY, this.endY, this.dataSet, this.numHorizontalGridlines, this.roundToPlaces, this.indicatorLine, this.indicatorText }); final double startX; final double endX; final double startY; final double endY; final int numHorizontalGridlines; final int roundToPlaces; final double indicatorLine; final String indicatorText; final List<Point> dataSet; } // 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; const double kIndicatorStrokeWidth = 2.0; const Color kIndicatorColor = const Color(0xFFFF4081); const double kIndicatorMargin = 2.0; class Chart extends StatelessWidget { Chart({ Key key, this.data }) : super(key: key); final ChartData data; @override Widget build(BuildContext context) { return new _ChartWrapper(textTheme: Theme.of(context).textTheme, data: data); } } class _ChartWrapper extends LeafRenderObjectWidget { _ChartWrapper({ Key key, this.textTheme, this.data }) : super(key: key); final TextTheme textTheme; final ChartData data; @override _RenderChart createRenderObject(BuildContext context) => new _RenderChart(textTheme: textTheme, data: data); @override void updateRenderObject(BuildContext context, _RenderChart renderObject) { renderObject ..textTheme = textTheme ..data = data; } } class _RenderChart extends RenderConstrainedBox { _RenderChart({ TextTheme textTheme, ChartData data }) : _painter = new ChartPainter(textTheme: textTheme, data: data), super(child: null, additionalConstraints: const BoxConstraints.expand()); final ChartPainter _painter; ChartData get data => _painter.data; void set data(ChartData value) { assert(value != null); if (value == _painter.data) return; _painter.data = value; markNeedsPaint(); } TextTheme get textTheme => _painter.textTheme; void set textTheme(TextTheme value) { assert(value != null); if (value == _painter.textTheme) return; _painter.textTheme = value; markNeedsPaint(); } @override void paint(PaintingContext context, Offset offset) { assert(size.width != null); assert(size.height != null); _painter.paint(context.canvas, offset & size); super.paint(context, offset); } } class _Gridline { double value; TextPainter labelPainter; Point labelPosition; Point start; Point end; } class _Indicator { Point start; Point end; TextPainter labelPainter; Point labelPosition; } class ChartPainter { ChartPainter({ TextTheme textTheme, ChartData data }) : _data = data, _textTheme = textTheme; ChartData _data; ChartData get data => _data; void set data(ChartData value) { assert(data != null); if (_data == value) return; _data = value; _needsLayout = true; } TextTheme _textTheme; TextTheme get textTheme => _textTheme; void set textTheme(TextTheme value) { assert(value != null); if (_textTheme == value) return; _textTheme = value; _needsLayout = true; } 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; _Indicator _indicator; 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 TextSpan( style: _textTheme.body1, text: '${gridline.value}' ); gridline.labelPainter = new TextPainter(text) ..layout(maxWidth: _rect.width); _horizontalGridlines.add(gridline); yScaleWidth = math.max(yScaleWidth, gridline.labelPainter.maxIntrinsicWidth); } 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, gridline.end.y - gridline.labelPainter.size.height / 2.0 ); } // Place the markers List<Point> dataSet = data.dataSet; assert(dataSet != null); assert(dataSet.length > 0); _markers = new List<Point>(); for(int i = 0; i < dataSet.length; i++) _markers.add(_convertPointToRectSpace(dataSet[i], markerRect)); // 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 TextSpan( style: _textTheme.body1, text: '${data.indicatorText}' ); _indicator.labelPainter = new TextPainter(text) ..layout(maxWidth: markerRect.width); _indicator.labelPosition = new Point( ((_indicator.start.x + _indicator.end.x) / 2.0) - _indicator.labelPainter.maxIntrinsicWidth / 2.0, _indicator.start.y - _indicator.labelPainter.size.height - kIndicatorMargin ); } } else { _indicator = null; } // we don't need to compute layout again unless something changes _needsLayout = false; } Point _convertPointToRectSpace(Point point, Rect rect) { 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); } void _paintGrid(Canvas canvas) { Paint paint = new Paint() ..strokeWidth = kGridStrokeWidth ..color = kGridColor; for(_Gridline gridline in _horizontalGridlines) { gridline.labelPainter.paint(canvas, gridline.labelPosition.toOffset()); canvas.drawLine(gridline.start, gridline.end, paint); } } void _paintChart(Canvas canvas) { Paint paint = new Paint() ..strokeWidth = kMarkerStrokeWidth ..color = kMarkerColor; Path path = new Path(); path.moveTo(_markers[0].x, _markers[0].y); for (Point marker in _markers) { canvas.drawCircle(marker, kMarkerRadius, paint); path.lineTo(marker.x, marker.y); } paint.style = PaintingStyle.stroke; canvas.drawPath(path, paint); } void _paintIndicator(Canvas canvas) { 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()); } void paint(Canvas canvas, Rect rect) { if (rect != _rect) _needsLayout = true; _rect = rect; if (_needsLayout) _layout(); _paintGrid(canvas); _paintChart(canvas); _paintIndicator(canvas); } }