diff --git a/lib/src/chart/scatter_chart/scatter_chart_data.dart b/lib/src/chart/scatter_chart/scatter_chart_data.dart index 55f0f4c10..b0ef7c237 100644 --- a/lib/src/chart/scatter_chart/scatter_chart_data.dart +++ b/lib/src/chart/scatter_chart/scatter_chart_data.dart @@ -6,6 +6,8 @@ import 'package:fl_chart/src/utils/lerp.dart'; import 'package:flutter/animation.dart'; import 'package:flutter/material.dart'; +import 'scatter_chart_helper.dart'; + /// [ScatterChart] needs this class to render itself. /// /// It holds data needed to draw a scatter chart, @@ -37,19 +39,19 @@ class ScatterChartData extends AxisChartData with EquatableMixin { /// /// [clipData] forces the [LineChart] to draw lines inside the chart bounding box. ScatterChartData({ - List scatterSpots, - FlTitlesData titlesData, - ScatterTouchData scatterTouchData, - List showingTooltipIndicators, - FlGridData gridData, - FlBorderData borderData, - FlAxisTitleData axisTitleData, - double minX, - double maxX, - double minY, - double maxY, - FlClipData clipData, - Color backgroundColor, + List? scatterSpots, + FlTitlesData? titlesData, + ScatterTouchData? scatterTouchData, + List? showingTooltipIndicators, + FlGridData? gridData, + FlBorderData? borderData, + FlAxisTitleData? axisTitleData, + double? minX, + double? maxX, + double? minY, + double? maxY, + FlClipData? clipData, + Color? backgroundColor, }) : scatterSpots = scatterSpots ?? const [], titlesData = titlesData ?? FlTitlesData(), scatterTouchData = scatterTouchData ?? ScatterTouchData(), @@ -61,69 +63,18 @@ class ScatterChartData extends AxisChartData with EquatableMixin { axisTitleData: axisTitleData ?? FlAxisTitleData(), clipData: clipData ?? FlClipData.none(), backgroundColor: backgroundColor, - ) { - initSuperMinMaxValues(minX, maxX, minY, maxY); - } + minX: minX ?? ScatterChartHelper.calculateMaxAxisValues(scatterSpots ?? const []).minX, + maxX: maxX ?? ScatterChartHelper.calculateMaxAxisValues(scatterSpots ?? const []).maxX, + minY: minY ?? ScatterChartHelper.calculateMaxAxisValues(scatterSpots ?? const []).minY, + maxY: maxY ?? ScatterChartHelper.calculateMaxAxisValues(scatterSpots ?? const []).maxY, + ); - /// fills [minX], [maxX], [minY], [maxY] if they are null, - /// based on the provided [scatterSpots]. - void initSuperMinMaxValues( - double minX, - double maxX, - double minY, - double maxY, - ) { - if (scatterSpots.isNotEmpty) { - final canModifyMinX = minX == null; - if (canModifyMinX) { - minX = scatterSpots[0].x; - } - - final canModifyMaxX = maxX == null; - if (canModifyMaxX) { - maxX = scatterSpots[0].x; - } - - final canModifyMinY = minY == null; - if (canModifyMinY) { - minY = scatterSpots[0].y; - } - - final canModifyMaxY = maxY == null; - if (canModifyMaxY) { - maxY = scatterSpots[0].y; - } - - for (var j = 0; j < scatterSpots.length; j++) { - final spot = scatterSpots[j]; - if (canModifyMaxX && spot.x > maxX) { - maxX = spot.x; - } - - if (canModifyMinX && spot.x < minX) { - minX = spot.x; - } - - if (canModifyMaxY && spot.y > maxY) { - maxY = spot.y; - } - - if (canModifyMinY && spot.y < minY) { - minY = spot.y; - } - } - } - super.minX = minX ?? 0; - super.maxX = maxX ?? 1; - super.minY = minY ?? 0; - super.maxY = maxY ?? 1; - } /// Lerps a [ScatterChartData] based on [t] value, check [Tween.lerp]. @override ScatterChartData lerp(BaseChartData a, BaseChartData b, double t) { - if (a is ScatterChartData && b is ScatterChartData && t != null) { + if (a is ScatterChartData && b is ScatterChartData) { return ScatterChartData( scatterSpots: lerpScatterSpotList(a.scatterSpots, b.scatterSpots, t), titlesData: FlTitlesData.lerp(a.titlesData, b.titlesData, t), @@ -148,19 +99,19 @@ class ScatterChartData extends AxisChartData with EquatableMixin { /// Copies current [ScatterChartData] to a new [ScatterChartData], /// and replaces provided values. ScatterChartData copyWith({ - List scatterSpots, - FlTitlesData titlesData, - ScatterTouchData scatterTouchData, - List showingTooltipIndicators, - FlGridData gridData, - FlBorderData borderData, - FlAxisTitleData axisTitleData, - double minX, - double maxX, - double minY, - double maxY, - FlClipData clipData, - Color backgroundColor, + List? scatterSpots, + FlTitlesData? titlesData, + ScatterTouchData? scatterTouchData, + List? showingTooltipIndicators, + FlGridData? gridData, + FlBorderData? borderData, + FlAxisTitleData? axisTitleData, + double? minX, + double? maxX, + double? minY, + double? maxY, + FlClipData? clipData, + Color? backgroundColor, }) { return ScatterChartData( scatterSpots: scatterSpots ?? this.scatterSpots, @@ -181,7 +132,7 @@ class ScatterChartData extends AxisChartData with EquatableMixin { /// Used for equality check, see [EquatableMixin]. @override - List get props => [ + List get props => [ scatterSpots, titlesData, scatterTouchData, @@ -217,9 +168,9 @@ class ScatterSpot extends FlSpot with EquatableMixin { ScatterSpot( double x, double y, { - bool show, - double radius, - Color color, + bool? show, + double? radius, + Color? color, }) : show = show ?? true, radius = radius ?? 6, color = color ?? Colors.primaries[((x * y) % Colors.primaries.length).toInt()], @@ -227,11 +178,11 @@ class ScatterSpot extends FlSpot with EquatableMixin { @override ScatterSpot copyWith({ - double x, - double y, - bool show, - double radius, - Color color, + double? x, + double? y, + bool? show, + double? radius, + Color? color, }) { return ScatterSpot( x ?? this.x, @@ -245,8 +196,8 @@ class ScatterSpot extends FlSpot with EquatableMixin { /// Lerps a [ScatterSpot] based on [t] value, check [Tween.lerp]. static ScatterSpot lerp(ScatterSpot a, ScatterSpot b, double t) { return ScatterSpot( - lerpDouble(a.x, b.x, t), - lerpDouble(a.y, b.y, t), + lerpDouble(a.x, b.x, t)!, + lerpDouble(a.y, b.y, t)!, show: b.show, radius: lerpDouble(a.radius, b.radius, t), color: Color.lerp(a.color, b.color, t), @@ -255,7 +206,7 @@ class ScatterSpot extends FlSpot with EquatableMixin { /// Used for equality check, see [EquatableMixin]. @override - List get props => [ + List get props => [ x, y, show, @@ -281,7 +232,7 @@ class ScatterTouchData extends FlTouchData with EquatableMixin { final bool handleBuiltInTouches; /// you can implement it to receive touches callback - final Function(ScatterTouchResponse) touchCallback; + final Function(ScatterTouchResponse)? touchCallback; /// You can disable or enable the touch system using [enabled] flag, /// if [handleBuiltInTouches] is true, [ScatterChart] shows a tooltip popup on top of the spots if @@ -294,11 +245,11 @@ class ScatterTouchData extends FlTouchData with EquatableMixin { /// It gives you a [ScatterTouchResponse] that contains some /// useful information about happened touch. ScatterTouchData({ - bool enabled, - ScatterTouchTooltipData touchTooltipData, - double touchSpotThreshold, - bool handleBuiltInTouches, - Function(ScatterTouchResponse) touchCallback, + bool? enabled, + ScatterTouchTooltipData? touchTooltipData, + double? touchSpotThreshold, + bool? handleBuiltInTouches, + Function(ScatterTouchResponse)? touchCallback, }) : touchTooltipData = touchTooltipData ?? ScatterTouchTooltipData(), touchSpotThreshold = touchSpotThreshold ?? 10, handleBuiltInTouches = handleBuiltInTouches ?? true, @@ -308,11 +259,11 @@ class ScatterTouchData extends FlTouchData with EquatableMixin { /// Copies current [ScatterTouchData] to a new [ScatterTouchData], /// and replaces provided values. ScatterTouchData copyWith({ - bool enabled, - LineTouchTooltipData touchTooltipData, - double touchSpotThreshold, - bool handleBuiltInTouches, - Function(ScatterTouchResponse) touchCallback, + bool? enabled, + ScatterTouchTooltipData? touchTooltipData, + double? touchSpotThreshold, + bool? handleBuiltInTouches, + Function(ScatterTouchResponse)? touchCallback, }) { return ScatterTouchData( enabled: enabled ?? this.enabled, @@ -325,7 +276,7 @@ class ScatterTouchData extends FlTouchData with EquatableMixin { /// Used for equality check, see [EquatableMixin]. @override - List get props => [ + List get props => [ enabled, touchTooltipData, touchSpotThreshold, @@ -358,7 +309,7 @@ class ScatterTouchResponse extends BaseTouchResponse with EquatableMixin { /// Used for equality check, see [EquatableMixin]. @override - List get props => [ + List get props => [ touchInput, touchedSpot, touchedSpotIndex, @@ -401,13 +352,13 @@ class ScatterTouchTooltipData with EquatableMixin { /// you can set [fitInsideHorizontally] true to force it to shift inside the chart horizontally, /// also you can set [fitInsideVertically] true to force it to shift inside the chart vertically. ScatterTouchTooltipData({ - Color tooltipBgColor, - double tooltipRoundedRadius, - EdgeInsets tooltipPadding, - double maxContentWidth, - GetScatterTooltipItems getTooltipItems, - bool fitInsideHorizontally, - bool fitInsideVertically, + Color? tooltipBgColor, + double? tooltipRoundedRadius, + EdgeInsets? tooltipPadding, + double? maxContentWidth, + GetScatterTooltipItems? getTooltipItems, + bool? fitInsideHorizontally, + bool? fitInsideVertically, }) : tooltipBgColor = tooltipBgColor ?? Colors.white, tooltipRoundedRadius = tooltipRoundedRadius ?? 4, tooltipPadding = tooltipPadding ?? const EdgeInsets.symmetric(horizontal: 16, vertical: 8), @@ -419,7 +370,7 @@ class ScatterTouchTooltipData with EquatableMixin { /// Used for equality check, see [EquatableMixin]. @override - List get props => [ + List get props => [ tooltipBgColor, tooltipRoundedRadius, tooltipPadding, @@ -440,9 +391,6 @@ typedef GetScatterTooltipItems = ScatterTooltipItem Function(ScatterSpot touched /// Default implementation for [ScatterTouchTooltipData.getTooltipItems]. ScatterTooltipItem defaultScatterTooltipItem(ScatterSpot touchedSpot) { - if (touchedSpot == null) { - return null; - } final textStyle = TextStyle( color: touchedSpot.color, fontWeight: FontWeight.bold, @@ -475,7 +423,7 @@ class ScatterTooltipItem with EquatableMixin { /// Used for equality check, see [EquatableMixin]. @override - List get props => [ + List get props => [ text, textStyle, bottomMargin, @@ -484,12 +432,14 @@ class ScatterTooltipItem with EquatableMixin { /// It lerps a [ScatterChartData] to another [ScatterChartData] (handles animation for updating values) class ScatterChartDataTween extends Tween { - ScatterChartDataTween({ScatterChartData begin, ScatterChartData end}) - : super(begin: begin, end: end); + ScatterChartDataTween({ + required ScatterChartData begin, + required ScatterChartData end + }) : super(begin: begin, end: end); /// Lerps a [ScatterChartData] based on [t] value, check [Tween.lerp]. @override ScatterChartData lerp(double t) { - return begin.lerp(begin, end, t); + return begin!.lerp(begin!, end!, t); } } diff --git a/lib/src/chart/scatter_chart/scatter_chart_helper.dart b/lib/src/chart/scatter_chart/scatter_chart_helper.dart new file mode 100644 index 000000000..6e6da5838 --- /dev/null +++ b/lib/src/chart/scatter_chart/scatter_chart_helper.dart @@ -0,0 +1,93 @@ +import 'package:equatable/equatable.dart'; + +import 'scatter_chart_data.dart'; + +/// Contains anything that helps ScatterChart works +class ScatterChartHelper { + + /// Contains List of cached results, base on [List] + /// + /// We use it to prevent redundant calculations + static final Map, ScatterChartMinMaxAxisValues> _cachedResults = {}; + + /// Calculates minX, maxX, minY, and maxY based on [scatterSpots], + /// returns cached values, to prevent redundant calculations. + static ScatterChartMinMaxAxisValues calculateMaxAxisValues(List scatterSpots) { + if (scatterSpots.isEmpty) { + return ScatterChartMinMaxAxisValues(0, 0, 0, 0); + } + if (_cachedResults.containsKey(scatterSpots)) { + return _cachedResults[scatterSpots]!.copyWith(readFromCache: true); + } + + var minX = scatterSpots[0].x; + var maxX = scatterSpots[0].x; + var minY = scatterSpots[0].y; + var maxY = scatterSpots[0].y; + for (var j = 0; j < scatterSpots.length; j++) { + final spot = scatterSpots[j]; + if (spot.x > maxX) { + maxX = spot.x; + } + + if (spot.x < minX) { + minX = spot.x; + } + + if (spot.y > maxY) { + maxY = spot.y; + } + + if (spot.y < minY) { + minY = spot.y; + } + } + + final result = ScatterChartMinMaxAxisValues(minX, maxX, minY, maxY); + _cachedResults[scatterSpots] = result; + return result; + } + +} + +/// Holds minX, maxX, minY, and maxY for use in [ScatterChartData] +class ScatterChartMinMaxAxisValues with EquatableMixin { + final double minX; + final double maxX; + final double minY; + final double maxY; + final bool readFromCache; + + ScatterChartMinMaxAxisValues( + this.minX, + this.maxX, + this.minY, + this.maxY, { + this.readFromCache = false, + }); + + @override + List get props => [ + minX, + maxX, + minY, + maxY, + readFromCache + ]; + + ScatterChartMinMaxAxisValues copyWith({ + double? minX, + double? maxX, + double? minY, + double? maxY, + bool? readFromCache + }) { + return ScatterChartMinMaxAxisValues( + minX ?? this.minX, + maxX ?? this.maxX, + minY ?? this.minY, + maxY ?? this.maxY, + readFromCache: readFromCache ?? this.readFromCache, + ); + } +} \ No newline at end of file diff --git a/test/chart/data_pool.dart b/test/chart/data_pool.dart index 75a2ea167..79b3ec9b5 100644 --- a/test/chart/data_pool.dart +++ b/test/chart/data_pool.dart @@ -2018,6 +2018,13 @@ final Function(ScatterSpot touchedSpots) scatterChartGetTooltipItems = (list) { return ScatterTooltipItem('check', const TextStyle(color: Colors.blue), 23); }; +final ScatterSpot scatterSpot1 = ScatterSpot(1, 40); +final ScatterSpot scatterSpot1Clone = ScatterSpot(1, 40); +final ScatterSpot scatterSpot2 = ScatterSpot(-4, -8); +final ScatterSpot scatterSpot2Clone = scatterSpot2.copyWith(); +final ScatterSpot scatterSpot3 = ScatterSpot(-14, 5); +final ScatterSpot scatterSpot4 = ScatterSpot(-0, 0); + final ScatterChartData scatterChartData1 = ScatterChartData( minY: 0, maxY: 12, diff --git a/test/chart/scatter_chart/scatter_chart_helper_test.dart b/test/chart/scatter_chart/scatter_chart_helper_test.dart new file mode 100644 index 000000000..d09325e38 --- /dev/null +++ b/test/chart/scatter_chart/scatter_chart_helper_test.dart @@ -0,0 +1,52 @@ +import '../data_pool.dart'; +import 'package:fl_chart/fl_chart.dart'; +import 'package:fl_chart/src/chart/scatter_chart/scatter_chart_helper.dart'; + + +void main() { + group('Check caching of ScatterChartHelper.calculateMaxAxisValues', () { + + // final ScatterSpot scatterSpot1 = ScatterSpot(1, 40); + // final ScatterSpot scatterSpot2 = ScatterSpot(-4, -8); + // final ScatterSpot scatterSpot3 = ScatterSpot(-14, 5); + // final ScatterSpot scatterSpot4 = ScatterSpot(-0, 0); + + test('Test read from cache', () { + final scatterSpots = [scatterSpot1, scatterSpot2, scatterSpot3] + final scatterSpotsClone = [scatterSpot1Clone, scatterSpot2Clone, scatterSpot3] + final result1 = ScatterChartHelper.calculateMaxAxisValues(scatterSpots) + final result2 = ScatterChartHelper.calculateMaxAxisValues(scatterSpotsClone) + expect(result1.readFromCache == false) + expect(result2.readFromCache == true) + }) + + test('Test validity 1', () { + final scatterSpots = [scatterSpot1, scatterSpot2, scatterSpot3, scatterSpot4] + final result = ScatterChartHelper.calculateMaxAxisValues(scatterSpots) + expect(result.minX == -14); + expect(result.maxX == 1); + expect(result.minY == -8); + expect(result.maxY == 40); + }) + + test('Test validity 2', () { + final scatterSpots = [ + ScatterSpot(3, -1), + ScatterSpot(-1, 3), + ] + final result = ScatterChartHelper.calculateMaxAxisValues(scatterSpots) + expect(result.minX == -1); + expect(result.maxX == 3); + expect(result.minY == -1); + expect(result.maxY == 3); + }) + + test('Test equality', () { + final scatterSpots = [scatterSpot1, scatterSpot2, scatterSpot3] + final scatterSpotsClone = [scatterSpot1Clone, scatterSpot2Clone, scatterSpot3] + final result1 = ScatterChartHelper.calculateMaxAxisValues(scatterSpots) + final result2 = ScatterChartHelper.calculateMaxAxisValues(scatterSpotsClone) + expect(result1 == result2) + }); + }) +} \ No newline at end of file