Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Flutter chart example/doc/basics

Tags:

flutter

charts

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:

enter image description here

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)

  • Question 1: The Theme from the MaterialApp doesn't cascade naturally to the children Widgets...why?
  • Question 2: I want to limit the grid to a value of 5 so thought that viewport 0,5 would help but it completely messes the scatterplot - why?
  • Question 3: How can Datum be named individually on the scatterplot?
  • Question 4: I tried to do the green arcs that delimit some areas but using the code in the example gallery doesn't work (I am using the line example as opposed to an arc one as a pure copy but it doesn't work anyway) - I could use a datum with transparent fill color but I would need to be able to cut it...
  • Question 5: how can I name axes?

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;
    }

like image 749
Minh Avatar asked Nov 07 '18 17:11

Minh


1 Answers

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;
}
like image 70
Minh Avatar answered Nov 05 '22 05:11

Minh