首页 > 解决方案 > Flutter 图表示例/doc/basics

问题描述

Flutter 图表看起来很棒,但我找不到合适的文档。根据我想要实现的目标,我有几个问题:

在此处输入图像描述

问题也在代码中作为注释(我已经添加了一些我对一些参数的理解,以防它帮助像我这样的初学者)

和代码:



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

标签: chartsflutter

解决方案


所以我最终自己构建了一个组件(因此没有真正回答这个问题,但仍然得到了想要的结果)

虽然我没有看主题级联

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

推荐阅读