Commit 4a2d9c44 authored by Collin Jackson's avatar Collin Jackson

Draw horizontal gridlines

parent 2843bd5b
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
library playfair; library playfair;
import 'dart:sky' as sky; import 'dart:sky' as sky;
import 'dart:math' as math;
import 'package:sky/widgets/framework.dart'; import 'package:sky/widgets/framework.dart';
import 'package:sky/widgets/theme.dart'; import 'package:sky/widgets/theme.dart';
......
...@@ -5,14 +5,24 @@ ...@@ -5,14 +5,24 @@
part of playfair; part of playfair;
class ChartData { class ChartData {
const ChartData({ this.startX, this.endX, this.startY, this.endY, this.dataSet }); const ChartData({ this.startX, this.endX, this.startY, this.endY, this.dataSet, this.numHorizontalGridlines, this.roundToPlaces });
final double startX; final double startX;
final double endX; final double endX;
final double startY; final double startY;
final double endY; final double endY;
final int numHorizontalGridlines;
final int roundToPlaces;
final List<Point> dataSet; 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;
class Chart extends LeafRenderObjectWrapper { class Chart extends LeafRenderObjectWrapper {
Chart({ Key key, this.data }) : super(key: key); Chart({ Key key, this.data }) : super(key: key);
...@@ -63,10 +73,26 @@ class RenderChart extends RenderConstrainedBox { ...@@ -63,10 +73,26 @@ class RenderChart extends RenderConstrainedBox {
} }
} }
class Gridline {
double value;
ParagraphPainter labelPainter;
Point labelPosition;
Point start;
Point end;
}
class ChartPainter { class ChartPainter {
ChartPainter(this.data); ChartPainter(ChartData data) : _data = data;
ChartData data; 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 _textTheme;
TextTheme get textTheme => _textTheme; TextTheme get textTheme => _textTheme;
...@@ -75,13 +101,78 @@ class ChartPainter { ...@@ -75,13 +101,78 @@ class ChartPainter {
if (_textTheme == value) if (_textTheme == value)
return; return;
_textTheme = value; _textTheme = value;
labels = [ _needsLayout = true;
new ParagraphPainter(new StyledTextSpan(_textTheme.body1, [new PlainTextSpan("${data.startY}")])), }
new ParagraphPainter(new StyledTextSpan(_textTheme.body1, [new PlainTextSpan("${data.endY}")])),
]; 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;
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}")]
);
gridline.labelPainter = new ParagraphPainter(text)
..maxWidth = _rect.width
..layout();
_horizontalGridlines.add(gridline);
yScaleWidth = math.max(yScaleWidth, gridline.labelPainter.maxContentWidth);
}
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.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));
} }
List<ParagraphPainter> labels; // we don't need to compute layout again unless something changes
_needsLayout = false;
}
Point _convertPointToRectSpace(Point point, Rect rect) { Point _convertPointToRectSpace(Point point, Rect rect) {
double x = rect.left + ((point.x - data.startX) / (data.endX - data.startX)) * rect.width; double x = rect.left + ((point.x - data.startX) / (data.endX - data.startX)) * rect.width;
...@@ -89,37 +180,37 @@ class ChartPainter { ...@@ -89,37 +180,37 @@ class ChartPainter {
return new Point(x, y); return new Point(x, y);
} }
void _paintChart(sky.Canvas canvas, Rect rect) { void _paintGrid(sky.Canvas canvas) {
Paint paint = new Paint() Paint paint = new Paint()
..strokeWidth = 2.0 ..strokeWidth = kGridStrokeWidth
..color = const Color(0xFF000000); ..color = kGridColor;
List<Point> dataSet = data.dataSet; for(Gridline gridline in _horizontalGridlines) {
assert(dataSet != null); gridline.labelPainter.paint(canvas, gridline.labelPosition.toOffset());
assert(dataSet.length > 0); canvas.drawLine(gridline.start, gridline.end, paint);
Path path = new Path();
Point start = _convertPointToRectSpace(data.dataSet[0], rect);
path.moveTo(start.x, start.y);
for(Point point in data.dataSet) {
Point current = _convertPointToRectSpace(point, rect);
canvas.drawCircle(current, 3.0, paint);
path.lineTo(current.x, current.y);
} }
paint.setStyle(sky.PaintingStyle.stroke);
canvas.drawPath(path, paint);
} }
void _paintScale(sky.Canvas canvas, Rect rect) { void _paintChart(sky.Canvas canvas) {
// TODO(jackson): Generalize this to draw the whole axis Paint paint = new Paint()
for(ParagraphPainter painter in labels) { ..strokeWidth = kMarkerStrokeWidth
painter.maxWidth = rect.width; ..color = kMarkerColor;
painter.layout(); Path path = new sky.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);
} }
labels[0].paint(canvas, rect.bottomLeft.toOffset()); paint.setStyle(sky.PaintingStyle.stroke);
labels[1].paint(canvas, rect.topLeft.toOffset()); canvas.drawPath(path, paint);
} }
void paint(sky.Canvas canvas, Rect rect) { void paint(sky.Canvas canvas, Rect rect) {
_paintChart(canvas, rect); if (rect != _rect)
_paintScale(canvas, rect); _needsLayout = true;
_rect = rect;
if (_needsLayout)
_layout();
_paintGrid(canvas);
_paintChart(canvas);
} }
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment