google / charts

https://pub.dev/packages/charts_flutter
Apache License 2.0
2.81k stars 1.23k forks source link

Is it possible to add marker/tooltip on line chart and bar chart? #58

Closed oneeall closed 3 years ago

oneeall commented 6 years ago

I have intention on looking for a specific point in a line chart of charts_flutter and then display the the marker to highlight the point after a points or bar is hit.

My question is, Is it possible to create a widget or add some label to the line chart and bar chart?

below is based on the my line chart example, on android apps

screen shot 2018-05-21 at 15 06 20
lorrainekan commented 6 years ago

We don't yet have a timeline for adding touch cards.

Winghin2517 commented 6 years ago

screen shot 2018-08-12 at 17 19 48

The graph as it stands has a vertical line that shows the place where the user has clicked on the graph. It would make sense for a marker to be place above the line similar to this:

screen shot 2018-08-12 at 17 22 55

arlindiDev commented 5 years ago

@oneeall did you find a solution to add the label where the user has selected a point in the graph ?

krishnakumarcn commented 5 years ago

Is this issue resolved?

komputerboy commented 5 years ago

any news about this?

stasgora commented 5 years ago

It can be done, although it took me some time to figure out. Screenshot_20190502-154757~2 To get the value I used SelectionModelConfig.changedListener.

Now for the tricky part: I extended CircleSymbolRenderer (for LinePointHighlighter.symbolRenderer) and overridden the paint method. Inside you can draw custom objects relative to the selected point.

Maybe in the future the authors will make this easier but for now you use this method to modify rendering process of the selected point in any way you need (using ChartCanvas methods)

EDIT: Since my initial comment I have received several questions asking for a deeper explanation so I put together an example widget class:

Expand flutter chart label example ```ruby import 'dart:math'; import 'package:flutter/material.dart'; import 'package:charts_flutter/flutter.dart'; import 'package:charts_flutter/src/text_element.dart'; import 'package:charts_flutter/src/text_style.dart' as style; class Chart extends StatelessWidget { @override Widget build(BuildContext context) { return LineChart( _createSampleData(), behaviors: [ LinePointHighlighter( symbolRenderer: CustomCircleSymbolRenderer() ) ], selectionModels: [ SelectionModelConfig( changedListener: (SelectionModel model) { if(model.hasDatumSelection) print(model.selectedSeries[0].measureFn(model.selectedDatum[0].index)); } ) ], ); } List> _createSampleData() { final data = [ new LinearSales(0, 5), new LinearSales(1, 25), new LinearSales(2, 100), new LinearSales(3, 75), ]; return [ new Series( id: 'Sales', colorFn: (_, __) => MaterialPalette.blue.shadeDefault, domainFn: (LinearSales sales, _) => sales.year, measureFn: (LinearSales sales, _) => sales.sales, data: data, ) ]; } } class CustomCircleSymbolRenderer extends CircleSymbolRenderer { @override void paint(ChartCanvas canvas, Rectangle bounds, {List dashPattern, Color fillColor, Color strokeColor, double strokeWidthPx}) { super.paint(canvas, bounds, dashPattern: dashPattern, fillColor: fillColor, strokeColor: strokeColor, strokeWidthPx: strokeWidthPx); canvas.drawRect( Rectangle(bounds.left - 5, bounds.top - 30, bounds.width + 10, bounds.height + 10), fill: Color.white ); var textStyle = style.TextStyle(); textStyle.color = Color.black; textStyle.fontSize = 15; canvas.drawText( TextElement("1", style: textStyle), (bounds.left).round(), (bounds.top - 28).round() ); } } class LinearSales { final int year; final int sales; LinearSales(this.year, this.sales); } ```
joaquinperaza commented 5 years ago

@stasgora But how to display the value, not the fixed "1"?

stasgora commented 5 years ago

@joaquinperaza just create simple one variable model, pass it to the CustomCircleSymbolRenderer on creation and update in changedListener

joaquinperaza commented 5 years ago

Thank you!

zarulizham commented 5 years ago

I've tried your code, seems it working for hardcoded String. I wrap setState the updated value like below:

changedListener: (charts.SelectionModel model) {
    if(model.hasDatumSelection) {
        setState(() {
            textSelected = (model.selectedSeries[0].measureFn(model.selectedDatum[0].index)).toString();  
        });
        debugPrint(textSelected);
    }
}

Then, the custom circle appear and disappear automatically in 1s.

This my constructor in CustomCircleSymbolRenderer class

String text;

CustomCircleSymbolRenderer(
    {String text}
) { this.text = text; }

This is the behaviour looks like: https://imgur.com/8CwRKme

stasgora commented 5 years ago

Yes, it does that. It is due to the fact that setState() causes the widget to rebuild and after that the selection is lost. To preserve it you have to manually set it in build method using InitialSelection.

zarulizham commented 5 years ago

I tried to use these two combination of behaviors,

      behaviors: [
        new charts.InitialSelection(selectedDataConfig: [
          new charts.SeriesDatumConfig<String>('Clicks', '2021')
        ]),
        new charts.LinePointHighlighter(
          symbolRenderer: CustomCircleSymbolRenderer(text: textSelected)
        ),
      ],

It seems crash. Crash log: https://pastebin.com/4i8x30tG

pockyzhang commented 5 years ago

Sir have you tried to implement it ?any code thanks!!

pockyzhang commented 5 years ago

ok i figured out. add String text; to CustomCircleSymbolRenderer add CustomCircleSymbolRenderer render; to State change render.text = selectedText; in setState

k3v1n-uncle commented 5 years ago

@pockyzhang 什么意思,能给个完整的页面demo吗

kamlesh9070 commented 5 years ago

@stasgora Is there any way can I display labels on line chart without click, means It should automatically display?

rrnicolay commented 5 years ago

ok i figured out. add String text; to CustomCircleSymbolRenderer add CustomCircleSymbolRenderer render; to State change render.text = selectedText; in setState

Thanks for the comment. But it didn't worked for me. render.text always get the right value (inside setState()), but the tooltip never appear. Edit: worked after removing the defaultRenderer.

celiyawong commented 5 years ago

The graph should show the label when it is displayed. No display is meaningless to the user.

celiyawong commented 5 years ago

Moreover, there is no Bezier curve.

kamlesh9070 commented 5 years ago

For Bar Graph, I hv implemented my custom label decorator which display labels on vertical bar chart: https://github.com/milanvadher/sadhana_revamp/blob/master/lib/charts/custom_bar_label_decorator.dart

To Use: defaultRenderer: new BarRendererConfig<String>( strokeWidthPx: 0.3, barRendererDecorator: CustomBarLabelDecorator<String>(labelAnchor: CustomBarLabelAnchor.end), ),

harshaIOT commented 5 years ago

@joaquinperaza just create simple one variable model, pass it to the CustomCircleSymbolRenderer on creation and update in changedListener

I have done the same thing but the value of the text is not updating, please help me

drafting-dreams commented 5 years ago

@stasgora Great work! Now I have two or more series in one chart. This will show symbols near all series. I wanna get all the data from each series into one variable. And only show one tooltip near one of the series. How can I achieve that?

anweramindev commented 5 years ago

@stasgora Great work! Now I have two or more series in one chart. This will show symbols near all series. I wanna get all the data from each series into one variable. And only show one tooltip near one of the series. How can I achieve that?

I am essentially looking for the same, I have multiple series? any luck figuring this out?

I am using the selection callback to just display the values in a card below the chart at the moment.

drafting-dreams commented 5 years ago

@stasgora Great work! Now I have two or more series in one chart. This will show symbols near all series. I wanna get all the data from each series into one variable. And only show one tooltip near one of the series. How can I achieve that?

I am essentially looking for the same, I have multiple series? any luck figuring this out?

I am using the selection callback to just display the values in a card below the chart at the moment.

Nope, I just made a compromise as you did. Instead putting the card below the chart. I stacked the card on the chart‘s top-right corner and made it a little transparent.

gzimbron commented 4 years ago

@joaquinperaza just create simple one variable model, pass it to the CustomCircleSymbolRenderer on creation and update in changedListener

Could you show example code please?

alamsz commented 4 years ago

@stasgora Great work! Now I have two or more series in one chart. This will show symbols near all series. I wanna get all the data from each series into one variable. And only show one tooltip near one of the series. How can I achieve that?

I am essentially looking for the same, I have multiple series? any luck figuring this out?

I am using the selection callback to just display the values in a card below the chart at the moment.

using gesture detector onTapDown function could help in locating x, y position of tap and you could render the tooltip in more proper location

kamaleshbasu commented 4 years ago

@stasgora the solution works well and I am more or less getting the desired outcome. Screenshot_1578383499

But to achieve it, we need to import files from src folder of the library, which should be avoided as best as possible. And it does throw the following warning: Don't import implementation files from another package.

Any idea how to resolve this, till an official solution is available?

stasgora commented 4 years ago

@kamaleshbasu not possible afaik, if it was possible, that would be an official solution of sorts (one that does not involve using internal files)

FairyFanFan commented 4 years ago

Thank you!

Excuse me ! how u get the solution? setState will cause anothor question ...

kamaleshbasu commented 4 years ago

@FannyFanFan you don't use setState() as it will trigger rebuild.

it's kind of a hacked solution where you use a global variable to keep track of the currently selected point. it is definitely an anti-pattern. Till the authors of the plugin enable a feature for us to control/display the labels programmatically, we have to make do with the hacked solution.

dazza5000 commented 4 years ago

I migrated to this library https://github.com/imaNNeoFighT/fl_chart

codenamics commented 4 years ago

Hello,

It can be done with Provider package i will post example today.

Basicaly what we need is to provide value to CustomCircleSymbolRenderer and than only this part will rebuild without whole widget

dnkoulouris commented 4 years ago

A solution is to create a static String variable inside the "CustomCircleSymbolRenderer" class. Make sure to update this variable in the "changedListener" with the new selected value.

AbhijeetBharti commented 4 years ago

@stasgora @pockyzhang @zarulizham

Thank you @stasgora ! Hey @zarulizham ! No need to use setState((){}); Just create a static variable and access it from CustomMeasureTickCount.

I have just modified the code. See :


import 'package:charts_flutter/flutter.dart' as charts;
import 'package:flutter/material.dart';
import 'dart:math';
import 'package:charts_flutter/flutter.dart';
import 'package:charts_flutter/src/text_element.dart';
import 'package:charts_flutter/src/text_style.dart' as style;

class CustomMeasureTickCount extends StatelessWidget {
final List<charts.Series> seriesList;
final bool animate;
static String pointerValue;
CustomMeasureTickCount(this.seriesList, {this.animate});

/// Creates a [TimeSeriesChart] with sample data and no transition.
factory CustomMeasureTickCount.withSampleData() {
return new CustomMeasureTickCount(
_createSampleData(),
// Disable animations for image tests.
animate: false,
);
}

@override
Widget build(BuildContext context) {
return new charts.TimeSeriesChart(seriesList,
animate: animate,
behaviors: [
LinePointHighlighter(symbolRenderer: CustomCircleSymbolRenderer())
],
selectionModels: [
SelectionModelConfig(changedListener: (SelectionModel model) {
if (model.hasDatumSelection)
pointerValue = model.selectedSeries[0]
.measureFn(model.selectedDatum[0].index)
.toString();
})
],

    /// Customize the measure axis to have 10 ticks
    primaryMeasureAxis: new charts.NumericAxisSpec(
        tickProviderSpec:
            new charts.BasicNumericTickProviderSpec(desiredTickCount: 10)));
}

/// Create one series with sample hard coded data.
static List<charts.Series<MyRow, DateTime>> _createSampleData() {
final data = [
new MyRow(new DateTime(2017, 9, 25), 6),
new MyRow(new DateTime(2017, 9, 26), 8),
new MyRow(new DateTime(2017, 9, 27), 6),
new MyRow(new DateTime(2017, 9, 28), 9),
new MyRow(new DateTime(2017, 9, 29), 11),
new MyRow(new DateTime(2017, 9, 30), 15),
new MyRow(new DateTime(2017, 10, 01), 25),
new MyRow(new DateTime(2017, 10, 02), 33),
new MyRow(new DateTime(2017, 10, 03), 27),
new MyRow(new DateTime(2017, 10, 04), 31),
new MyRow(new DateTime(2017, 10, 05), 23),
];

return [
  new charts.Series<MyRow, DateTime>(
    id: 'Cost',
    domainFn: (MyRow row, _) => row.timeStamp,
    measureFn: (MyRow row, _) => row.cost,
    data: data,
    colorFn: (_, __) => charts.MaterialPalette.indigo.shadeDefault,
  )
];
}
}

/// Sample time series data type.
class MyRow {
final DateTime timeStamp;
final int cost;
MyRow(this.timeStamp, this.cost);
}

class CustomCircleSymbolRenderer extends CircleSymbolRenderer {
@override
void paint(ChartCanvas canvas, Rectangle bounds,
{List dashPattern,
Color fillColor,
Color strokeColor,
double strokeWidthPx}) {
super.paint(canvas, bounds,
dashPattern: dashPattern,
fillColor: fillColor,
strokeColor: strokeColor,
strokeWidthPx: strokeWidthPx);
canvas.drawRect(
Rectangle(bounds.left - 5, bounds.top - 30, bounds.width + 10,
bounds.height + 10),
fill: Color.white);
var textStyle = style.TextStyle();
textStyle.color = Color.black;
textStyle.fontSize = 15;
canvas.drawText(
TextElement(CustomMeasureTickCount.pointerValue, style: textStyle),
(bounds.left).round(),
(bounds.top - 28).round());
}
}
zjelo commented 4 years ago

Hi, did anyone figure out how to show labels on line chart yet without clicking?

markathomas commented 4 years ago

@zjelo it's not possible in Flutter on mobile; only on web. This is due to the difference in mouse events on web and touch pointer events on mobile. You can try to emulate it by wrapping the chart in a Listener class.

markathomas commented 4 years ago

To the Charts developers, the solution presented in this ticket works but not with ScatterPlotChart. Is there a way to make it work with ScatterPlotChart with multiple series. Here's my chart:

Widget _buildChart(BuildContext context) {
    final num now = DateTime.now().toLocal().millisecondsSinceEpoch * 1.0;
    final num start = now - (1000 * 60 * 60 * 24);
    return new charts.ScatterPlotChart(_getSeries(),
        animate: true,
        behaviors: [charts.LinePointHighlighter(symbolRenderer: CustomCircleSymbolRenderer(() => selection))],
        selectionModels: [
          charts.SelectionModelConfig(changedListener: (charts.SelectionModel model) {
            if (model.hasDatumSelection) {
              int date = model.selectedDatum[0].datum.date;
              selection = '${this.bpMap[date].systolic}/${this.bpMap[date].diastolic}';
            }
          })
        ],
        primaryMeasureAxis: new charts.NumericAxisSpec(
          tickProviderSpec: new charts.BasicNumericTickProviderSpec(zeroBound: false),
          renderSpec: charts.SmallTickRendererSpec(
              minimumPaddingBetweenLabelsPx: 10, axisLineStyle: charts.LineStyleSpec(color: ChartColors.transparent)),
        ),
        domainAxis: new charts.NumericAxisSpec(
            viewport: charts.NumericExtents(start, now),
            tickProviderSpec: new charts.BasicNumericTickProviderSpec(zeroBound: false, dataIsInWholeNumbers: true),
            renderSpec: charts.SmallTickRendererSpec(
                labelOffsetFromTickPx: 10,
                minimumPaddingBetweenLabelsPx: 10,
                axisLineStyle: charts.LineStyleSpec(color: ChartColors.transparent)),
            tickFormatterSpec: charts.BasicNumericTickFormatterSpec((measure) {
              final DateTime date = DateTime.fromMillisecondsSinceEpoch(measure.toInt());
              int hour = date.hour;
              return hour < 10 ? '0${hour}00' : '${hour}00';
            })),
        defaultRenderer: new charts.PointRendererConfig(
            pointRendererDecorators: [new charts.ComparisonPointsDecorator(symbolRenderer: new charts.CylinderSymbolRenderer())]));
  }

The selection model is firing correctly but the custom symbol renderer is not ever being called. Here's a screenshot of two charts I'm using. The top line chart shows the tooltip correctly on click. The bottom scatter chart does not

Screenshot from 2020-03-02 07-13-53

Sarathlal13 commented 4 years ago

@stasgora @AbhijeetBharti On a timeseries chart with multiple lines, when click on a line the label shows in the wrong line,

Sarathlal13 commented 4 years ago

when touch on the red line, the value shows on the blue, please help WhatsApp Image 2020-03-05 at 17 59 38

celiyawong commented 4 years ago

I didn't use google / charts, sorry

------------------ 原始邮件 ------------------ 发件人: "Sarathlal13"<notifications@github.com>; 发送时间: 2020年3月5日(星期四) 晚上8:33 收件人: "google/charts"<charts@noreply.github.com>; 抄送: "叽歪的蚊子"<740551528@qq.com>; "Comment"<comment@noreply.github.com>; 主题: Re: [google/charts] Is it possible to add marker/tooltip on line chart and bar chart? (#58)

when touch on the red line, the value shows on the blue, please help

— You are receiving this because you commented. Reply to this email directly, view it on GitHub, or unsubscribe.

rwson commented 4 years ago

I also encountered this problem when using linechart. There is no parameter to support tooltip. I referenced the reply from @stasgora , so I implemented a CustomCircleSymbolRenderer and a TooltipMgr class. TooltipMgr is mainly responsible for the problem that CustomCircleSymbolRenderer cannot pass in parameters. CustomCircleSymbolRenderer is mainly rendering. tooltip. Here is the code and approximate screenshot of my implementation:

//   tooltip.dart
import 'dart:math';

import 'package:bill/adaptor.dart';

import 'package:charts_flutter/flutter.dart';
import 'package:charts_flutter/src/text_element.dart' as ChartText;
import 'package:charts_flutter/src/text_style.dart' as ChartStyle;

import 'package:flutter/material.dart';

String _title;

String _subTitle;

class ToolTipMgr {

  static String get title => _title;

  static String get subTitle => _subTitle;

  static setTitle(Map<String, dynamic> data) {
    if (data['title'] != null && data['title'].length > 0) {
      _title = data['title'];
    }

    if (data['subTitle'] != null && data['subTitle'].length > 0) {
      _subTitle = data['subTitle'];
    }
  }

}

class CustomCircleSymbolRenderer extends CircleSymbolRenderer {

  double height = Adaptor.px(450.0);

  @override
  void paint(ChartCanvas canvas, Rectangle<num> bounds,
      {List<int> dashPattern,
        Color fillColor,
        FillPatternType fillPattern,
        Color strokeColor,
        double strokeWidthPx}) {

    super.paint(canvas, bounds,
        dashPattern: dashPattern,
        fillColor: fillColor,
        strokeColor: strokeColor,
        strokeWidthPx: strokeWidthPx);
    canvas.drawRect(Rectangle(bounds.left - 5, bounds.top, bounds.width + 100, bounds.height + 10), fill: Color.black);

    ChartStyle.TextStyle textStyle = ChartStyle.TextStyle();

    textStyle.color = Color.white;
    textStyle.fontSize = 15;

    canvas.drawText(ChartText.TextElement(ToolTipMgr.title, style: textStyle),
        (bounds.left).round(), (bounds.top + 2).round());
  }
}
//   mypage.dart
//   import other modules
import './tooltip.dart';

class SelectionLineHighlight extends StatelessWidget {
  final List<charts.Series> seriesList;
  final bool animate;

  SelectionLineHighlight(this.seriesList, {this.animate});

  factory SelectionLineHighlight.withSampleData() {
    return new SelectionLineHighlight(
      _createSampleData(),
      animate: false,
    );
  }

  @override
  Widget build(BuildContext context) {
    // This is just a simple line chart with a behavior that highlights the
    // selected points along the lines. A point will be drawn at the selected
    // datum's x,y coordinate, and a vertical follow line will be drawn through
    // it.
    //
    // A [Charts.LinePointHighlighter] behavior is added manually to enable the
    // highlighting effect.
    //
    // As an alternative, [defaultInteractions] can be set to true to include
    // the default chart interactions, including a LinePointHighlighter.
    return new charts.LineChart(
      seriesList,
      animate: animate,
      defaultRenderer: charts.LineRendererConfig(
        includeArea: true,
        includePoints: false,
        includeLine: false,
        stacked: true,
      ),
      behaviors: [
        new charts.LinePointHighlighter(
            symbolRenderer: CustomCircleSymbolRenderer()),
        new charts.SelectNearest(eventTrigger: charts.SelectionTrigger.tapAndDrag)
      ],
      selectionModels: [
        charts.SelectionModelConfig(
            changedListener: (charts.SelectionModel model) {
          if (model.hasDatumSelection) {
            ToolTipMgr.setTitle({
              'title': '${model.selectedSeries[0].measureFn(model.selectedDatum[0].datum.year)}',
              'subTitle': '111133'
            });
//            print(${model.selectedSeries[0].measureFn(model.selectedDatum[0].datum.year)});
          }
        })
      ]
    );
  }

  /// Create one series with sample hard coded data.
  static List<charts.Series<LinearSales, int>> _createSampleData() {
    final data = [
      new LinearSales(0, 5),
      new LinearSales(1, 25),
      new LinearSales(2, 1000),
      new LinearSales(3, 75),
      new LinearSales(4, 75),
      new LinearSales(6, 100)
    ];

    return [
      new charts.Series<LinearSales, int>(
        id: 'Sales',
        domainFn: (LinearSales sales, _) => sales.year,
        measureFn: (LinearSales sales, _) => sales.sales,
        data: data,
      )
    ];
  }
}

image image and you can jump to the TooltipMgr class and the paint method in CustomCircleSymbolRenderer according to the specific business.

TenPetr commented 4 years ago

@joaquinperaza just create simple one variable model, pass it to the CustomCircleSymbolRenderer on creation and update in changedListener

Could you please show me, how to do that by providing some code snippet? Thanks a lot!

harrisbaitalrayan commented 4 years ago

Hi, Using CustomCircleSymbolRenderer works perfectly for bar chart. Can a similar markup be done for pie chart ?

codenamics commented 4 years ago

Hello, how to show tooltip on line chart without clicking on it?

Robinp1011 commented 4 years ago

when touch on the red line, the value shows on the blue, please help WhatsApp Image 2020-03-05 at 17 59 38

hey is there any way to focus tooltip on both lines simultaneouly??

means for the same value of x axis how can i show the value of both red and blue graph in label

speedlightwp commented 4 years ago

Hi, I am able to use it on Bar Chart however there is no effect on Pie Chart. Can this be use on Pie Chart?

LinePointHighlighter(symbolRenderer: CustomCircleSymbolRenderer())

class CustomCircleSymbolRenderer extends CircleSymbolRenderer

primemb commented 4 years ago

I also encountered this problem when using linechart. There is no parameter to support tooltip. I referenced the reply from @stasgora , so I implemented a CustomCircleSymbolRenderer and a TooltipMgr class. TooltipMgr is mainly responsible for the problem that CustomCircleSymbolRenderer cannot pass in parameters. CustomCircleSymbolRenderer is mainly rendering. tooltip. Here is the code and approximate screenshot of my implementation:

//   tooltip.dart
import 'dart:math';

import 'package:bill/adaptor.dart';

import 'package:charts_flutter/flutter.dart';
import 'package:charts_flutter/src/text_element.dart' as ChartText;
import 'package:charts_flutter/src/text_style.dart' as ChartStyle;

import 'package:flutter/material.dart';

String _title;

String _subTitle;

class ToolTipMgr {

  static String get title => _title;

  static String get subTitle => _subTitle;

  static setTitle(Map<String, dynamic> data) {
    if (data['title'] != null && data['title'].length > 0) {
      _title = data['title'];
    }

    if (data['subTitle'] != null && data['subTitle'].length > 0) {
      _subTitle = data['subTitle'];
    }
  }

}

class CustomCircleSymbolRenderer extends CircleSymbolRenderer {

  double height = Adaptor.px(450.0);

  @override
  void paint(ChartCanvas canvas, Rectangle<num> bounds,
      {List<int> dashPattern,
        Color fillColor,
        FillPatternType fillPattern,
        Color strokeColor,
        double strokeWidthPx}) {

    super.paint(canvas, bounds,
        dashPattern: dashPattern,
        fillColor: fillColor,
        strokeColor: strokeColor,
        strokeWidthPx: strokeWidthPx);
    canvas.drawRect(Rectangle(bounds.left - 5, bounds.top, bounds.width + 100, bounds.height + 10), fill: Color.black);

    ChartStyle.TextStyle textStyle = ChartStyle.TextStyle();

    textStyle.color = Color.white;
    textStyle.fontSize = 15;

    canvas.drawText(ChartText.TextElement(ToolTipMgr.title, style: textStyle),
        (bounds.left).round(), (bounds.top + 2).round());
  }
}
//   mypage.dart
//   import other modules
import './tooltip.dart';

class SelectionLineHighlight extends StatelessWidget {
  final List<charts.Series> seriesList;
  final bool animate;

  SelectionLineHighlight(this.seriesList, {this.animate});

  factory SelectionLineHighlight.withSampleData() {
    return new SelectionLineHighlight(
      _createSampleData(),
      animate: false,
    );
  }

  @override
  Widget build(BuildContext context) {
    // This is just a simple line chart with a behavior that highlights the
    // selected points along the lines. A point will be drawn at the selected
    // datum's x,y coordinate, and a vertical follow line will be drawn through
    // it.
    //
    // A [Charts.LinePointHighlighter] behavior is added manually to enable the
    // highlighting effect.
    //
    // As an alternative, [defaultInteractions] can be set to true to include
    // the default chart interactions, including a LinePointHighlighter.
    return new charts.LineChart(
      seriesList,
      animate: animate,
      defaultRenderer: charts.LineRendererConfig(
        includeArea: true,
        includePoints: false,
        includeLine: false,
        stacked: true,
      ),
      behaviors: [
        new charts.LinePointHighlighter(
            symbolRenderer: CustomCircleSymbolRenderer()),
        new charts.SelectNearest(eventTrigger: charts.SelectionTrigger.tapAndDrag)
      ],
      selectionModels: [
        charts.SelectionModelConfig(
            changedListener: (charts.SelectionModel model) {
          if (model.hasDatumSelection) {
            ToolTipMgr.setTitle({
              'title': '${model.selectedSeries[0].measureFn(model.selectedDatum[0].datum.year)}',
              'subTitle': '111133'
            });
//            print(${model.selectedSeries[0].measureFn(model.selectedDatum[0].datum.year)});
          }
        })
      ]
    );
  }

  /// Create one series with sample hard coded data.
  static List<charts.Series<LinearSales, int>> _createSampleData() {
    final data = [
      new LinearSales(0, 5),
      new LinearSales(1, 25),
      new LinearSales(2, 1000),
      new LinearSales(3, 75),
      new LinearSales(4, 75),
      new LinearSales(6, 100)
    ];

    return [
      new charts.Series<LinearSales, int>(
        id: 'Sales',
        domainFn: (LinearSales sales, _) => sales.year,
        measureFn: (LinearSales sales, _) => sales.sales,
        data: data,
      )
    ];
  }
}

image image and you can jump to the TooltipMgr class and the paint method in CustomCircleSymbolRenderer according to the specific business.

hey, is it possible to make tooltip title font bold and add some color to it ?

rampicos commented 4 years ago

@stasgora @pockyzhang @zarulizham

Thank you @stasgora ! Hey @zarulizham ! No need to use setState((){}); Just create a static variable and access it from CustomMeasureTickCount.

I have just modified the code. See :

import 'package:charts_flutter/flutter.dart' as charts;
import 'package:flutter/material.dart';
import 'dart:math';
import 'package:charts_flutter/flutter.dart';
import 'package:charts_flutter/src/text_element.dart';
import 'package:charts_flutter/src/text_style.dart' as style;

class CustomMeasureTickCount extends StatelessWidget {
final List<charts.Series> seriesList;
final bool animate;
static String pointerValue;
CustomMeasureTickCount(this.seriesList, {this.animate});

/// Creates a [TimeSeriesChart] with sample data and no transition.
factory CustomMeasureTickCount.withSampleData() {
return new CustomMeasureTickCount(
_createSampleData(),
// Disable animations for image tests.
animate: false,
);
}

@override
Widget build(BuildContext context) {
return new charts.TimeSeriesChart(seriesList,
animate: animate,
behaviors: [
LinePointHighlighter(symbolRenderer: CustomCircleSymbolRenderer())
],
selectionModels: [
SelectionModelConfig(changedListener: (SelectionModel model) {
if (model.hasDatumSelection)
pointerValue = model.selectedSeries[0]
.measureFn(model.selectedDatum[0].index)
.toString();
})
],

    /// Customize the measure axis to have 10 ticks
    primaryMeasureAxis: new charts.NumericAxisSpec(
        tickProviderSpec:
            new charts.BasicNumericTickProviderSpec(desiredTickCount: 10)));
}

/// Create one series with sample hard coded data.
static List<charts.Series<MyRow, DateTime>> _createSampleData() {
final data = [
new MyRow(new DateTime(2017, 9, 25), 6),
new MyRow(new DateTime(2017, 9, 26), 8),
new MyRow(new DateTime(2017, 9, 27), 6),
new MyRow(new DateTime(2017, 9, 28), 9),
new MyRow(new DateTime(2017, 9, 29), 11),
new MyRow(new DateTime(2017, 9, 30), 15),
new MyRow(new DateTime(2017, 10, 01), 25),
new MyRow(new DateTime(2017, 10, 02), 33),
new MyRow(new DateTime(2017, 10, 03), 27),
new MyRow(new DateTime(2017, 10, 04), 31),
new MyRow(new DateTime(2017, 10, 05), 23),
];

return [
  new charts.Series<MyRow, DateTime>(
    id: 'Cost',
    domainFn: (MyRow row, _) => row.timeStamp,
    measureFn: (MyRow row, _) => row.cost,
    data: data,
    colorFn: (_, __) => charts.MaterialPalette.indigo.shadeDefault,
  )
];
}
}

/// Sample time series data type.
class MyRow {
final DateTime timeStamp;
final int cost;
MyRow(this.timeStamp, this.cost);
}

class CustomCircleSymbolRenderer extends CircleSymbolRenderer {
@override
void paint(ChartCanvas canvas, Rectangle bounds,
{List dashPattern,
Color fillColor,
Color strokeColor,
double strokeWidthPx}) {
super.paint(canvas, bounds,
dashPattern: dashPattern,
fillColor: fillColor,
strokeColor: strokeColor,
strokeWidthPx: strokeWidthPx);
canvas.drawRect(
Rectangle(bounds.left - 5, bounds.top - 30, bounds.width + 10,
bounds.height + 10),
fill: Color.white);
var textStyle = style.TextStyle();
textStyle.color = Color.black;
textStyle.fontSize = 15;
canvas.drawText(
TextElement(CustomMeasureTickCount.pointerValue, style: textStyle),
(bounds.left).round(),
(bounds.top - 28).round());
}
}

For the Stacked Bar Chart I'm getting same value for all Bar in a single Stack

image

is it possible show it for a single bar? I'm using following code for generating Stacked Bar chart.

charts.BarChart(
      seriesList,
      animate: animate,
      barGroupingType: charts.BarGroupingType.stacked,
      behaviors: [
        LinePointHighlighter(symbolRenderer: CustomCircleSymbolRenderer(),
            drawFollowLinesAcrossChart: false,
            defaultRadiusPx: 5,
            showVerticalFollowLine: charts.LinePointHighlighterFollowLineType.none,
            selectionModelType: charts.SelectionModelType.info,
          showHorizontalFollowLine: charts.LinePointHighlighterFollowLineType.none,

        )
      ],
      selectionModels: [
        SelectionModelConfig(changedListener: (SelectionModel model) {
          if (model.hasDatumSelection)
            pointerValue = "Selected: "+model.selectedSeries[0]
                .measureFn(model.selectedDatum[0].index)
                .toString();
          print(pointerValue);
        })
      ],

    );
letica commented 4 years ago

For the Stacked Bar Chart I'm getting same value for all Bar in a single Stack

image

is it possible show it for a single bar? I'm using following code for generating Stacked Bar chart.

charts.BarChart(
      seriesList,
      animate: animate,
      barGroupingType: charts.BarGroupingType.stacked,
      behaviors: [
        LinePointHighlighter(symbolRenderer: CustomCircleSymbolRenderer(),
            drawFollowLinesAcrossChart: false,
            defaultRadiusPx: 5,
            showVerticalFollowLine: charts.LinePointHighlighterFollowLineType.none,
            selectionModelType: charts.SelectionModelType.info,
          showHorizontalFollowLine: charts.LinePointHighlighterFollowLineType.none,

        )
      ],
      selectionModels: [
        SelectionModelConfig(changedListener: (SelectionModel model) {
          if (model.hasDatumSelection)
            pointerValue = "Selected: "+model.selectedSeries[0]
                .measureFn(model.selectedDatum[0].index)
                .toString();
          print(pointerValue);
        })
      ],

    );

did you figure out with this problem?

letica commented 4 years ago

thanks everyone! I did it ❤️

image

here are some snippets:

final size = MediaQuery.of(context).size;

//---
behaviors: [
        charts.SeriesLegend(
          position: charts.BehaviorPosition.top,
          horizontalFirst: false,
          desiredMaxRows: 2,
          cellPadding: EdgeInsets.only(right: 4.0, bottom: 4.0),
        ),
        charts.SelectNearest(
          eventTrigger: charts.SelectionTrigger.tapAndDrag
        ),
        charts.LinePointHighlighter(
          symbolRenderer: CustomCircleSymbolRenderer(size: size),
        ),
 ],
selectionModels: [
        charts.SelectionModelConfig(
          type: charts.SelectionModelType.info,
          changedListener: (charts.SelectionModel model) {
            if (model.hasDatumSelection) {
              selectedDatum = [];
              model.selectedDatum.forEach((charts.SeriesDatum datumPair) {
                selectedDatum.add({
                  'color': datumPair.series.colorFn(0),
                  'text': '${datumPair.datum.x}: ${datumPair.datum.y}'
                });
              });
            }
          }
        )
      ],

//---
class CustomCircleSymbolRenderer extends charts.CircleSymbolRenderer {
  final size;

  CustomCircleSymbolRenderer({ this.size });

  @override
  void paint(charts.ChartCanvas canvas, Rectangle bounds, {
    List dashPattern,
    charts.Color fillColor,
    charts.FillPatternType fillPattern,
    charts.Color strokeColor,
    double strokeWidthPx
  }) {
    super.paint(
      canvas,
      bounds,
      dashPattern: dashPattern,
      fillColor: fillColor,
      strokeColor: strokeColor,
      strokeWidthPx: strokeWidthPx
    );

    List tooltips = _LineChartWidgetState.selectedDatum;
    String unit = _LineChartWidgetState.unit;
    if (tooltips != null && tooltips.length > 0) {
      num tipTextLen = (tooltips[0]['text'] + unit).length;
      num rectWidth = bounds.width + tipTextLen * 8.3;
      num rectHeight = bounds.height + 20 + (tooltips.length - 1) * 18;
      num left = bounds.left > (size?.width ?? 300) / 2
        ? (bounds.left > size?.width / 4 ? bounds.left - rectWidth : bounds.left - rectWidth / 2)
        : bounds.left - 40;

      canvas.drawRect(
        Rectangle(left, 0, rectWidth, rectHeight),
        fill: charts.Color.fromHex(code: '#666666')
      );

      for (int i = 0; i < tooltips.length; i++) {
        canvas.drawPoint(
          point: Point(left.round() + 8, (i + 1) * 15),
          radius: 3,
          fill: tooltips[i]['color'],
          stroke: charts.Color.white,
          strokeWidthPx: 1,
        );
        chartStyle.TextStyle textStyle = chartStyle.TextStyle();
        textStyle.color = charts.Color.white;
        textStyle.fontSize = 13;
        canvas.drawText(chartText.TextElement(tooltips[i]['text'] + unit, style: textStyle), left.round() + 15, i * 15 + 8);
      }
    }
  }
}