Skip to content

Instantly share code, notes, and snippets.

Revisions

  1. @cirediew cirediew revised this gist Aug 28, 2020. 3 changed files with 235 additions and 0 deletions.
    78 changes: 78 additions & 0 deletions tooltip_data.dart
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,78 @@
    import 'package:my_app/database/database.dart';
    import 'package:my_app/util/extensions/iterable.ext.dart'; //used for firstOrNull
    import 'package:my_app/util/number_formatter.dart';
    import 'package:charts_flutter/flutter.dart';
    import 'package:equatable/equatable.dart';
    import 'package:flutter/cupertino.dart';
    import 'package:intl/intl.dart';

    class TooltipData with EquatableMixin {
    num value = 0;
    String formattedValue = '';
    String formattedIndex = '';
    int index = 0;
    int size = 0;
    int fractionDigits = 2;
    bool dateIsInFuture = false;
    int textSize = 14;
    double textScaleFactor = 1.0;
    NumberFormat format = NumberFormat.decimalPattern('en');

    TooltipData({
    this.value,
    this.formattedValue,
    this.formattedIndex,
    this.index,
    this.size,
    this.fractionDigits,
    });

    @override
    List get props => [
    value,
    formattedValue,
    formattedIndex,
    index,
    size,
    fractionDigits,
    dateIsInFuture,
    textSize,
    textScaleFactor,
    ];

    void updateFromModel(
    BuildContext context, {
    SelectionModel<DateTime> selectionModel,
    EnergyAssetCategory category,
    DateFormat dateFormat,
    double textScaleFactor,
    }) {
    value = selectionModel.selectedSeries.firstOrNull?.measureFn(
    selectionModel.selectedDatum.firstOrNull?.index,
    );
    format = decimalFormat(
    decimalDigits: fractionDigits ?? category.fractionDigits,
    );
    formattedValue = '${format.format(value)} ${category.suffix}';

    final date = selectionModel.selectedSeries.firstOrNull?.domainFn(
    selectionModel.selectedDatum.firstOrNull?.index,
    );

    dateIsInFuture = date?.isAfter(DateTime.now()) ?? false;
    formattedIndex = dateFormat.format(date);
    index = selectionModel.selectedDatum.firstOrNull?.index;
    size = selectionModel.selectedSeries.firstOrNull?.data?.length;
    this.textScaleFactor = textScaleFactor;
    }

    @override
    String toString() => 'TooltipData{'
    'hash: $hashCode, '
    'value: $value, '
    'formattedValue: $formattedValue, '
    'formattedIndex: $formattedIndex, '
    'index: $index, '
    'size: $size, '
    'dateIsInFuture: $dateIsInFuture}';
    }
    157 changes: 157 additions & 0 deletions year_chart.dart
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,157 @@
    import 'package:my_app/database/data_classes/bundle_chart_data.dart';
    import 'package:my_app/database/database.dart';
    import 'package:my_app/util/chart/chart_util.dart';
    import 'package:my_app/util/chart/custom_date_time_factory.dart';
    import 'package:my_app/util/chart/single_bundle_symbol_renderer.dart';
    import 'package:my_app/util/chart/tooltip_data.dart';
    import 'package:charts_common/common.dart' show SymbolRenderer;
    import 'package:charts_flutter/flutter.dart';
    import 'package:equatable/equatable.dart';
    import 'package:flutter/material.dart';
    import 'package:intl/intl.dart';

    class YearChart extends StatefulWidget {
    final EnergyAssetCategory category;
    final List<Series<ChartDataPoint, DateTime>> seriesList;

    const YearChart(this.seriesList, this.category);

    @override
    _YearChartState createState() => _YearChartStateState();
    }

    class _YearChartState extends State<YearChart> {
    SelectionModel<DateTime> _selectionModel;
    TooltipData _tooltipData;
    MarginSpec _marginSpec;

    @override
    void initState() {
    super.initState();
    _tooltipData = TooltipData(fractionDigits: 0);
    _marginSpec = MarginSpec.fromPercent(
    minPercent: 5,
    maxPercent: 50,
    );
    }

    @override
    Widget build(BuildContext context) {
    final Color textColor =
    ColorUtil.fromDartColor(Theme.of(context).colorScheme.onSurface);
    final textScaleFactor = MediaQuery.textScaleFactorOf(context);

    if (_tooltipData.textScaleFactor != textScaleFactor) {
    _tooltipData.textScaleFactor = textScaleFactor;
    }
    return TimeSeriesChart(
    widget.seriesList,
    layoutConfig: LayoutConfig(
    leftMarginSpec: _marginSpec,
    topMarginSpec: MarginSpec.fromPercent(
    minPercent: 10,
    maxPercent: 50,
    ),
    rightMarginSpec: MarginSpec.defaultSpec,
    bottomMarginSpec: _marginSpec,
    ),
    defaultRenderer:
    BarRendererConfig<DateTime>(groupingType: BarGroupingType.stacked),
    dateTimeFactory:
    CustomDateTimeFactory(Localizations.localeOf(context).languageCode),
    primaryMeasureAxis: NumericAxisSpec(
    showAxisLine: false,
    renderSpec: SmallTickRendererSpec<num>(
    labelStyle: TextStyleSpec(
    color: textColor,
    fontSize: (ChartUtil.defaultLabelSize * textScaleFactor).round()),
    ),
    ),
    domainAxis: DateTimeAxisSpec(
    renderSpec: SmallTickRendererSpec<DateTime>(
    labelStyle: TextStyleSpec(
    color: textColor,
    fontSize: (ChartUtil.defaultLabelSize * textScaleFactor).round()),
    ),
    tickFormatterSpec: const AutoDateTimeTickFormatterSpec(
    minute: ChartUtil.emptyFormatterSpec,
    hour: ChartUtil.emptyFormatterSpec,
    day: ChartUtil.emptyFormatterSpec,
    month: ChartUtil.monthFormatterSpec,
    year: ChartUtil.yearFormatterSpec,
    ),
    ),
    behaviors: [
    SelectNearest(),
    DomainHighlighter(),
    CustomLinePointHighlighter(
    symbolRenderer: SingleBundleSymbolRenderer(
    model: _selectionModel,
    tooltipData: _tooltipData,
    textColor: textColor,
    textScaleFactor: textScaleFactor,
    backgroundColor: ColorUtil.fromDartColor(
    Theme.of(context).canvasColor,
    ),
    strokeColor: ColorUtil.fromDartColor(
    widget.category.getColor(),
    ),
    ),
    drawFollowLinesAcrossChart: true,
    showVerticalFollowLine: LinePointHighlighterFollowLineType.none,
    showHorizontalFollowLine: LinePointHighlighterFollowLineType.all,
    defaultRadiusPx: 0,
    ),
    ],
    selectionModels: [
    SelectionModelConfig(
    changedListener: (SelectionModel<DateTime> model) {
    if (model != null && model.hasAnySelection) {
    _tooltipData.updateFromModel(context,
    selectionModel: model,
    category: widget.category,
    dateFormat: DateFormat.MMMM(),
    textScaleFactor: textScaleFactor);
    }
    },
    ),
    ],
    );
    }
    }

    ///Overridden class because [symbolRenderer] is missing from equals and hashcode in [LinePointHighlighter]
    class CustomLinePointHighlighter extends LinePointHighlighter
    with EquatableMixin {
    CustomLinePointHighlighter({
    SelectionModelType selectionModelType,
    double defaultRadiusPx,
    double radiusPaddingPx,
    LinePointHighlighterFollowLineType showHorizontalFollowLine,
    LinePointHighlighterFollowLineType showVerticalFollowLine,
    List<int> dashPattern,
    bool drawFollowLinesAcrossChart,
    SymbolRenderer symbolRenderer,
    }) : super(
    selectionModelType: selectionModelType,
    defaultRadiusPx: defaultRadiusPx,
    radiusPaddingPx: radiusPaddingPx,
    showHorizontalFollowLine: showHorizontalFollowLine,
    showVerticalFollowLine: showVerticalFollowLine,
    dashPattern: dashPattern,
    drawFollowLinesAcrossChart: drawFollowLinesAcrossChart,
    symbolRenderer: symbolRenderer,
    );

    @override
    List<Object> get props => [
    selectionModelType,
    defaultRadiusPx,
    radiusPaddingPx,
    showHorizontalFollowLine,
    showVerticalFollowLine,
    dashPattern,
    drawFollowLinesAcrossChart,
    symbolRenderer,
    ];
    }
  2. @cirediew cirediew created this gist Aug 28, 2020.
    132 changes: 132 additions & 0 deletions SingleBundleSymbolRenderer.dart
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,132 @@
    import 'dart:math' show Rectangle;

    import 'package:my_app/database/database.dart';
    import 'package:my_app/util/chart/tooltip_data.dart';
    import 'package:charts_flutter/flutter.dart'
    show
    CircleSymbolRenderer,
    Color,
    SelectionModel,
    ChartCanvas,
    FillPatternType;
    import 'package:charts_flutter/src/text_element.dart'; // ignore: implementation_imports
    import 'package:charts_flutter/src/text_style.dart'; // ignore: implementation_imports
    import 'package:equatable/equatable.dart';
    import 'package:meta/meta.dart';

    class SingleBundleSymbolRenderer extends CircleSymbolRenderer
    with EquatableMixin {
    final Color textColor;
    final Color backgroundColor;
    final Color strokeColor;
    final EnergyAssetCategory category;
    final TooltipData tooltipData;
    final SelectionModel<DateTime> model;
    final int horizontalMargin;
    final int verticalMargin;
    final int bottomPadding;
    final int textSize;
    final double textScaleFactor;
    final TextStyle textStyle;

    SingleBundleSymbolRenderer({
    @required this.model,
    @required this.tooltipData,
    @required this.textColor,
    @required this.backgroundColor,
    @required this.strokeColor,
    this.category,
    this.horizontalMargin = 4,
    this.verticalMargin = 7,
    this.bottomPadding = 4,
    this.textSize = 14,
    this.textScaleFactor = 1.0,
    bool isSolid = true,
    }) : textStyle = TextStyle()
    ..color = textColor
    ..fontSize = textSize,
    super(isSolid: isSolid);

    @override
    bool shouldRepaint(SingleBundleSymbolRenderer oldRenderer) =>
    this != oldRenderer;

    @override
    List<Object> get props => [
    model,
    tooltipData,
    textColor,
    backgroundColor,
    strokeColor,
    category,
    horizontalMargin,
    verticalMargin,
    bottomPadding,
    textSize,
    textScaleFactor,
    isSolid,
    textStyle,
    ];

    @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,
    );
    if (tooltipData.dateIsInFuture) {
    return;
    }
    TextElement textElement = category != null
    ? TextElement(
    '${tooltipData.formattedIndex}\n'
    '${category.title}: ${tooltipData.formattedValue}',
    style: textStyle,
    textScaleFactor: tooltipData.textScaleFactor,
    )
    : TextElement(
    '${tooltipData.formattedIndex}\n'
    '${tooltipData.formattedValue}',
    style: textStyle,
    textScaleFactor: tooltipData.textScaleFactor,
    );

    final offsetX = bounds.left -
    (textElement.measurement.horizontalSliceWidth *
    (tooltipData.index /
    ((tooltipData.size - 1 == 0 ? 1 : tooltipData.size - 1))));
    final offsetY = bounds.top -
    textElement.measurement.verticalSliceWidth -
    verticalMargin;

    canvas.drawRect(
    Rectangle(
    offsetX - horizontalMargin,
    offsetY - verticalMargin - bottomPadding,
    textElement.measurement.horizontalSliceWidth + horizontalMargin * 2,
    textElement.measurement.verticalSliceWidth + verticalMargin * 2,
    ),
    fill: backgroundColor,
    stroke: strokeColor,
    strokeWidthPx: 1,
    );

    canvas.drawText(
    textElement,
    offsetX.toInt(),
    offsetY.toInt() - bottomPadding,
    );
    }
    }