Flutter charts looks great but I can't find a good doc for it. I have a few questions based on what I want to achieve:
Questions are also in the code as comments (and I have added some of my understanding for some parameters in case it helps beginners like me)
and the code:
import 'package:flutter/material.dart';
import 'package:charts_flutter/flutter.dart' as charts;
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
//populating data by calling the setSeries function
var seriesList = setSeries();
return new MaterialApp(
title: 'Material Title',
//question 1: This doesn't seem to be passed to the underlying widgets - Why? calling Theme.of all the time would be the weird
theme: new ThemeData(
primarySwatch: Colors.green,
),
home:
ListView(
children: [
//Added that to see text outside the plot widget
Text('A scatter plot'),
//plot would not display when not put in a SizedBox - I assume it is because ListView doesn't give constraints
SizedBox(
height: 300.0,
//Only height is required
//width: 200.0,
child: charts.ScatterPlotChart(
//Providing the data needed (see below to look at the data)
seriesList,
animate: false,
//Adds the legend based on the data in the Series in field "id" or "displayName"
//It also adds "dots" in the graph itself to show series...it is confusing as they look like data points
//behaviors: [charts.SeriesLegend()],
/* Goes with question 4: This doesn't work at all and doesn't compile - I wanted an arc but even trying a line like in the gallery example doesn't work
customSeriesRenderers: [
new charts.LineRendererConfig(
// ID used to link series to this renderer.
customRendererId: 'customArc',
// Configure the regression line to be painted above the points.
//
// By default, series drawn by the point renderer are painted on
// top of those drawn by a line renderer.
layoutPaintOrder: charts.LayoutViewPaintOrder.point + 1)
],
*/
primaryMeasureAxis: charts.NumericAxisSpec(
tickProviderSpec: charts.BasicNumericTickProviderSpec(
//Ticks are the ones inside the plotting area, excluding the min and max axis values
desiredTickCount: 3,
),
//Question 2: Data point values are to be 0-5, so I expected viewport with a max of 5 to "crop" the display showing up to 5 only
//but it actually gets the widget to diplay outside its SizedBox + shows data twice with / without format
//Almost like an offset - how does this work? (chand to (0,6) and it looks nicer...but not what I want)
viewport: charts.NumericExtents(0, 5),
),
domainAxis: charts.NumericAxisSpec(
tickProviderSpec: charts.BasicNumericTickProviderSpec(
desiredTickCount: 3,
),
viewport: charts.NumericExtents(0, 5),
),
),
),
],
),
);
}
//For beginners like me:
//Series is defined in the doc as Series, so my stating Series means that T=PlotPoint, D=num
//...so domainFn (the abscissa) is of type num now...
//had I done Series domainFn would have taken ints only as abscissa values (i.e. 1.3 would be converted into 1)
List> setSeries() {
var dataOne = [
PlotPoint(1.0,1.5,10,'a','circle',
charts.MaterialPalette.pink.shadeDefault,
charts.MaterialPalette.green.shadeDefault,
10.0),
PlotPoint(2.3,2.3,15,'b','rect',
charts.MaterialPalette.pink.shadeDefault,
charts.MaterialPalette.green.shadeDefault,
5.0),
PlotPoint(4.7, 3.8, 5, 'c', 'rect',
null,
charts.MaterialPalette.green.shadeDefault,
null),
PlotPoint(5,5,10,'d','circle',
charts.MaterialPalette.yellow.shadeDefault,
charts.MaterialPalette.green.shadeDefault,
5.0),
];
var dataTwo = [
PlotPoint(4,4,60,'other','circle',
charts.MaterialPalette.transparent,
charts.MaterialPalette.blue.shadeDefault,
2.0),
];
var dataThree = [
PlotPoint(4,5,1,'limit','circle',
charts.MaterialPalette.transparent,
charts.MaterialPalette.purple.shadeDefault,
2.0),
PlotPoint(5,4,1,'limit','circle',
charts.MaterialPalette.transparent,
charts.MaterialPalette.purple.shadeDefault,
2.0),
];
return [
//First series in List - matching type expectation
charts.Series(
//Name of the series
id: 'one',
//the data to use which should be of type here PlotPoint
data: dataOne,
//The X / abscissa
domainFn: (PlotPoint pData, _) => pData.x,
//The Y / Ordinate
measureFn: (PlotPoint pData, _) => pData.y,
//Used in the Legend instead of the "id" value
displayName: 'a',
//Color of the stroke
colorFn: (PlotPoint pData, _) => pData.strokeColor,
//XXX - Must work for LineChart only
dashPatternFn: (PlotPoint pData, _) => [1, 5],
//Defines the lowest a Datum has been (to be used when showing a data point along with a range) -> pass data like the current value
//domainLowerBoundFn: (PlotPoint pData, _) => pData.lowestValueForAGivenPlotPoint,
//Defines the highest a Datum has been (to be used when showing a data point along with a range) -> pass data like the current value
//domainUpperBoundFn: ,
//Color to use to fill the data point
fillColorFn: (PlotPoint pData, _) => pData.fillColor,
//XXX never used
//fillPatternFn: ,
//Question 3: how can I show the name of the datum on the graph?
labelAccessorFn: (PlotPoint pData, _) => pData.label,
//XXX - No idea
//insideLabelStyleAccessorFn: ,
//XXX - No idea
//outsideLabelStyleAccessorFn: ,
//Defines the lowest a Datum has been (to be used when showing a data point along with a range) -> pass data like the current value
//measureLowerBoundFn: ,
//Defines the highest a Datum has been (to be used when showing a data point along with a range) -> pass data like the current value
//measureUpperBoundFn: ,
//XXX - No idea
//measureOffsetFn: ,
//XXX - No idea
//overlaySeries: false,
//The radius of the itemt to plot in pixel
radiusPxFn: (PlotPoint pData, _) => pData.radius,
//XXX - No idea
//seriesCategory: ,
//Stroke width
strokeWidthPxFn: (PlotPoint pData, _) => pData.strokeWidth,
),
//Created 2 series as points within the same serie are not shown when overlapping...so created 2
charts.Series(
id: 'two',
data: dataTwo,
domainFn: (PlotPoint pData, _) => pData.x,
measureFn: (PlotPoint pData, _) => pData.y,
displayName: 'b',
colorFn: (PlotPoint pData, _) => pData.strokeColor,
fillColorFn: (PlotPoint pData, _) => pData.fillColor,
labelAccessorFn: (PlotPoint pData, _) => pData.label,
radiusPxFn: (PlotPoint pData, _) => pData.radius,
strokeWidthPxFn: (PlotPoint pData, _) => pData.strokeWidth,
),
charts.Series(
id: 'three',
data: dataThree,
domainFn: (PlotPoint pData, _) => pData.x,
measureFn: (PlotPoint pData, _) => pData.y,
displayName: 'c',
colorFn: (PlotPoint pData, _) => pData.strokeColor,
fillColorFn: (PlotPoint pData, _) => pData.fillColor,
labelAccessorFn: (PlotPoint pData, _) => pData.label,
radiusPxFn: (PlotPoint pData, _) => pData.radius,
strokeWidthPxFn: (PlotPoint pData, _) => pData.strokeWidth,
)
//Question 4: wanted to use this to mark that Serie and create an arc to define areas...example in library doesn't work
//..setAttribute(charts.rendererIdKey, 'customArc')
,
];
}
}
class PlotPoint {
num _x;
num _y;
num _radius;
String _label;
String _shape;
charts.Color _fillColor;
charts.Color _strokeColor;
double _strokeWidth;
PlotPoint(this._x, this._y, this._radius, this._label, this._shape,
this._fillColor, this._strokeColor, this._strokeWidth);
num get x => _x;
num get y => _y;
num get radius => _radius;
String get label => _label;
String get shape => _shape;
charts.Color get fillColor => _fillColor;
charts.Color get strokeColor => _strokeColor;
double get strokeWidth => _strokeWidth;
}
So I ended up building a component myself (hence not really answering the question but still getting to the outcome desired)
I didn't look at Theme cascading though
import 'package:flutter/material.dart';
//Initially used ParagraphBuilder and then canvas.drawParagraph
//but TextSpan and TextPainter allowed me to find the size of string
//import 'dart:ui' as ui;
import 'dart:math';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Material App Title',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'A title'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => new _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return ListView(
children: <Widget>[
Text(widget.title),
SizedBox(
height: 350.0,
//width not required as widget is taking all the space because of ListView I think
//width: 300.0,
child: Card(
child: CustomPaint(
//using the class defined below and passing an array of PlotPoint (also defined below)
painter: ScatterPlot5(
plotPoints: <PlotPoint>[
PlotPoint(
4,
2,
2.0,
text: 'test1',
textSize: 10.0,
textColor: Colors.blue,
shape: 'circ',
fillColor: Colors.green,
strokeColor: Colors.pink,
strokeWidth: 2.0,
),
PlotPoint(
3,
4,
10.0,
text: 'test2',
textSize: 15.0,
textColor: Colors.blue,
shape: 'rect',
fillColor: Colors.green,
strokeColor: Colors.red,
strokeWidth: 2.0,
),
],
quadrantColor: Colors.green,
quadrantStrokeWidth: 1.0,
xAxisText: 'a large text',
yAxisText: 'a large text too',
),
),
),
),
],
);
}
}
class ScatterPlot5 extends CustomPainter {
//List of PlotPoints to be plotted
List<PlotPoint> plotPoints;
//Attributes for the frame of the plotting area
Color quadrantColor;
double quadrantStrokeWidth;
//Attributes for the axis text
Color axisTextColor;
double axisTextFontSize;
String yAxisText;
String xAxisText;
//I know my values will be between 0 and 5 but could be computed by going through the PlotPoints
final num maxValue = 5.0;
final num minValue = 0.0;
//Y Space between the widget border and what will be plotted
double yPadding = 5.0;
//Y Space between the text and the arrow
double yAxisTextMargin = 5.0;
//Y Space for the height of the axis arrow (the measures for the Y axis arrow are the default as the X arrow is basically a rotation of the Y one, so not creating another set of var for it)
double yAxisArrowHeight = 10.0;
//Y Space between the axis arrow and the frame
double yAxisArrowMargin = 5.0;
//Y Space to be calculated in order for the plotting area to be square and centered
double ySquareMargin = 0.0;
//Y coordinate representing the 0 on the Y axis
double yBase;
//Y height of the plotting area
double plotHeight;
//Y factor to translate coordinate such as (0,4) into pixels coordinate
double yIncrement;
//X Space between the widget border and what will be plotted
double xPadding = 5.0;
//X offset for the axis text to be displayed (a bit on the left for the Y axis, a bit overflowing on the right for the X axis)
double xAxisTextWidthOffset = 10.0;
//X width of the axis arrow
double xAxisArrowWidth = 10.0;
//X space between the axis arrow and the frame
double xAxisArrowMargin = 5.0;
//X Space to be calculated in order for the plotting area to be square and centered
double xSquareMargin = 0.0;
//X coordinate representing the 0 on the X axis
double xBase;
//X width of the plotting area
double plotWidth;
//X factor to translate coordinate such as (0,4) into pixels coordinate
double xIncrement;
//Y offset when displaying the text of a PlotPoint (weirdly enough centering on Y the text still has an offset...so compensating with this)
double yPlotPointTextOffset = 2.0;
//X space between the PlotPoint displayed and its text
double xPlotPointMargin = 5.0;
ScatterPlot5(
{this.plotPoints,
this.quadrantColor,
this.quadrantStrokeWidth,
this.axisTextColor = Colors.black,
this.axisTextFontSize = 15.0,
this.yAxisText = 'Y',
this.xAxisText = 'X'});
//To calculate all the required values that will allow the ploting area to be square and centered + support translating point coordinates into pixels
void _setValues(Size size) {
//resting these values to be recalculated in case of a screen orientation change/reshape
ySquareMargin = 0.0;
xSquareMargin = 0.0;
//finding the max space of the arrow (by default it fits in a square, but could be a pointy one)
double maxArrow = (yAxisArrowHeight > xAxisArrowWidth)
? yAxisArrowHeight
: xAxisArrowWidth;
//Y space that between the border of the widget and the plotting area
double nonPlotYUnit = yPadding +
axisTextFontSize +
yAxisTextMargin +
maxArrow +
yAxisArrowMargin;
//Y base is hence the size of the widget minus the empty area
yBase = size.height - nonPlotYUnit;
//Y height is thus the size minus the 2 empty areas
plotHeight = size.height - (2 * nonPlotYUnit);
//X base is the the border of the widget (at 0) + the empty space
//(using the right hand side where the axis arrow is as the default to make sure the plotting area will be centered)
//Note that this is a worst case scenario as xAxisTextWidthOffset could fit within the space left by the other attributes
xBase = xPadding + maxArrow + xAxisArrowMargin + xAxisTextWidthOffset;
plotWidth = size.width - 2 * xBase;
//reseting plot dimensions to the minimum to ensure we have a square and set the additional margin needed to achieve it
if (plotHeight > plotWidth) {
ySquareMargin = (plotHeight - plotWidth) / 2;
plotHeight = plotWidth;
//reseting the base as it moved
yBase -= ySquareMargin;
} else {
xSquareMargin = (plotWidth - plotHeight) / 2;
plotWidth = plotHeight;
//reseting the base as it moved
xBase += xSquareMargin;
}
//Setting the factor for translation of coordinates into pixels
yIncrement = plotHeight / maxValue;
xIncrement = plotWidth / maxValue;
}
//Takes plotting area coordinates (0-5) and returns their pixel equivalent
Offset _coord(num x, num y) {
return Offset(xBase + x * xIncrement, yBase - y * yIncrement);
}
@override
void paint(Canvas canvas, Size size) {
//Before painting reset values to make sure all data is in line with current size/orientation
_setValues(size);
//Setting up the paint for the frame
Paint quadrantPaint = Paint()
..color = quadrantColor
..strokeWidth = quadrantStrokeWidth
..style = PaintingStyle.stroke;
//The overall rectangle
canvas.drawRect( Rect.fromPoints(_coord(0, 5), _coord(5, 0)), quadrantPaint, );
//The top right arc
canvas.drawArc(
Rect.fromPoints(_coord(3.5, 6.5), _coord(6.5, 3.5)),
//starting angle
pi / 2,
//angle to add to the starting angle (not the target angle...)
pi / 2,
true,
quadrantPaint,
);
//The bottom left arc
canvas.drawArc(
Rect.fromPoints(_coord(-1.5, 1.5), _coord(1.5, -1.5)),
3 * pi / 2,
pi / 2,
true,
quadrantPaint,
);
//The little axis extension to get to the top left arrow
Offset topLeft = _coord(0, 5);
canvas.drawLine(topLeft, Offset(topLeft.dx, topLeft.dy - yAxisArrowMargin), quadrantPaint);
//The little axis extension to get to the bottow right arrow
Offset bottomRight = _coord(5, 0);
canvas.drawLine(bottomRight, Offset(bottomRight.dx + xAxisArrowMargin, bottomRight.dy), quadrantPaint);
//Changing the style to fill to draw the arrows
quadrantPaint.style = PaintingStyle.fill;
//creating the Y axis arrow
Path yPath = Path();
yPath.moveTo(_coord(0, 5).dx - xAxisArrowWidth / 2, _coord(0, 5).dy - yAxisArrowMargin);
yPath.relativeLineTo(xAxisArrowWidth, 0.0);
yPath.relativeLineTo(-xAxisArrowWidth / 2, -yAxisArrowHeight);
yPath.relativeLineTo(-xAxisArrowWidth / 2, yAxisArrowHeight);
canvas.drawPath(yPath, quadrantPaint);
//creating the X axis arrow
//remember that height/width of the arrow are for the Y axis top right arrow...need to rotate that for the X one to look the same
Path xPath = Path();
xPath.moveTo(_coord(5, 0).dx + xAxisArrowMargin, _coord(5, 0).dy - xAxisArrowWidth / 2);
xPath.relativeLineTo(0.0, xAxisArrowWidth);
xPath.relativeLineTo(yAxisArrowHeight, -xAxisArrowWidth / 2);
xPath.relativeLineTo(-yAxisArrowHeight, -xAxisArrowWidth / 2);
canvas.drawPath(xPath, quadrantPaint);
/* I initually used Paragraph builder but couldn't calculate height and width for the text object...leaving it here as an example
ui.ParagraphBuilder yAxisbuilder = ui.ParagraphBuilder(
ui.ParagraphStyle(
fontSize: axisTextFontSize,
textAlign: TextAlign.left,
),
)
..pushStyle(ui.TextStyle(color: axisTextColor))
..addText(yAxisText);
ui.Paragraph yPara = yAxisbuilder.build()
..layout(ui.ParagraphConstraints(width: 100.0));
canvas.drawParagraph(
yPara,
Offset(_coord(0,5).dx-xAxisTextWidthOffset,_coord(0,5).dy-yAxisArrowMargin-yAxisArrowHeight-yAxisTextMargin-yAxisTextHeight),
);
*/
//X axis label 1) create span, 2) create TextPainter, 3) layout the painter and paint it
TextSpan xSpan = TextSpan(
style: TextStyle(
color: axisTextColor,
fontSize: axisTextFontSize,
),
text: xAxisText,
);
TextPainter xtp = TextPainter(
text: xSpan,
textAlign: TextAlign.left,
textDirection: TextDirection.ltr,
);
xtp.maxLines = 1;
xtp.layout();
xtp.paint(canvas, Offset(_coord(5, 0).dx + xAxisTextWidthOffset - xtp.width, _coord(5, 0).dy + xAxisArrowWidth / 2 + yAxisTextMargin), );
//Y axis label
TextSpan ySpan = TextSpan(
style: TextStyle(
color: axisTextColor,
fontSize: axisTextFontSize,
),
text: yAxisText,
);
TextPainter ytp = TextPainter(
text: ySpan,
textAlign: TextAlign.left,
textDirection: TextDirection.ltr,
);
ytp.maxLines = 1;
ytp.layout();
ytp.paint(canvas, Offset(_coord(0, 5).dx - xAxisTextWidthOffset, _coord(0, 5).dy - yAxisArrowMargin - yAxisArrowHeight - yAxisTextMargin - axisTextFontSize),);
//Now the points
for (int i = 0; i < plotPoints.length; i++) {
//Creating the paint for each point with first the fill information
Paint ppPaint = Paint()
..color = plotPoints[i].fillColor
..strokeWidth = plotPoints[i].strokeWidth
..style = PaintingStyle.fill;
//defining the point position
Offset ppOffset = _coord(plotPoints[i].x, plotPoints[i].y);
//Depending on the shape wanted, draw a rect of a circle
//note that 2 things are painted, 1 the filled version, then another version with the border only after ppPaint.style has been changed
if (plotPoints[i].shape == 'rect') {
Rect rect = Rect.fromCircle(
center: ppOffset,
radius: plotPoints[i].radius,
);
canvas.drawRect(rect, ppPaint);
//changing paint to focus on the stroke
ppPaint.color = plotPoints[i].strokeColor;
ppPaint.style = PaintingStyle.stroke;
//paint the same rect but with the stroke style set
canvas.drawRect(rect, ppPaint);
} else {
canvas.drawCircle(ppOffset, plotPoints[i].radius, ppPaint);
//changing paint to focus on the stroke
ppPaint.color = plotPoints[i].strokeColor;
ppPaint.style = PaintingStyle.stroke;
//paint the same rect but with the stroke style set
canvas.drawCircle(ppOffset, plotPoints[i].radius, ppPaint);
}
//Text for the PlotPoint
TextSpan ppSpan = TextSpan(
style: TextStyle(
color: plotPoints[i].textColor,
fontSize: plotPoints[i].textSize,
),
text: plotPoints[i].text,
);
TextPainter pptp = TextPainter(
text: ppSpan,
textAlign: TextAlign.left,
textDirection: TextDirection.ltr,
);
pptp.maxLines = 1;
pptp.layout();
//XXX add collision detection with other plotpoint text too
//if text is going out of canvas then paint it to the left of the plot point otherwise on the right
if (ppOffset.dx + plotPoints[i].radius + xPlotPointMargin + pptp.width > size.width) {
pptp.paint(
canvas,Offset(ppOffset.dx - plotPoints[i].radius - xPlotPointMargin - pptp.width, ppOffset.dy - yPlotPointTextOffset - plotPoints[i].textSize / 2),);
} else {
pptp.paint(canvas, Offset(ppOffset.dx + plotPoints[i].radius + xPlotPointMargin, ppOffset.dy - yPlotPointTextOffset - plotPoints[i].textSize / 2),);
}
}
}
@override
bool shouldRepaint(ScatterPlot5 old) => true;
}
//class to provide point info
class PlotPoint {
num _x;
num _y;
num _radius;
String text;
double textSize;
Color textColor;
String shape;
Color fillColor;
Color strokeColor;
double strokeWidth;
PlotPoint(this._x, this._y, this._radius,
{this.text = '',
this.textSize = 10.0,
this.textColor = Colors.black,
this.shape = 'circ',
this.fillColor = Colors.blue,
this.strokeColor = Colors.black,
this.strokeWidth = 1.0});
num get x => _x;
num get y => _y;
num get radius => _radius;
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With