Is there a way to put label text vertically in flutter charts_flutter: ^0.8.1

4,330

To achieve it you need to create a new class where you need to overwrite one of the internal classes - 'BarRendererDecorator'. In that class there is a method - _decorateVerticalBars which writes the value inside the bars. There is a method call - canvas.drawText() which needs to be fed with one new argument rotation:-math.pi/2. Also to center the text inside the bar need to change the X offset like this - labelX = labelX + (labelElement.measurement.horizontalSliceWidth / 2.5).round(); And then in your decorator call in your graph use the new class you have created. Here is the complete code of the new class. And accept it as answer if it works for you.

import 'dart:math' as math;
import 'package:meta/meta.dart' show required;
import "package:charts_common/common.dart"
    show
        Color,
        GraphicsFactory,
        TextDirection,
        TextElement,
        TextStyle,
        ChartCanvas,
        TextStyleSpec,
        ImmutableBarRendererElement,
        BarRendererDecorator;
import "package:charts_common/src/data/series.dart" show AccessorFn;
import "chart_canvas_wrkaround.dart";
import "package:charts_flutter/src/chart_canvas.dart" as Canvasee;
import "package:charts_common/common.dart" as comm;
/* import '../../common/color.dart' show Color;
import '../../common/graphics_factory.dart' show GraphicsFactory;
import '../../common/text_element.dart' show TextDirection, TextElement;
import '../../common/text_style.dart' show TextStyle;
import '../../data/series.dart' show AccessorFn;
import '../cartesian/axis/spec/axis_spec.dart' show TextStyleSpec;
import '../common/chart_canvas.dart' show ChartCanvas;
import 'bar_renderer.dart' show ImmutableBarRendererElement;
import 'bar_renderer_decorator.dart' show BarRendererDecorator; */
class BarLabelDecoratorWorkAround<D> extends BarRendererDecorator<D> {
  // Default configuration
  static const _defaultLabelPosition = BarLabelPosition.auto;
  static const _defaultLabelPadding = 5;
  static const _defaultHorizontalLabelAnchor = BarLabelAnchor.start;
  static const _defaultVerticalLabelAnchor = BarLabelAnchor.end;
  static final _defaultInsideLabelStyle =
      new TextStyleSpec(fontSize: 12, color: Color.white);
  static final _defaultOutsideLabelStyle =
      new TextStyleSpec(fontSize: 12, color: Color.black);
  static final _labelSplitPattern = '\n';
  static final _defaultMultiLineLabelPadding = 2;
  /// Configures [TextStyleSpec] for labels placed inside the bars.
  final TextStyleSpec insideLabelStyleSpec;
  /// Configures [TextStyleSpec] for labels placed outside the bars.
  final TextStyleSpec outsideLabelStyleSpec;
  /// Configures where to place the label relative to the bars.
  final BarLabelPosition labelPosition;
  /// For labels drawn inside the bar, configures label anchor position.
  final BarLabelAnchor labelAnchor;
  /// Space before and after the label text.
  final int labelPadding;
  BarLabelDecoratorWorkAround(
      {TextStyleSpec insideLabelStyleSpec,
      TextStyleSpec outsideLabelStyleSpec,
      this.labelAnchor = null,
      this.labelPosition = _defaultLabelPosition,
      this.labelPadding = _defaultLabelPadding})
      : insideLabelStyleSpec = insideLabelStyleSpec ?? _defaultInsideLabelStyle,
        outsideLabelStyleSpec =
            outsideLabelStyleSpec ?? _defaultOutsideLabelStyle;
  @override
  void decorate(Iterable<ImmutableBarRendererElement<D>> barElements,
      comm.ChartCanvas canvas, GraphicsFactory graphicsFactory,
      {@required math.Rectangle drawBounds,
      @required double animationPercent,
      @required bool renderingVertically,
      bool rtl = false}) {
    // Only decorate the bars when animation is at 100%.
    if (animationPercent != 1.0) {
      return;
    }
    /* final newCanvas = canvas as Canvasee.ChartCanvas;
    final canvee =
        ChartCanvasWorkAround(newCanvas.canvas, newCanvas.graphicsFactory); */
    if (renderingVertically) {
      _decorateVerticalBars(
          barElements, canvas, graphicsFactory, drawBounds, rtl);
    } else {
      _decorateHorizontalBars(
          barElements, canvas, graphicsFactory, drawBounds, rtl);
    }
  }
  void _decorateVerticalBars(
      Iterable<ImmutableBarRendererElement<D>> barElements,
      ChartCanvas canvas,
      GraphicsFactory graphicsFactory,
      math.Rectangle drawBounds,
      bool rtl) {
    // Create [TextStyle] from [TextStyleSpec] to be used by all the elements.
    // The [GraphicsFactory] is needed so it can't be created earlier.
    final insideLabelStyle =
        _getTextStyle(graphicsFactory, insideLabelStyleSpec);
    final outsideLabelStyle =
        _getTextStyle(graphicsFactory, outsideLabelStyleSpec);
    for (var element in barElements) {
      final labelFn = element.series.labelAccessorFn;
      final datumIndex = element.index;
      final label = (labelFn != null) ? labelFn(datumIndex) : null;
      // If there are custom styles, use that instead of the default or the
      // style defined for the entire decorator.
      final datumInsideLabelStyle = _getDatumStyle(
          element.series.insideLabelStyleAccessorFn,
          datumIndex,
          graphicsFactory,
          defaultStyle: insideLabelStyle);
      final datumOutsideLabelStyle = _getDatumStyle(
          element.series.outsideLabelStyleAccessorFn,
          datumIndex,
          graphicsFactory,
          defaultStyle: outsideLabelStyle);
      // Skip calculation and drawing for this element if no label.
      if (label == null || label.isEmpty) {
        continue;
      }
      var labelElements = label
          .split(_labelSplitPattern)
          .map((labelPart) => graphicsFactory.createTextElement(labelPart));
      final bounds = element.bounds;
      // Get space available inside and outside the bar.
      final totalPadding = labelPadding * 2;
      final insideBarHeight = bounds.height - totalPadding;
      final outsideBarHeight = drawBounds.height - bounds.height - totalPadding;
      var calculatedLabelPosition = labelPosition;
      if (calculatedLabelPosition == BarLabelPosition.auto) {
        // For auto, first try to fit the text inside the bar.
        labelElements = labelElements.map(
            (labelElement) => labelElement..textStyle = datumInsideLabelStyle);
        final labelMaxWidth = labelElements
            .map(
                (labelElement) => labelElement.measurement.horizontalSliceWidth)
            .fold(0, (max, current) => max > current ? max : current);
        // Total label height depends on the label element's text style.
        final totalLabelHeight = _getTotalLabelHeight(labelElements);
        // A label fits if the length and width of the text fits.
        calculatedLabelPosition =
            totalLabelHeight < insideBarHeight && labelMaxWidth < bounds.width
                ? BarLabelPosition.inside
                : BarLabelPosition.outside;
      }
      // Set the max width, text style, and text direction.
      labelElements = labelElements.map((labelElement) => labelElement
        ..textStyle = calculatedLabelPosition == BarLabelPosition.inside
            ? datumInsideLabelStyle
            : datumOutsideLabelStyle
        ..maxWidth = bounds.height * 100
        ..textDirection = rtl ? TextDirection.rtl : TextDirection.ltr);
      // Total label height depends on the label element's text style.
      final totalLabelHeight = _getTotalLabelHeight(labelElements);
      var labelsDrawn = 0;
      for (var labelElement in labelElements) {
        // Calculate the start position of label based on [labelAnchor].
        int labelY;
        final labelHeight = labelElement.measurement.verticalSliceWidth.round();
        final offsetHeight =
            (labelHeight + _defaultMultiLineLabelPadding) * labelsDrawn;
        if (calculatedLabelPosition == BarLabelPosition.inside) {
          final _labelAnchor = labelAnchor ?? _defaultVerticalLabelAnchor;
          switch (_labelAnchor) {
            case BarLabelAnchor.end:
              labelY = bounds.top + labelPadding + offsetHeight;
              break;
            case BarLabelAnchor.middle:
              labelY = (bounds.bottom -
                      bounds.height / 2 -
                      totalLabelHeight / 2 +
                      offsetHeight)
                  .round();
              break;
            case BarLabelAnchor.start:
              labelY = bounds.bottom -
                  labelPadding -
                  totalLabelHeight +
                  offsetHeight;
              break;
          }
        } else {
          // calculatedLabelPosition == LabelPosition.outside
          labelY = bounds.top - labelPadding - totalLabelHeight + offsetHeight;
        }
        // Center the label inside the bar.
        int labelX = (bounds.left +
                bounds.width / 2 -
                labelElement.measurement.horizontalSliceWidth / 2)
            .round();
        labelX = labelX +
            (labelElement.measurement.horizontalSliceWidth / 2.5).round();
        canvas.drawText(labelElement, labelX, labelY, rotation: -math.pi / 2);
        labelsDrawn += 1;
      }
    }
  }
  void _decorateHorizontalBars(
      Iterable<ImmutableBarRendererElement<D>> barElements,
      ChartCanvas canvas,
      GraphicsFactory graphicsFactory,
      math.Rectangle drawBounds,
      bool rtl) {
    // Create [TextStyle] from [TextStyleSpec] to be used by all the elements.
    // The [GraphicsFactory] is needed so it can't be created earlier.
    final insideLabelStyle =
        _getTextStyle(graphicsFactory, insideLabelStyleSpec);
    final outsideLabelStyle =
        _getTextStyle(graphicsFactory, outsideLabelStyleSpec);
    for (var element in barElements) {
      final labelFn = element.series.labelAccessorFn;
      final datumIndex = element.index;
      final label = (labelFn != null) ? labelFn(datumIndex) : null;
      // If there are custom styles, use that instead of the default or the
      // style defined for the entire decorator.
      final datumInsideLabelStyle = _getDatumStyle(
          element.series.insideLabelStyleAccessorFn,
          datumIndex,
          graphicsFactory,
          defaultStyle: insideLabelStyle);
      final datumOutsideLabelStyle = _getDatumStyle(
          element.series.outsideLabelStyleAccessorFn,
          datumIndex,
          graphicsFactory,
          defaultStyle: outsideLabelStyle);
      // Skip calculation and drawing for this element if no label.
      if (label == null || label.isEmpty) {
        continue;
      }
      final bounds = element.bounds;
      // Get space available inside and outside the bar.
      final totalPadding = labelPadding * 2;
      final insideBarWidth = bounds.width - totalPadding;
      final outsideBarWidth = drawBounds.width - bounds.width - totalPadding;
      final labelElement = graphicsFactory.createTextElement(label);
      var calculatedLabelPosition = labelPosition;
      if (calculatedLabelPosition == BarLabelPosition.auto) {
        // For auto, first try to fit the text inside the bar.
        labelElement.textStyle = datumInsideLabelStyle;
        // A label fits if the space inside the bar is >= outside bar or if the
        // length of the text fits and the space. This is because if the bar has
        // more space than the outside, it makes more sense to place the label
        // inside the bar, even if the entire label does not fit.
        calculatedLabelPosition = (insideBarWidth >= outsideBarWidth ||
                labelElement.measurement.horizontalSliceWidth < insideBarWidth)
            ? BarLabelPosition.inside
            : BarLabelPosition.outside;
      }
      // Set the max width and text style.
      if (calculatedLabelPosition == BarLabelPosition.inside) {
        labelElement.textStyle = datumInsideLabelStyle;
        labelElement.maxWidth = insideBarWidth;
      } else {
        // calculatedLabelPosition == LabelPosition.outside
        labelElement.textStyle = datumOutsideLabelStyle;
        labelElement.maxWidth = outsideBarWidth;
      }
      // Only calculate and draw label if there's actually space for the label.
      if (labelElement.maxWidth > 0) {
        // Calculate the start position of label based on [labelAnchor].
        int labelX;
        if (calculatedLabelPosition == BarLabelPosition.inside) {
          final _labelAnchor = labelAnchor ?? _defaultHorizontalLabelAnchor;
          switch (_labelAnchor) {
            case BarLabelAnchor.middle:
              labelX = (bounds.left +
                      bounds.width / 2 -
                      labelElement.measurement.horizontalSliceWidth / 2)
                  .round();
              labelElement.textDirection =
                  rtl ? TextDirection.rtl : TextDirection.ltr;
              break;
            case BarLabelAnchor.end:
            case BarLabelAnchor.start:
              final alignLeft = rtl
                  ? (_labelAnchor == BarLabelAnchor.end)
                  : (_labelAnchor == BarLabelAnchor.start);
              if (alignLeft) {
                labelX = bounds.left + labelPadding;
                labelElement.textDirection = TextDirection.ltr;
              } else {
                labelX = bounds.right - labelPadding;
                labelElement.textDirection = TextDirection.rtl;
              }
              break;
          }
        } else {
          // calculatedLabelPosition == LabelPosition.outside
          labelX = bounds.right + labelPadding;
          labelElement.textDirection = TextDirection.ltr;
        }
        // Center the label inside the bar.
        final labelY = (bounds.top +
                (bounds.bottom - bounds.top) / 2 -
                labelElement.measurement.verticalSliceWidth / 2)
            .round();
        canvas.drawText(labelElement, labelX, labelY);
      }
    }
  }
  /// Helper function to get the total height for a group of labels.
  /// This includes the padding in between the labels.
  int _getTotalLabelHeight(Iterable<TextElement> labelElements) =>
      (labelElements.first.measurement.verticalSliceWidth *
              labelElements.length)
          .round() +
      _defaultMultiLineLabelPadding * (labelElements.length - 1);
  // Helper function that converts [TextStyleSpec] to [TextStyle].
  TextStyle _getTextStyle(
      GraphicsFactory graphicsFactory, TextStyleSpec labelSpec) {
    return graphicsFactory.createTextPaint()
      ..color = labelSpec?.color ?? Color.black
      ..fontFamily = labelSpec?.fontFamily
      ..fontSize = labelSpec?.fontSize ?? 12
      ..lineHeight = labelSpec?.lineHeight;
  }
  /// Helper function to get datum specific style
  TextStyle _getDatumStyle(AccessorFn<TextStyleSpec> labelFn, int datumIndex,
      GraphicsFactory graphicsFactory,
      {TextStyle defaultStyle}) {
    final styleSpec = (labelFn != null) ? labelFn(datumIndex) : null;
    return (styleSpec != null)
        ? _getTextStyle(graphicsFactory, styleSpec)
        : defaultStyle;
  }
}
/// Configures where to place the label relative to the bars.
enum BarLabelPosition {
  /// Automatically try to place the label inside the bar first and place it on
  /// the outside of the space available outside the bar is greater than space
  /// available inside the bar.
  auto,
  /// Always place label on the outside.
  outside,
  /// Always place label on the inside.
  inside,
}
/// Configures where to anchor the label for labels drawn inside the bars.
enum BarLabelAnchor {
  /// Anchor to the measure start.
  start,
  /// Anchor to the middle of the measure range.
  middle,
  /// Anchor to the measure end.
  end,
}
Share:
4,330
Sudheer K
Author by

Sudheer K

At Day: I work for my company. At Night : I spend time on learning new things and maintain my contacts. At Holidays : I go out for my hobbies (Mainly Photographhy)and rest time I spend time with my buddies. I am a person who first build something. then make some crazy edits so that whole thing goes upside down.. And then fix it. I love making changes..

Updated on December 14, 2022

Comments

  • Sudheer K
    Sudheer K 3 minutes

    I am trying to create a graph using charts_flutter: ^0.8.1 package in flutter.

    Here is my code. I have added SlidingViewport and PanAndZoomBehavior in behaviour.

      charts.BarChart(
                generateGraphData(months),
                barRendererDecorator: charts.BarLabelDecorator<String>(
                labelAnchor: charts.BarLabelAnchor.middle,
                labelPosition: charts.BarLabelPosition.inside,                ),
                domainAxis: new charts.OrdinalAxisSpec(viewport: new charts.OrdinalViewport('month', 5),),
                  animate: false,
                  rtlSpec: charts.RTLSpec(axisDirection: charts.AxisDirection.reversed),
                  defaultInteractions: false,
                  behaviors: [
                    new charts.SlidingViewport(),
                    new charts.PanAndZoomBehavior(),
                  ],
                ),
    

    Is it possible to make the labeltext align vertically like in Horizontal Bar Label Bar? Some thing like the below image. I want to align the label text in the direction of bar chart. something like this

    • Braj
      Braj about 3 years
      Did you find solution for this?
    • Sudheer K
      Sudheer K about 3 years
      @Braj No I did not
    • Braj
      Braj about 3 years
      i will post the solution in some time
  • Sudheer K
    Sudheer K almost 3 years
    I was able to set the label vertically. But the text is not getting positioned to the center of chart. Can you add a sample usage also?