From 335af231a6c9da283bf84c07935dfcd10bd0a96e Mon Sep 17 00:00:00 2001 From: imaNNeoFighT Date: Mon, 25 May 2020 01:35:14 +0430 Subject: [PATCH] Implemented auto calculate interval function, and used for titles, and grid lines, it prevents performance issues, like #101, #322. --- .../chart/bar_chart/bar_chart_painter.dart | 8 +- .../base/axis_chart/axis_chart_data.dart | 21 +++-- .../base/axis_chart/axis_chart_painter.dart | 23 +++--- .../base/base_chart/base_chart_data.dart | 5 +- .../chart/line_chart/line_chart_painter.dart | 16 +++- .../scatter_chart/scatter_chart_painter.dart | 16 +++- lib/src/utils/utils.dart | 77 +++++++++++++++++++ repo_files/documentations/base_chart.md | 6 +- test/utils/utils_test.dart | 28 +++++++ 9 files changed, 168 insertions(+), 32 deletions(-) diff --git a/lib/src/chart/bar_chart/bar_chart_painter.dart b/lib/src/chart/bar_chart/bar_chart_painter.dart index 7d9ae5096..928e6883b 100644 --- a/lib/src/chart/bar_chart/bar_chart_painter.dart +++ b/lib/src/chart/bar_chart/bar_chart_painter.dart @@ -284,6 +284,8 @@ class BarChartPainter extends AxisChartPainter with TouchHandler with TouchHandler with TouchHandler maxY - minY; + + /// Difference of [maxX] and [minX] + double get horizontalDiff => maxX - minX; + AxisChartData({ FlGridData gridData, FlAxisTitleData axisTitleData, @@ -243,8 +250,8 @@ class SideTitles with EquatableMixin { /// [textStyle] determines the text style of them, /// [margin] determines margin of texts from the border line, /// - /// by default, texts are showing with 1.0 interval, - /// you can change this value using [interval], + /// texts are showing with provided [interval], + /// or you can let it be null to be calculated using [getEfficientInterval], /// /// you can change rotation of drawing titles using [rotateAngle]. SideTitles({ @@ -264,7 +271,7 @@ class SideTitles with EquatableMixin { fontSize: 11, ), margin = margin ?? 6, - interval = interval ?? 1.0, + interval = interval, rotateAngle = rotateAngle ?? 0.0; /// Lerps a [SideTitles] based on [t] value, check [Tween.lerp]. @@ -352,7 +359,7 @@ class FlGridData with EquatableMixin { /// Determines showing or hiding all horizontal lines. final bool drawHorizontalLine; - /// Determines interval between horizontal lines. + /// Determines interval between horizontal lines, left it null to be auto calculated. final double horizontalInterval; /// Gives you a y value, and gets a [FlLine] that represents specified line. @@ -364,7 +371,7 @@ class FlGridData with EquatableMixin { /// Determines showing or hiding all vertical lines. final bool drawVerticalLine; - /// Determines interval between vertical lines. + /// Determines interval between vertical lines, left it null to be auto calculated. final double verticalInterval; /// Gives you a x value, and gets a [FlLine] that represents specified line. @@ -407,11 +414,11 @@ class FlGridData with EquatableMixin { CheckToShowGrid checkToShowVerticalLine, }) : show = show ?? true, drawHorizontalLine = drawHorizontalLine ?? true, - horizontalInterval = horizontalInterval ?? 1.0, + horizontalInterval = horizontalInterval, getDrawingHorizontalLine = getDrawingHorizontalLine ?? defaultGridLine, checkToShowHorizontalLine = checkToShowHorizontalLine ?? showAllGrids, drawVerticalLine = drawVerticalLine ?? false, - verticalInterval = verticalInterval ?? 1.0, + verticalInterval = verticalInterval, getDrawingVerticalLine = getDrawingVerticalLine ?? defaultGridLine, checkToShowVerticalLine = checkToShowVerticalLine ?? showAllGrids; diff --git a/lib/src/chart/base/axis_chart/axis_chart_painter.dart b/lib/src/chart/base/axis_chart/axis_chart_painter.dart index 722d7621a..11b882704 100644 --- a/lib/src/chart/base/axis_chart/axis_chart_painter.dart +++ b/lib/src/chart/base/axis_chart/axis_chart_painter.dart @@ -4,6 +4,7 @@ import 'package:fl_chart/src/chart/bar_chart/bar_chart_painter.dart'; import 'package:fl_chart/src/chart/base/base_chart/base_chart_painter.dart'; import 'package:fl_chart/src/chart/line_chart/line_chart_painter.dart'; import 'package:fl_chart/src/extensions/canvas_extension.dart'; +import 'package:fl_chart/src/utils/utils.dart'; import 'package:flutter/material.dart'; import 'axis_chart_data.dart'; @@ -201,14 +202,15 @@ abstract class AxisChartPainter extends BaseChartPainte final Size usableViewSize = getChartUsableDrawSize(viewSize); // Show Vertical Grid if (data.gridData.drawVerticalLine) { - double verticalSeek = data.minX + data.gridData.verticalInterval; + final int verticalInterval = data.gridData.verticalInterval ?? + getEfficientInterval(viewSize.width, data.horizontalDiff); + double verticalSeek = data.minX + verticalInterval; final double delta = data.maxX - data.minX; - final int count = delta ~/ data.gridData.verticalInterval; + final int count = delta ~/ verticalInterval; final double lastPosition = count * verticalSeek; final bool lastPositionOverlapsWithBorder = lastPosition == data.maxX; - final end = - lastPositionOverlapsWithBorder ? data.maxX - data.gridData.verticalInterval : data.maxX; + final end = lastPositionOverlapsWithBorder ? data.maxX - verticalInterval : data.maxX; while (verticalSeek <= end) { if (data.gridData.checkToShowVerticalLine(verticalSeek)) { @@ -223,21 +225,22 @@ abstract class AxisChartPainter extends BaseChartPainte final double y2 = usableViewSize.height + getTopOffsetDrawSize(); canvas.drawDashedLine(Offset(x1, y1), Offset(x2, y2), _gridPaint, flLineStyle.dashArray); } - verticalSeek += data.gridData.verticalInterval; + verticalSeek += verticalInterval; } } // Show Horizontal Grid if (data.gridData.drawHorizontalLine) { - double horizontalSeek = data.minY + data.gridData.horizontalInterval; + final int horizontalInterval = data.gridData.horizontalInterval ?? + getEfficientInterval(viewSize.height, data.verticalDiff); + double horizontalSeek = data.minY + horizontalInterval; final double delta = data.maxY - data.minY; - final int count = delta ~/ data.gridData.horizontalInterval; + final int count = delta ~/ horizontalInterval; final double lastPosition = count * horizontalSeek; final bool lastPositionOverlapsWithBorder = lastPosition == data.maxY; - final end = - lastPositionOverlapsWithBorder ? data.maxY - data.gridData.horizontalInterval : data.maxY; + final end = lastPositionOverlapsWithBorder ? data.maxY - horizontalInterval : data.maxY; while (horizontalSeek <= end) { if (data.gridData.checkToShowHorizontalLine(horizontalSeek)) { @@ -253,7 +256,7 @@ abstract class AxisChartPainter extends BaseChartPainte canvas.drawDashedLine(Offset(x1, y1), Offset(x2, y2), _gridPaint, flLine.dashArray); } - horizontalSeek += data.gridData.horizontalInterval; + horizontalSeek += horizontalInterval; } } } diff --git a/lib/src/chart/base/base_chart/base_chart_data.dart b/lib/src/chart/base/base_chart/base_chart_data.dart index 4b0f1ac03..afa5bcfcf 100644 --- a/lib/src/chart/base/base_chart/base_chart_data.dart +++ b/lib/src/chart/base/base_chart/base_chart_data.dart @@ -1,4 +1,5 @@ import 'package:equatable/equatable.dart'; +import 'package:fl_chart/src/utils/utils.dart'; import 'package:flutter/material.dart'; import 'base_chart_painter.dart'; @@ -127,9 +128,9 @@ typedef GetTitleFunction = String Function(double value); /// The default [SideTitles.getTitles] function. /// -/// It maps the axis number to a string and returns it. +/// formats the axis number to a shorter string using [formatNumber]. String defaultGetTitle(double value) { - return '$value'; + return formatNumber(value); } /// This class holds the touch response details. diff --git a/lib/src/chart/line_chart/line_chart_painter.dart b/lib/src/chart/line_chart/line_chart_painter.dart index 2fe16d179..848105cee 100644 --- a/lib/src/chart/line_chart/line_chart_painter.dart +++ b/lib/src/chart/line_chart/line_chart_painter.dart @@ -842,6 +842,8 @@ class LineChartPainter extends AxisChartPainter // Left Titles final leftTitles = targetData.titlesData.leftTitles; + final leftInterval = + leftTitles.interval ?? getEfficientInterval(viewSize.height, data.verticalDiff); if (leftTitles.showTitles) { double verticalSeek = data.minY; while (verticalSeek <= data.maxY) { @@ -867,12 +869,14 @@ class LineChartPainter extends AxisChartPainter tp.paint(canvas, Offset(x, y)); canvas.restore(); - verticalSeek += leftTitles.interval; + verticalSeek += leftInterval; } } // Top titles final topTitles = targetData.titlesData.topTitles; + final topInterval = + topTitles.interval ?? getEfficientInterval(viewSize.width, data.horizontalDiff); if (topTitles.showTitles) { double horizontalSeek = data.minX; while (horizontalSeek <= data.maxX) { @@ -899,12 +903,14 @@ class LineChartPainter extends AxisChartPainter tp.paint(canvas, Offset(x, y)); canvas.restore(); - horizontalSeek += topTitles.interval; + horizontalSeek += topInterval; } } // Right Titles final rightTitles = targetData.titlesData.rightTitles; + final rightInterval = + rightTitles.interval ?? getEfficientInterval(viewSize.height, data.verticalDiff); if (rightTitles.showTitles) { double verticalSeek = data.minY; while (verticalSeek <= data.maxY) { @@ -931,12 +937,14 @@ class LineChartPainter extends AxisChartPainter tp.paint(canvas, Offset(x, y)); canvas.restore(); - verticalSeek += rightTitles.interval; + verticalSeek += rightInterval; } } // Bottom titles final bottomTitles = targetData.titlesData.bottomTitles; + final bottomInterval = + bottomTitles.interval ?? getEfficientInterval(viewSize.width, data.horizontalDiff); if (bottomTitles.showTitles) { double horizontalSeek = data.minX; while (horizontalSeek <= data.maxX) { @@ -961,7 +969,7 @@ class LineChartPainter extends AxisChartPainter tp.paint(canvas, Offset(x, y)); canvas.restore(); - horizontalSeek += bottomTitles.interval; + horizontalSeek += bottomInterval; } } } diff --git a/lib/src/chart/scatter_chart/scatter_chart_painter.dart b/lib/src/chart/scatter_chart/scatter_chart_painter.dart index afbf7f87a..fc12daa23 100644 --- a/lib/src/chart/scatter_chart/scatter_chart_painter.dart +++ b/lib/src/chart/scatter_chart/scatter_chart_painter.dart @@ -65,6 +65,8 @@ class ScatterChartPainter extends AxisChartPainter // Left Titles final leftTitles = targetData.titlesData.leftTitles; + final leftInterval = + leftTitles.interval ?? getEfficientInterval(viewSize.height, data.verticalDiff); if (leftTitles.showTitles) { double verticalSeek = data.minY; while (verticalSeek <= data.maxY) { @@ -90,12 +92,14 @@ class ScatterChartPainter extends AxisChartPainter tp.paint(canvas, Offset(x, y)); canvas.restore(); - verticalSeek += leftTitles.interval; + verticalSeek += leftInterval; } } // Top titles final topTitles = targetData.titlesData.topTitles; + final topInterval = + topTitles.interval ?? getEfficientInterval(viewSize.width, data.horizontalDiff); if (topTitles.showTitles) { double horizontalSeek = data.minX; while (horizontalSeek <= data.maxX) { @@ -122,12 +126,14 @@ class ScatterChartPainter extends AxisChartPainter tp.paint(canvas, Offset(x, y)); canvas.restore(); - horizontalSeek += topTitles.interval; + horizontalSeek += topInterval; } } // Right Titles final rightTitles = targetData.titlesData.rightTitles; + final rightInterval = + rightTitles.interval ?? getEfficientInterval(viewSize.height, data.verticalDiff); if (rightTitles.showTitles) { double verticalSeek = data.minY; while (verticalSeek <= data.maxY) { @@ -154,12 +160,14 @@ class ScatterChartPainter extends AxisChartPainter tp.paint(canvas, Offset(x, y)); canvas.restore(); - verticalSeek += rightTitles.interval; + verticalSeek += rightInterval; } } // Bottom titles final bottomTitles = targetData.titlesData.bottomTitles; + final bottomInterval = + bottomTitles.interval ?? getEfficientInterval(viewSize.width, data.horizontalDiff); if (bottomTitles.showTitles) { double horizontalSeek = data.minX; while (horizontalSeek <= data.maxX) { @@ -186,7 +194,7 @@ class ScatterChartPainter extends AxisChartPainter tp.paint(canvas, Offset(x, y)); canvas.restore(); - horizontalSeek += bottomTitles.interval; + horizontalSeek += bottomInterval; } } } diff --git a/lib/src/utils/utils.dart b/lib/src/utils/utils.dart index a6019385e..e627055d9 100644 --- a/lib/src/utils/utils.dart +++ b/lib/src/utils/utils.dart @@ -1,4 +1,5 @@ import 'dart:math' as math; +import 'dart:math'; import 'package:flutter/material.dart'; @@ -101,3 +102,79 @@ Color lerpGradient(List colors, List stops, double t) { } return colors.last; } + +/// Returns an efficient interval for showing axis titles, or grid lines or ... +/// +/// If there isn't any provided interval, we use this function to calculate an interval to apply, +/// using [axisViewSize] / [pixelPerInterval], we calculate the allowedCount lines in the axis, +/// then using [diffInYAxis] / allowedCount, we can find out how much interval we need, +/// then we round that number by finding nearest number in this pattern: +/// 1, 2, 5, 10, 20, 50, 100, 200, 500, 1000, 5000, 10000,... +int getEfficientInterval(double axisViewSize, double diffInYAxis, {double pixelPerInterval = 10}) { + final int allowedCount = axisViewSize ~/ pixelPerInterval; + final double accurateInterval = diffInYAxis / allowedCount; + return _roundInterval(accurateInterval); +} + +int _roundInterval(double input) { + int count = 0; + + if (input >= 10) { + count++; + } + + while (input ~/ 100 != 0) { + input /= 10; + count++; + } + + final double scaled = input >= 10 ? input.round() / 10 : input; + + if (scaled >= 2.6) { + return 5 * pow(10, count); + } else if (scaled >= 1.6) { + return 2 * pow(10, count); + } else { + return 1 * pow(10, count); + } +} + +/// billion number +const double billion = 1000000000; + +/// million number +const double million = 1000000; + +/// kilo (thousands) number +const double kilo = 1000; + +/// Formats and add symbols (K, M, B) at the end of number. +/// +/// if number is larger than [billion], it returns a short number like 13.3B, +/// if number is larger than [million], it returns a short number line 43M, +/// if number is larger than [kilo], it returns a short number like 4K, +/// otherwise it returns number itself. +/// also it removes .0, at the end of number for simplicity. +String formatNumber(double number) { + String resultNumber; + String symbol; + if (number >= billion) { + resultNumber = (number / billion).toStringAsFixed(1); + symbol = 'B'; + } else if (number >= million) { + resultNumber = (number / million).toStringAsFixed(1); + symbol = 'M'; + } else if (number >= kilo) { + resultNumber = (number / kilo).toStringAsFixed(1); + symbol = 'K'; + } else { + resultNumber = number.toStringAsFixed(1); + symbol = ''; + } + + if (resultNumber.endsWith('.0')) { + resultNumber = resultNumber.replaceAll('.0', ''); + } + + return resultNumber + symbol; +} diff --git a/repo_files/documentations/base_chart.md b/repo_files/documentations/base_chart.md index 89443168b..e5070f44c 100644 --- a/repo_files/documentations/base_chart.md +++ b/repo_files/documentations/base_chart.md @@ -29,7 +29,7 @@ |reservedSize| a reserved space to show titles|22| |textStyle| [TextStyle](https://api.flutter.dev/flutter/painting/TextStyle-class.html) the style to use for title text |TextStyle(color: Colors.black, fontSize: 11)| |margin| margin between each title | 6| -|interval| interval to display each title on a side | 1.0 | +|interval| interval to display each title on a side, left it null to be calculate automatically | null | |rotateAngle| the clockwise angle of rotating title in degrees | 0.0 | @@ -48,11 +48,11 @@ currently we have these touch behaviors: |:-------|:----------|:------------| |show|determines to show or hide the background grid data|true| |drawHorizontalLine|determines to show or hide the horizontal grid lines|true| -|horizontalInterval|interval space of grid|1.0| +|horizontalInterval|interval space of grid, left it null to be calculate automatically |null| |getDrawingHorizontalLine|a function to get the line style of each grid line by giving the related axis value|defaultGridLine| |checkToShowHorizontalLine|a function to check whether to show or hide the horizontal grid by giving the related axis value |showAllGrids| |drawVerticalLine|determines to show or hide the vertical grid lines|false| -|verticalInterval|interval space of grid|1.0| +|verticalInterval|interval space of grid, left it null to be calculate automatically |null| |getDrawingVerticalLine|a function to get the line style of each grid line by giving the related axis value|defaultGridLine| |checkToShowVerticalLine|a function to determine whether to show or hide the vertical grid by giving the related axis value |showAllGrids| diff --git a/test/utils/utils_test.dart b/test/utils/utils_test.dart index 03b959fff..e5850993b 100644 --- a/test/utils/utils_test.dart +++ b/test/utils/utils_test.dart @@ -52,4 +52,32 @@ void main() { ], null, 1.0), Colors.green); }); + + test('test getEfficientInterval', () { + expect(getEfficientInterval(472, 340), 5); + expect(getEfficientInterval(820, 10000), 100); + expect(getEfficientInterval(1024, 412345234), 5000000); + expect(getEfficientInterval(720, 812394712349), 10000000000); + }); + + test('test formatNumber', () { + expect(formatNumber(0), '0'); + expect(formatNumber(423), '423'); + expect(formatNumber(1000), '1K'); + expect(formatNumber(1234), '1.2K'); + expect(formatNumber(10000), '10K'); + expect(formatNumber(41234), '41.2K'); + expect(formatNumber(82349), '82.3K'); + expect(formatNumber(82350), '82.3K'); + expect(formatNumber(82351), '82.4K'); + expect(formatNumber(100000), '100K'); + expect(formatNumber(101000), '101K'); + expect(formatNumber(2345123), '2.3M'); + expect(formatNumber(2352123), '2.4M'); + expect(formatNumber(521000000), '521M'); + expect(formatNumber(4324512345), '4.3B'); + expect(formatNumber(4000000000), '4B'); + expect(formatNumber(823147521343), '823.1B'); + expect(formatNumber(8231475213435), '8231.5B'); + }); }