From 5130adcce621ef83958e2bacbdf49e197f12a57f Mon Sep 17 00:00:00 2001 From: Trent Piercy Date: Mon, 9 Jul 2018 21:04:17 -0500 Subject: [PATCH 1/2] Add grid lines with labels --- lib/src/sparkline.dart | 113 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 108 insertions(+), 5 deletions(-) diff --git a/lib/src/sparkline.dart b/lib/src/sparkline.dart index 08fe43b..30b497b 100644 --- a/lib/src/sparkline.dart +++ b/lib/src/sparkline.dart @@ -68,6 +68,12 @@ class Sparkline extends StatelessWidget { this.fillGradient, this.fallbackHeight = 100.0, this.fallbackWidth = 300.0, + this.enableGridLines = false, + this.gridLineColor = Colors.grey, + this.gridLineAmount = 5, + this.gridLineWidth = 0.5, + this.gridLineLabelColor = Colors.grey, + this.labelPrefix = "\$", }) : assert(data != null), assert(lineWidth != null), assert(lineColor != null), @@ -160,6 +166,22 @@ class Sparkline extends StatelessWidget { /// * [fallbackWidth], the same but horizontally. final double fallbackHeight; + /// Enable or disable grid lines + final bool enableGridLines; + + /// Color of grid lines and label text + final Color gridLineColor; + final Color gridLineLabelColor; + + /// Number of grid lines + final int gridLineAmount; + + /// Width of grid lines + final double gridLineWidth; + + /// Symbol prefix for grid line labels + final String labelPrefix; + @override Widget build(BuildContext context) { return new LimitedBox( @@ -179,6 +201,12 @@ class Sparkline extends StatelessWidget { pointsMode: pointsMode, pointSize: pointSize, pointColor: pointColor, + enableGridLines: enableGridLines, + gridLineColor: gridLineColor, + gridLineAmount: gridLineAmount, + gridLineLabelColor: gridLineLabelColor, + gridLineWidth: gridLineWidth, + labelPrefix: labelPrefix ), ), ); @@ -198,8 +226,14 @@ class _SparklinePainter extends CustomPainter { @required this.pointsMode, @required this.pointSize, @required this.pointColor, - }) : _max = dataPoints.reduce(math.max), - _min = dataPoints.reduce(math.min); + @required this.enableGridLines, + this.gridLineColor, + this.gridLineAmount, + this.gridLineWidth, + this.gridLineLabelColor, + this.labelPrefix + }) : _max = dataPoints.reduce(math.max), + _min = dataPoints.reduce(math.min); final List dataPoints; @@ -220,11 +254,48 @@ class _SparklinePainter extends CustomPainter { final double _max; final double _min; + final bool enableGridLines; + final Color gridLineColor; + final int gridLineAmount; + final double gridLineWidth; + final Color gridLineLabelColor; + final String labelPrefix; + + List gridLineTextPainters = []; + + update() { + if (enableGridLines) { + double gridLineValue; + for (int i = 0; i < gridLineAmount; i++) { + // Label grid lines + gridLineValue = _max - (((_max - _min) / (gridLineAmount - 1)) * i); + + String gridLineText; + if (gridLineValue < 1) { + gridLineText = gridLineValue.toStringAsPrecision(4); + } else if (gridLineValue < 999) { + gridLineText = gridLineValue.toStringAsFixed(2); + } else { + gridLineText = gridLineValue.round().toString(); + } + + gridLineTextPainters.add(new TextPainter( + text: new TextSpan( + text: labelPrefix + gridLineText, + style: new TextStyle( + color: gridLineLabelColor, + fontSize: 10.0, + fontWeight: FontWeight.bold)), + textDirection: TextDirection.ltr)); + gridLineTextPainters[i].layout(); + } + } + } + @override void paint(Canvas canvas, Size size) { - final double width = size.width - lineWidth; + double width = size.width - lineWidth; final double height = size.height - lineWidth; - final double widthNormalizer = width / (dataPoints.length - 1); final double heightNormalizer = height / (_max - _min); final Path path = new Path(); @@ -232,6 +303,33 @@ class _SparklinePainter extends CustomPainter { Offset startPoint; + if (gridLineTextPainters.isEmpty) { + update(); + } + + if (enableGridLines) { + width = size.width - gridLineTextPainters[0].text.text.length * 6; + Paint gridPaint = new Paint() + ..color = gridLineColor + ..strokeWidth = gridLineWidth; + + double gridLineDist = height / (gridLineAmount - 1); + double gridLineY; + + // Draw grid lines + for (int i = 0; i < gridLineAmount; i++) { + gridLineY = (gridLineDist * i).round().toDouble(); + canvas.drawLine(new Offset(0.0, gridLineY), + new Offset(width, gridLineY), gridPaint); + + // Label grid lines + gridLineTextPainters[i] + .paint(canvas, new Offset(width + 2.0, gridLineY - 6.0)); + } + } + + final double widthNormalizer = width / dataPoints.length; + for (int i = 0; i < dataPoints.length; i++) { double x = i * widthNormalizer + lineWidth / 2; double y = @@ -315,6 +413,11 @@ class _SparklinePainter extends CustomPainter { fillGradient != old.fillGradient || pointsMode != old.pointsMode || pointSize != old.pointSize || - pointColor != old.pointColor; + pointColor != old.pointColor || + enableGridLines != old.enableGridLines || + gridLineColor != old.gridLineColor || + gridLineAmount != old.gridLineAmount || + gridLineWidth != old.gridLineWidth || + gridLineLabelColor != old.gridLineLabelColor; } } From a9d97ec94a026b14524bc7634abef9570f161580 Mon Sep 17 00:00:00 2001 From: Trent Piercy Date: Thu, 20 Dec 2018 08:56:04 -0600 Subject: [PATCH 2/2] Grid Lines README * Added example to README * Added ability to control digit precision of labels --- README.md | 24 ++++++++++++++++++++++ lib/src/sparkline.dart | 32 +++++++++++++++-------------- screenshots/example_grid_lines.png | Bin 0 -> 11574 bytes 3 files changed, 41 insertions(+), 15 deletions(-) create mode 100644 screenshots/example_grid_lines.png diff --git a/README.md b/README.md index 65f1a07..d9db198 100644 --- a/README.md +++ b/README.md @@ -169,6 +169,30 @@ new Sparkline( ![fill above example screenshot](screenshots/example_fill_gradient.png) +### Grid Lines + +| Property | Default | +|------------------------|:---------------------:| +| enableGridLines | false | +| gridLineColor | Colors.grey | +| gridLineLabelColor | Colors.grey | +| gridLinelabelPrefix | "" | +| gridLineAmount | 5 | +| gridLineWidth | 0.5 | +| gridLineLabelPrecision | 3 | + +Example: + +```dart +new Sparkline( + data: data, + enableGridLines: true, + gridLineAmount: 3, +), +``` + +![grid line example screenshot](screenshots/example_grid_lines.png) + --- ### Todo: diff --git a/lib/src/sparkline.dart b/lib/src/sparkline.dart index 30b497b..aceb91a 100644 --- a/lib/src/sparkline.dart +++ b/lib/src/sparkline.dart @@ -73,7 +73,8 @@ class Sparkline extends StatelessWidget { this.gridLineAmount = 5, this.gridLineWidth = 0.5, this.gridLineLabelColor = Colors.grey, - this.labelPrefix = "\$", + this.gridLinelabelPrefix = "", + this.gridLineLabelPrecision = 3 }) : assert(data != null), assert(lineWidth != null), assert(lineColor != null), @@ -180,7 +181,10 @@ class Sparkline extends StatelessWidget { final double gridLineWidth; /// Symbol prefix for grid line labels - final String labelPrefix; + final String gridLinelabelPrefix; + + /// Digit precision of grid line labels + final int gridLineLabelPrecision; @override Widget build(BuildContext context) { @@ -206,7 +210,8 @@ class Sparkline extends StatelessWidget { gridLineAmount: gridLineAmount, gridLineLabelColor: gridLineLabelColor, gridLineWidth: gridLineWidth, - labelPrefix: labelPrefix + gridLinelabelPrefix: gridLinelabelPrefix, + gridLineLabelPrecision: gridLineLabelPrecision ), ), ); @@ -231,7 +236,8 @@ class _SparklinePainter extends CustomPainter { this.gridLineAmount, this.gridLineWidth, this.gridLineLabelColor, - this.labelPrefix + this.gridLinelabelPrefix, + this.gridLineLabelPrecision }) : _max = dataPoints.reduce(math.max), _min = dataPoints.reduce(math.min); @@ -259,7 +265,8 @@ class _SparklinePainter extends CustomPainter { final int gridLineAmount; final double gridLineWidth; final Color gridLineLabelColor; - final String labelPrefix; + final String gridLinelabelPrefix; + final int gridLineLabelPrecision; List gridLineTextPainters = []; @@ -270,18 +277,11 @@ class _SparklinePainter extends CustomPainter { // Label grid lines gridLineValue = _max - (((_max - _min) / (gridLineAmount - 1)) * i); - String gridLineText; - if (gridLineValue < 1) { - gridLineText = gridLineValue.toStringAsPrecision(4); - } else if (gridLineValue < 999) { - gridLineText = gridLineValue.toStringAsFixed(2); - } else { - gridLineText = gridLineValue.round().toString(); - } + String gridLineText = gridLineValue.toStringAsPrecision(gridLineLabelPrecision); gridLineTextPainters.add(new TextPainter( text: new TextSpan( - text: labelPrefix + gridLineText, + text: gridLinelabelPrefix + gridLineText, style: new TextStyle( color: gridLineLabelColor, fontSize: 10.0, @@ -418,6 +418,8 @@ class _SparklinePainter extends CustomPainter { gridLineColor != old.gridLineColor || gridLineAmount != old.gridLineAmount || gridLineWidth != old.gridLineWidth || - gridLineLabelColor != old.gridLineLabelColor; + gridLineLabelColor != old.gridLineLabelColor || + gridLinelabelPrefix != old.gridLinelabelPrefix || + gridLineLabelPrecision != old.gridLineLabelPrecision; } } diff --git a/screenshots/example_grid_lines.png b/screenshots/example_grid_lines.png new file mode 100644 index 0000000000000000000000000000000000000000..131044adb9c09ff4ed2855f6848219d905f89799 GIT binary patch literal 11574 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