Incorrect calculation of axis interval
- Dominant language
- Dart
- Stars
- 7.6k
- Forks
- 2k
- Avg merge
- 9d 1h
- Merged PRs (30d)
- 2
Description
**Describe the bug**
Automatic axis interval is not properly calculated. In the example below I am trying to represent the evolution of a particular value over time.
**To Reproduce**
```dart
import 'package:fl_chart_app/presentation/resources/app_resources.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
class _LineChart extends StatelessWidget {
const _LineChart({required this.isShowingMainData});
final bool isShowingMainData;
@override
Widget build(BuildContext context) {
return LineChart(
sampleData1,
duration: const Duration(milliseconds: 250),
);
}
LineChartData get sampleData1 => LineChartData(
lineTouchData: lineTouchData1,
gridData: gridData,
titlesData: titlesData1,
borderData: borderData,
lineBarsData: lineBarsData1,
);
LineTouchData get lineTouchData1 => LineTouchData(
handleBuiltInTouches: true,
touchTooltipData: LineTouchTooltipData(
getTooltipColor: (touchedSpot) => Colors.blueGrey.withOpacity(0.8),
),
);
FlTitlesData get titlesData1 => FlTitlesData(
bottomTitles: AxisTitles(
sideTitles: bottomTitles,
),
rightTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
topTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
leftTitles: AxisTitles(
sideTitles: leftTitles(),
),
);
List get lineBarsData1 => [
lineChartBarData1_1,
lineChartBarData1_2,
];
Widget leftTitleWidgets(double value, TitleMeta meta) {
const style = TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
);
String text;
switch (value.toInt()) {
case 1:
text = '1m';
break;
case 2:
text = '2m';
break;
case 3:
text = '3m';
break;
case 4:
text = '5m';
break;
case 5:
text = '6m';
break;
default:
return Container();
}
return Text(text, style: style, textAlign: TextAlign.center);
}
SideTitles leftTitles() => SideTitles(
getTitlesWidget: leftTitleWidgets,
showTitles: true,
interval: 1,
reservedSize: 40,
);
Widget bottomTitleWidgets(double value, TitleMeta meta) {
const style = TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
);
DateTime dateTime = DateTime.fromMillisecondsSinceEpoch(value.toInt());
DateFormat dateFormat = DateFormat('dd/MM/yyyy');
Widget text = Text(dateFormat.format(dateTime), style: style);
return SideTitleWidget(
axisSide: meta.axisSide,
fitInside: SideTitleFitInsideData.fromTitleMeta(meta),
space: 10,
child: text,
);
}
SideTitles get bottomTitles => SideTitles(
showTitles: true,
reservedSize: 32,
getTitlesWidget: bottomTitleWidgets,
minIncluded: true,
maxIncluded: true,
);
FlGridData get gridData => const FlGridData(show: false);
FlBorderData get borderData => FlBorderData(
show: true,
border: Border(
bottom:
BorderSide(color: AppColors.primary.withOpacity(0.2), width: 4),
left: const BorderSide(color: Colors.transparent),
right: const BorderSide(color: Colors.transparent),
top: const BorderSide(color: Colors.transparent),
),
);
LineChartBarData get lineChartBarData1_1 {
List spots = const [
FlSpot(1729659600000.0, 1),
FlSpot(1729573200000.0, 1.5),
FlSpot(1729291369000.0, 1.4),
FlSpot(1729262809000.0, 3.4),
FlSpot(1729227600000.0, 2),
FlSpot(1729198190000.0, 2.2),
FlSpot(1729169570000.0, 1.8),
FlSpot(1729141200000.0, 1.8),
FlSpot(1729140622000.0, 1.8),
FlSpot(1729112007000.0, 1.8),
FlSpot(1729083422000.0, 1.8),
FlSpot(1729054800000.0, 1.8),
];
return LineChartBarData(
isCurved: true,
color: AppColors.contentColorGreen,
barWidth: 8,
isStrokeCapRound: true,
dotData: const FlDotData(show: false),
belowBarData: BarAreaData(show: false),
spots: spots,
);
}
LineChartBarData get lineChartBarData1_2 {
List spots = const [
FlSpot(1729659600000.0, 1.2),
FlSpot(1729573200000.0, 1.6),
FlSpot(1729291369000.0, 1.5),
FlSpot(1729262809000.0, 3.6),
FlSpot(1729227600000.0, 2.3),
FlSpot(1729198190000.0, 2.6),
FlSpot(1729169570000.0, 1.9),
FlSpot(1729141200000.0, 1.9),
FlSpot(1729140622000.0, 2.2),
FlSpot(1729112007000.0, 1.9),
FlSpot(1729083422000.0, 1.2),
FlSpot(1729054800000.0, 1.4),
];
return LineChartBarData(
isCurved: true,
color: AppColors.contentColorBlue,
barWidth: 8,
isStrokeCapRound: true,
dotData: const FlDotData(show: false),
belowBarData: BarAreaData(show: false),
spots: spots,
);
}
}
class LineChartSample1Test extends StatefulWidget {
const LineChartSample1Test({super.key});
@override
State createState() => LineChartSample1TestState();
}
class LineChartSample1TestState extends State {
late bool isShowingMainData;
@override
void initState() {
super.initState();
isShowingMainData = true;
}
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.only(
right: 20,
),
child: SizedBox(
height: 500,
child: Stack(
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const SizedBox(
height: 37,
),
const Text(
'Monthly Sales',
style: TextStyle(
color: AppColors.primary,
fontSize: 32,
fontWeight: FontWeight.bold,
letterSpacing: 2,
),
textAlign: TextAlign.center,
),
const SizedBox(
height: 37,
),
Expanded(
child: Padding(
padding: const EdgeInsets.only(right: 16, left: 6),
child: _LineChart(isShowingMainData: isShowingMainData),
),
),
const SizedBox(
height: 10,
),
],
),
IconButton(
icon: Icon(
Icons.refresh,
color: Colors.white.withOpacity(isShowingMainData ? 1.0 : 0.5),
),
onPressed: () {
setState(() {
isShowingMainData = !isShowingMainData;
});
},
)
],
),
),
);
}
}
```
**Screenshots**
Incorrect behaviour:
https://github.com/user-attachments/assets/1f26e141-2230-47f7-9c1f-9b6ae612f9b8
Correct behaviour after own modifications:
https://github.com/user-attachments/assets/c10c17b0-897b-4d3e-8d78-cd1aa53a6abc
**Versions**
- Flutter: 3.24.3
- FlChart: 0.69.0
------------------------------------------------------------------------------------------------------------
As you can see, dates in the x-axis overlap, I am just using example "LineChartSample1" in the repo with slight modifications.
Find below the modifications I had to make in order to work as per my needs, just in case you find it useful. Don't take these modifications for granted, this is just intended to prove my point.
- utils.dart
In the code below, the value that works is `accurateInterval`, the original `roundInterval(accurateInterval);` gives completely wrong number of elements that can fit in the axis (much larger than actual value).
Also notice the `pixelPerInterval ` parameter, you should be able to indicate this some way depending on the length of final titles in the axis. Or it should be automatically calculated based on font size and labels on the axis.
From this:
```dart
double getEfficientInterval(
double axisViewSize,
double diffInAxis, {
double pixelPerInterval = 40,
}) {
final allowedCount = math.max(axisViewSize ~/ pixelPerInterval, 1);
if (diffInAxis == 0) {
return 1;
}
final accurateInterval =
diffInAxis == 0 ? axisViewSize : diffInAxis / allowedCount;
if (allowedCount <= 2) {
return accurateInterval;
}
return roundInterval(accurateInterval);
}
```
To this:
```dart
double getEfficientInterval(
double axisViewSize,
double diffInAxis, {
double pixelPerInterval = 125,
}) {
final allowedCount = math.max(axisViewSize ~/ pixelPerInterval, 1);
if (diffInAxis == 0) {
return 1;
}
final accurateInterval =
diffInAxis == 0 ? axisViewSize : diffInAxis / allowedCount;
return accurateInterval;
}
```
- axis_chart_helper.dart
Again I cannot fully understand all calculations so this works as long as `minIncluded` and `maxIncluded` are both `true`. This modification is needed to avoid the overlapping of the titles, specifically for first and last elements.
Function `Utils().getBestInitialIntervalValue()` doesn't work for this particular case, it always says elements don't overlap but they do all the time.
From this:
```dart
Iterable iterateThroughAxis({
required double min,
bool minIncluded = true,
required double max,
bool maxIncluded = true,
required double baseLine,
required double interval,
}) sync* {
final initialValue = Utils()
.getBestInitialIntervalValue(min, max, interval, baseline: baseLine);
var axisSeek = initialValue;
final firstPositionOverlapsWithMin = axisSeek == min;
if (!minIncluded && firstPositionOverlapsWithMin) {
// If initial value is equal to data minimum,
// move first label one interval further
axisSeek += interval;
}
final diff = max - min;
final count = diff ~/ interval;
final lastPosition = initialValue + (count * interval);
final lastPositionOverlapsWithMax = lastPosition == max;
final end =
!maxIncluded && lastPositionOverlapsWithMax ? max - interval : max;
final epsilon = interval / 100000;
if (minIncluded && !firstPositionOverlapsWithMin) {
// Data minimum shall be included and is not yet covered
yield min;
}
while (axisSeek <= end + epsilon) {
yield axisSeek;
axisSeek += interval;
}
if (maxIncluded && !lastPositionOverlapsWithMax) {
yield max;
}
}
```
To this:
```dart
Iterable iterateThroughAxis({
required double min,
bool minIncluded = true,
required double max,
bool maxIncluded = true,
required double baseLine,
required double interval,
}) sync* {
final initialValue = Utils()
.getBestInitialIntervalValue(min, max, interval, baseline: baseLine);
var axisSeek = initialValue;
final firstPositionOverlapsWithMin = axisSeek == min;
if (!minIncluded && firstPositionOverlapsWithMin) {
// If initial value is equal to data minimum,
// move first label one interval further
axisSeek += interval;
}
final diff = max - min;
final count = diff ~/ interval;
final lastPosition = initialValue + (count * interval);
final lastPositionOverlapsWithMax = lastPosition == max;
final end =
!maxIncluded && lastPositionOverlapsWithMax ? max - interval : max;
final epsilon = interval / 100000;
if (minIncluded) {
// Data minimum shall be included and is not yet covered
yield min;
}
axisSeek = min + interval;
while (axisSeek < end + epsilon) {
yield axisSeek;
axisSeek += interval;
}
if (maxIncluded) {
yield max;
}
}
```
Contributor guide
Research direction
Reproduce the overlapping x-axis dates with the LineChartSample1 example and the supplied timestamp data. Start with getEfficientInterval in utils.dart and iterateThroughAxis plus getBestInitialIntervalValue in axis_chart_helper.dart, comparing their interval and boundary-label behavior against the screenshots. Done means automatic intervals fit the axis without overlapping titles, including the first and last labels.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- dart, flutter
- Domain
- data-visualization
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100