Skip to content

Variable TimeScale margins and min/max values #358

@Drexel2k

Description

@Drexel2k

Hey!

I have a combined line/bar chart with a variable with TimeScale-scale and I configure the TimeScale with given ticks and min and max values.

The reason is that I have data only set on some of the ticks, not on all of them and I want the line to interpolate values between the values and to draw to the border of the chart with the correct interpolated value if there is value before the first tick or after the last tick.

But if I set the min and max value on the scale, I can't set a margin for the scale, so bars and values on the corner cases are cut off. I think it should be possible to set a margin even if you set min and max values and that the margins then apply to the min and max value on the chart..

Here is an example screenshot, whre you seee that the right bar and values are cut off, because of no margin:

Image

And here is an example code where I can't set marginMin and MarginMax on the 'datevar' variable scale:

import "package:flutter/material.dart";
import "package:graphic/graphic.dart";

class BarLinechart extends StatelessWidget {
  BarLinechart({super.key});

  final List<Tuple> _data = [
    {"dateinfo": DateTime(2025, 8, 11), "kcalintake": 1000, "kcaltarget": 1100},
    {"dateinfo": DateTime(2025, 9, 15), "kcalintake": 900, "kcaltarget": 1000},
    {"dateinfo": DateTime(2025, 11, 17), "kcalintake": 800, "kcaltarget": 900},
    {"dateinfo": DateTime(2025, 12, 1), "kcalintake": 700, "kcaltarget": 800},
  ];

  final Map<DateTime, String> _xAxisInfo = {
    DateTime(2025, 8, 25): "35/25",
    DateTime(2025, 9, 1): "36/25",
    DateTime(2025, 9, 8): "37/25",
    DateTime(2025, 9, 15): "38/25",
    DateTime(2025, 9, 22): "39/25",
    DateTime(2025, 9, 29): "40/25",
    DateTime(2025, 10, 6): "41/25",
    DateTime(2025, 10, 13): "42/25",
    DateTime(2025, 10, 20): "43/25",
    DateTime(2025, 10, 27): "44/25",
    DateTime(2025, 11, 3): "45/25",
    DateTime(2025, 11, 10): "46/25",
    DateTime(2025, 11, 17): "47/25",
    DateTime(2025, 11, 24): "48/25",
    DateTime(2025, 12, 1): "49/25",    
  };

  @override
  Widget build(BuildContext context) {
    final ColorScheme colorTheme = Theme.of(context).colorScheme;

    double barSize = 18;

    int? maxkCalIntake = _data.reduce((currentEntry, nextEntry) {
      if (currentEntry["kcalintake"] == null) {
        return nextEntry;
      }

      if (nextEntry["kcalintake"] == null) {
        return currentEntry;
      }

      if (currentEntry["kcalintake"] > nextEntry["kcalintake"]) {
        return currentEntry;
      } else {
        return nextEntry;
      }
    })["kcalintake"];

    int? maxkCalTarget = _data.reduce((currentEntry, nextEntry) {
      if (currentEntry["kcalintake"] == null) {
        return nextEntry;
      }

      if (nextEntry["kcalintake"] == null) {
        return currentEntry;
      }

      if (currentEntry["kcaltarget"] > nextEntry["kcaltarget"]) {
        return currentEntry;
      } else {
        return nextEntry;
      }
    })["kcaltarget"];

    int maxValue = 0;
    if (maxkCalIntake != null) {
      maxValue = maxkCalIntake;
    }

    if (maxkCalTarget != null) {
      if (maxkCalTarget > maxValue) {
        maxValue = maxkCalTarget;
      }
    }

    //offset for point values
    double markOffset = -15;
    int yAxisScaleMaxValue = (maxValue * 1.3).toInt();
    if (maxValue >= 100000) {
      markOffset = -20;
      yAxisScaleMaxValue = (maxValue * 1.5).toInt();
    } else if (maxValue >= 50000) {
      markOffset = -18;
      yAxisScaleMaxValue = (maxValue * 1.4).toInt();
    } else if (maxValue >= 10000) {
      markOffset = -18;
      yAxisScaleMaxValue = (maxValue * 1.35).toInt();
    } else if (maxValue >= 5000) {
      markOffset = -15;
      yAxisScaleMaxValue = (maxValue * 1.5).toInt();
    }

    double xAxisLabelXOffset = 14;
    double xAxisLabelYOffset = 18;

    DateTime displayFrom =_xAxisInfo.keys.first;

    DateTime displayUntil =_xAxisInfo.keys.last;

    return Column(
      children: [
        SizedBox(height: 5),
        SizedBox(
          width: 400,
          height: 150,
          child: Chart(
            data: _data,
            variables: {
              "datevar": Variable(
                accessor: (Map<dynamic, dynamic> map) => map["dateinfo"] as DateTime,
                scale: TimeScale(
                  min: displayFrom,
                  max: displayUntil,
                  ticks: _xAxisInfo.keys.toList(),
                  formatter: (DateTime date) {
                    return _xAxisInfo[date];
                  },
                ),
              ),
              "kcalintakeVar": Variable(
                accessor: (Map<dynamic, dynamic> map) => map["kcalintake"] != null ? map["kcalintake"] as num : 0,
                scale: LinearScale(
                  min: 0,
                  max: yAxisScaleMaxValue,
                  //value is num/double, this removes the decimal separator on y axis label.
                  formatter: (value) => value.toString(),
                  //ticks: [0, 500, 1000, 1500, 2000, 2500, 3000, 3500],
                ),
              ),
              "kcaltargetVar": Variable(
                accessor: (Map<dynamic, dynamic> map) => map["kcaltarget"] != null ? map["kcaltarget"] as num : 0,
                scale: LinearScale(min: 0, max: yAxisScaleMaxValue),
              ),
            },
            marks: [
              IntervalMark(
                size: SizeEncode(value: barSize),
                label: LabelEncode(
                  encoder: (Map<dynamic, dynamic> map) => Label(
                    map["kcalintakeVar"] > 0 ? map["kcalintakeVar"].toString() : "",
                    LabelStyle(
                      textStyle: TextStyle(fontSize: 10, color: const Color(0xff808080)),
                      offset: Offset(6, markOffset),
                      rotation: 4.72,
                    ),
                  ),
                ),
                color: ColorEncode(value: colorTheme.primary),
              ),
              LineMark(
                position: Varset("datevar") * Varset("kcaltargetVar"),
                size: SizeEncode(value: 1.5),
                color: ColorEncode(value: colorTheme.tertiary),
              ),
            ],
            axes: [
              AxisGuide(
                dim: Dim.x,
                line: PaintStyle(strokeColor: Color(0xffe8e8e8), strokeWidth: 1),
                label: LabelStyle(
                  textStyle: TextStyle(fontSize: 10, color: colorTheme.secondary),
                  offset: Offset(xAxisLabelXOffset, xAxisLabelYOffset),
                  rotation: 1,
                ),
              ),
              AxisGuide(
                dim: Dim.y,
                label: LabelStyle(
                  textStyle: TextStyle(fontSize: 10, color: colorTheme.secondary),
                  offset: const Offset(-7.5, 0),
                ),
                grid: PaintStyle(strokeColor: colorTheme.surfaceDim, strokeWidth: 1),
              ),
            ],
            padding: (_) => const EdgeInsets.fromLTRB(40, 5, 20, 20),
          ),
        ),
      ],
    );
  }
}

The example maybe doesn't make sense, I copied it from different charts, but it technically demonstrates the issue...

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions