flutter-charts
flutter-charts copied to clipboard
Chart are displayed incorrectly when axisMin and axisMax are set
First of all, thank you for this amazing library, I really like the idea of using widgets to create charts.
I'm creating a line chart with target line
(y: 3.6) and target area
(y: 0 ~ 2).
I encountered some issues after setting axisMin
and axisMax
:
1. I have a TargetAreaDecoration
from 0 to 2, when axisMin
is set to 2, it is incorrectly displayed on the x-axis.
The solution I came up with is to dynamically modify the targetMin
and targetMax
of the TargetAreaDecoration
based on axisMin
and axisMax
. For example, when axisMin
is 1, I would change the TargetAreaDecoration's targetMin
from 0 to 1, so it won't be displayed on the x-axis anymore.
2. From the blue area in the graph, it can be observed that the rendering behavior of widgetItemBuilder
is inconsistent with that of SparkLineDecoration
. The size of widgetItemBuilder
is much larger than SparkLineDecoration
.
I created a Position
Widget within widgetItemBuilder
and positioned the point widget using its top
property. If the verticalMultiplier
parameter can be added to widgetItemBuilder
, I can calculate the correct position of the point:
Position(
bottom: (_mappedValues[data.listIndex][data.itemIndex].max! - axisMin) * verticalMultiplier + 8,
)
3. The target line is also being displayed in the wrong place.
The solution I came up with is similar to the first issue. I dynamically modify verticalMultiplier * (3.6 - axisMin)
based on axisMin
and axisMax
.
Do you have any suggestions? Thank you.
Codes
import 'package:charts_painter/chart.dart';
import 'package:flutter/material.dart';
const double axisWidth = 80.0;
class LineChart extends StatelessWidget {
final bool useAxis;
LineChart({Key? key, this.useAxis = false}) : super(key: key);
final List<List<ChartItem<double>>> _mappedValues = [
[ChartItem(2.0), ChartItem(5.0), ChartItem(8.0), ChartItem(3.0), ChartItem(6.0)]
];
@override
Widget build(BuildContext context) {
return SizedBox(
height: MediaQuery.of(context).size.height / 2,
child: Row(
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 32),
child: AnimatedContainer(
duration: const Duration(milliseconds: 350),
width: axisWidth,
child: DecorationsRenderer(
[
HorizontalAxisDecoration(
asFixedDecoration: true,
lineWidth: 0,
axisStep: 2,
showValues: true,
endWithChart: false,
axisValue: (value) => '$value',
legendFontStyle: Theme.of(context).textTheme.bodyMedium,
valuesAlign: TextAlign.center,
valuesPadding: const EdgeInsets.only(left: -axisWidth, bottom: -10),
showLines: false,
showTopValue: true,
)
],
ChartState<double>(
data: ChartData(
_mappedValues,
axisMin: useAxis ? 2 : null,
axisMax: useAxis ? 8 : null,
dataStrategy: const DefaultDataStrategy(stackMultipleValues: true),
),
itemOptions: WidgetItemOptions(widgetItemBuilder: (data) {
return const SizedBox();
}),
backgroundDecorations: [
GridDecoration(
showVerticalValues: true,
verticalLegendPosition: VerticalLegendPosition.bottom,
verticalValuesPadding: const EdgeInsets.only(top: 8.0),
verticalAxisStep: 2,
gridWidth: 1,
textStyle: Theme.of(context).textTheme.labelSmall,
),
],
),
),
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 32),
child: AnimatedChart<double>(
width: MediaQuery.of(context).size.width - axisWidth - 8,
duration: const Duration(milliseconds: 450),
state: ChartState<double>(
data: ChartData(
_mappedValues,
axisMin: useAxis ? 2 : null,
axisMax: useAxis ? 8 : null,
dataStrategy: const DefaultDataStrategy(stackMultipleValues: true),
),
itemOptions: WidgetItemOptions(widgetItemBuilder: (data) {
return Stack(
clipBehavior: Clip.none,
children: [
Container(color: Colors.blue.withOpacity(0.2)),
Positioned(
top: -24,
left: 0,
right: 0,
child: Column(
children: [
Center(
child: Text(
_mappedValues[data.listIndex][data.itemIndex].max.toString()))
],
),
),
Positioned(
top: -5,
left: 0,
right: 0,
child: Column(
children: [
Center(
child: Container(
width: 10,
height: 10,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
borderRadius: const BorderRadius.all(Radius.circular(8)),
border: Border.all(
width: 1.4,
color: Theme.of(context).colorScheme.surface)),
),
)
],
),
),
],
);
}),
foregroundDecorations: [],
backgroundDecorations: [
GridDecoration(
horizontalAxisStep: 2,
showVerticalGrid: false,
showVerticalValues: true,
verticalLegendPosition: VerticalLegendPosition.bottom,
verticalValuesPadding: const EdgeInsets.only(top: 8.0),
verticalAxisStep: 1,
gridColor: Theme.of(context).colorScheme.outline.withOpacity(0.3),
dashArray: [8, 8],
gridWidth: 1,
textStyle: Theme.of(context).textTheme.labelSmall,
),
WidgetDecoration(
widgetDecorationBuilder:
(context, chartState, itemWidth, verticalMultiplier) {
return Padding(
padding: chartState.defaultMargin,
child: Stack(
children: [
Positioned(
right: 0,
left: 0,
bottom: verticalMultiplier * 3.6,
child: CustomPaint(painter: DashedLinePainter()),
),
],
),
);
},
),
TargetAreaDecoration(
targetAreaFillColor: Theme.of(context).colorScheme.error.withOpacity(0.6),
targetLineColor: Colors.transparent,
lineWidth: 0,
targetMax: 2,
targetMin: 0,
),
SparkLineDecoration(
lineWidth: 2,
lineColor: Theme.of(context).colorScheme.primary,
smoothPoints: true,
listIndex: 0,
),
],
),
),
),
)
],
),
);
}
}
class DashedLinePainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
double dashWidth = 8, dashSpace = 8, startX = 0;
final paint = Paint()
..color = Colors.blue
..strokeWidth = 1;
while (startX < size.width) {
canvas.drawLine(Offset(startX, 0), Offset(startX + dashWidth, 0), paint);
startX += dashWidth + dashSpace;
}
}
@override
bool shouldRepaint(CustomPainter oldDelegate) => false;
}
Hi @iamiota
Thanks for detailed report. I was able to reproduce it with your code easily and fixing it right now. There is a issue when calculating size for widgets when axisMin is set. I will add some tests to cover this case as well so we can be sure that values are displayed correctly 😄
@lukaknezic Hi Luka, thank you for your fix.❤️ I have tried the latest code, and the size of the widget is now correct.
However, I have encountered another issue when dynamically modifying the chart data.
If _mappedValues
, axisMin
and axisMax
are the props of LineChart
, and when the props change according to the following, the chart throws repeat errors: 'BoxConstraints has a negative minimum height.'
Additionally, in the video, it can be observed that the animation of the line extends beyond the horizontal axis area.
I used ClipRect
to clip the AnimatedChart
to prevent the line animation from being drawn across the full screen.
final List<List<ChartItem<double>>> _mappedValues = [
[ChartItem(2.0), ChartItem(5.0), ChartItem(8.0), ChartItem(3.0), ChartItem(6.0)]
];
axisMin: 2
axisMax: 8
/// AnimatedChart's duration: const Duration(milliseconds: 500),
/// If milliseconds = 30, everything is good.
/// data change to
final List<List<ChartItem<double>>> _mappedValues = [
[ChartItem(32.0), ChartItem(35.0), ChartItem(38.0), ChartItem(33.0), ChartItem(36.0)]
];
axisMin: 32
axisMax: 38
https://github.com/infinum/flutter-charts/assets/7531576/a7c2443b-10f8-463d-a24d-001de75049cf
negative minimum height should also be fixed, in same branch 😄