From 0737de3a1290f0e3a07528d3c2b1ad9250bf96ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Mandi=C4=87?= Date: Sat, 14 Jan 2023 23:49:12 +0100 Subject: [PATCH 01/18] Add calculation based on number of items --- lib/chart/model/theme/chart_behaviour.dart | 15 +++++-- lib/chart/widgets/chart_widget.dart | 51 +++++++++++++++------- 2 files changed, 47 insertions(+), 19 deletions(-) diff --git a/lib/chart/model/theme/chart_behaviour.dart b/lib/chart/model/theme/chart_behaviour.dart index 22b480e..1df3abf 100644 --- a/lib/chart/model/theme/chart_behaviour.dart +++ b/lib/chart/model/theme/chart_behaviour.dart @@ -12,13 +12,18 @@ class ChartBehaviour { /// If chart is scrollable then it will ignore width limit and it should be wrapped in [SingleChildScrollView] const ChartBehaviour({ bool isScrollable = false, + int? visibleItems, this.onItemClicked, - }) : _isScrollable = isScrollable ? 1.0 : 0.0; + }) : _isScrollable = isScrollable ? 1.0 : 0.0, + _visibleItems = visibleItems; - ChartBehaviour._lerp(this._isScrollable, this.onItemClicked); + ChartBehaviour._lerp( + this._isScrollable, this._visibleItems, this.onItemClicked); final double _isScrollable; + final int? _visibleItems; + /// Return index of item clicked. Since graph can be multi value, user /// will have to handle clicked index to show data they want to show final ValueChanged? onItemClicked; @@ -30,11 +35,13 @@ class ChartBehaviour { static ChartBehaviour lerp(ChartBehaviour a, ChartBehaviour b, double t) { // This values should never return null, this is for null-safety // But if it somehow does occur, then revert to default values - final _scrollableLerp = + final scrollableLerp = lerpDouble(a._isScrollable, b._isScrollable, t) ?? 0.0; + final visibleLerp = lerpDouble(a._visibleItems, b._visibleItems, t); return ChartBehaviour._lerp( - _scrollableLerp, + scrollableLerp, + visibleLerp?.toInt(), t > 0.5 ? b.onItemClicked : a.onItemClicked, ); } diff --git a/lib/chart/widgets/chart_widget.dart b/lib/chart/widgets/chart_widget.dart index 45e1443..b854f7f 100644 --- a/lib/chart/widgets/chart_widget.dart +++ b/lib/chart/widgets/chart_widget.dart @@ -16,34 +16,55 @@ class _ChartWidget extends StatelessWidget { final double? width; final ChartState state; + double _horizontalPadding() { + return state.data.items.foldIndexed(0.0, + (index, double prevValue, _) { + return max(prevValue, state.itemOptions.padding.horizontal); + }); + } + + double _wantedItemWidthNormal() { + return state.data.items.foldIndexed(0.0, + (index, double prevValue, _) { + return max( + prevValue, + max( + state.itemOptions.minBarWidth ?? 0.0, + state.itemOptions.maxBarWidth ?? 0.0, + ), + ); + }); + } + + double _wantedItemWidthForSetVisibleItems(double width) { + final visibleItems = state.behaviour._visibleItems; + if (visibleItems == null) { + return _wantedItemWidthNormal(); + } + + final wantedItemWidth = width / visibleItems - _horizontalPadding(); + return max(_wantedItemWidthNormal(), wantedItemWidth); + } + @override Widget build(BuildContext context) { return LayoutBuilder( builder: (context, constraints) { - // What size does the item want to be? - final _wantedItemWidth = state.data.items.foldIndexed(0.0, - (index, double prevValue, _) { - return max( - prevValue, - max(state.itemOptions.minBarWidth ?? 0.0, - state.itemOptions.maxBarWidth ?? 0.0)); - }); - final _width = constraints.maxWidth.isFinite ? constraints.maxWidth : width!; final _height = constraints.maxHeight.isFinite ? constraints.maxHeight : height!; - final _listSize = state.data.listSize; + // What size does the item want to be? + final _wantedItemWidth = state.behaviour.isScrollable + ? _wantedItemWidthForSetVisibleItems(_width) + : _wantedItemWidthNormal(); - final _horizontalPadding = state.data.items.foldIndexed(0.0, - (index, double prevValue, _) { - return max(prevValue, state.itemOptions.padding.horizontal); - }); + final _listSize = state.data.listSize; final _size = Size( _width + - (((_wantedItemWidth + _horizontalPadding) * _listSize) - + (((_wantedItemWidth + _horizontalPadding()) * _listSize) - _width) * state.behaviour._isScrollable, _height); From 272134c77a93d6fcaae3e319ba43415b33cf64cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Mandi=C4=87?= Date: Sun, 15 Jan 2023 00:13:05 +0100 Subject: [PATCH 02/18] Use double for visibleItems --- lib/chart/model/theme/chart_behaviour.dart | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/chart/model/theme/chart_behaviour.dart b/lib/chart/model/theme/chart_behaviour.dart index 1df3abf..ccda335 100644 --- a/lib/chart/model/theme/chart_behaviour.dart +++ b/lib/chart/model/theme/chart_behaviour.dart @@ -4,17 +4,19 @@ part of charts_painter; /// [isScrollable] - If chart is scrollable then width of canvas is ignored and /// chart will take any size it needs. Chart has to be wrapped with [SingleChildScrollView] /// or similar scrollable widget. -/// [multiValueStack] - Defaults to true, Dictates how items in stack will be shown, if set to true items will stack on -/// each other, on false they will be side by side. /// [onItemClicked] - Returns index of clicked item. class ChartBehaviour { /// Default constructor for ChartBehaviour /// If chart is scrollable then it will ignore width limit and it should be wrapped in [SingleChildScrollView] const ChartBehaviour({ bool isScrollable = false, - int? visibleItems, + double? visibleItems, this.onItemClicked, - }) : _isScrollable = isScrollable ? 1.0 : 0.0, + }) : assert(visibleItems == null || isScrollable, + 'visibleItems are only used when chart is scrollable'), + assert(visibleItems == null || visibleItems > 0, + 'visibleItems must be greater than 0'), + _isScrollable = 1.0, _visibleItems = visibleItems; ChartBehaviour._lerp( @@ -22,7 +24,7 @@ class ChartBehaviour { final double _isScrollable; - final int? _visibleItems; + final double? _visibleItems; /// Return index of item clicked. Since graph can be multi value, user /// will have to handle clicked index to show data they want to show @@ -41,7 +43,7 @@ class ChartBehaviour { return ChartBehaviour._lerp( scrollableLerp, - visibleLerp?.toInt(), + visibleLerp, t > 0.5 ? b.onItemClicked : a.onItemClicked, ); } From a321a694d3323aeaae9d230ac1762582756096b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Mandi=C4=87?= Date: Sun, 15 Jan 2023 15:41:29 +0100 Subject: [PATCH 03/18] Refactor for readability --- lib/chart/model/theme/chart_behaviour.dart | 11 +++-- .../item_theme/bar/bar_item_options.dart | 5 +++ .../bubble/bubble_item_options.dart | 5 +++ .../model/theme/item_theme/item_options.dart | 20 +++++++-- .../widget/widget_item_options.dart | 18 +++++--- lib/chart/widgets/chart_widget.dart | 41 +++++++++++-------- 6 files changed, 68 insertions(+), 32 deletions(-) diff --git a/lib/chart/model/theme/chart_behaviour.dart b/lib/chart/model/theme/chart_behaviour.dart index ccda335..513302f 100644 --- a/lib/chart/model/theme/chart_behaviour.dart +++ b/lib/chart/model/theme/chart_behaviour.dart @@ -10,21 +10,20 @@ class ChartBehaviour { /// If chart is scrollable then it will ignore width limit and it should be wrapped in [SingleChildScrollView] const ChartBehaviour({ bool isScrollable = false, - double? visibleItems, + this.visibleItems, this.onItemClicked, }) : assert(visibleItems == null || isScrollable, 'visibleItems are only used when chart is scrollable'), assert(visibleItems == null || visibleItems > 0, 'visibleItems must be greater than 0'), - _isScrollable = 1.0, - _visibleItems = visibleItems; + _isScrollable = isScrollable ? 1.0 : 0.0; ChartBehaviour._lerp( - this._isScrollable, this._visibleItems, this.onItemClicked); + this._isScrollable, this.visibleItems, this.onItemClicked); final double _isScrollable; - final double? _visibleItems; + final double? visibleItems; /// Return index of item clicked. Since graph can be multi value, user /// will have to handle clicked index to show data they want to show @@ -39,7 +38,7 @@ class ChartBehaviour { // But if it somehow does occur, then revert to default values final scrollableLerp = lerpDouble(a._isScrollable, b._isScrollable, t) ?? 0.0; - final visibleLerp = lerpDouble(a._visibleItems, b._visibleItems, t); + final visibleLerp = lerpDouble(a.visibleItems, b.visibleItems, t); return ChartBehaviour._lerp( scrollableLerp, diff --git a/lib/chart/model/theme/item_theme/bar/bar_item_options.dart b/lib/chart/model/theme/item_theme/bar/bar_item_options.dart index 5f63327..37835fe 100644 --- a/lib/chart/model/theme/item_theme/bar/bar_item_options.dart +++ b/lib/chart/model/theme/item_theme/bar/bar_item_options.dart @@ -19,6 +19,7 @@ class BarItemOptions extends ItemOptions { const BarItemOptions({ EdgeInsets padding = EdgeInsets.zero, EdgeInsets multiValuePadding = EdgeInsets.zero, + ItemWidthCalculator? widthCalculator, double? maxBarWidth, double? minBarWidth, double startPosition = 0.5, @@ -29,6 +30,7 @@ class BarItemOptions extends ItemOptions { maxBarWidth: maxBarWidth, minBarWidth: minBarWidth, startPosition: startPosition, + widthCalculator: widthCalculator, geometryPainter: barPainter, itemBuilder: barItemBuilder); @@ -38,12 +40,14 @@ class BarItemOptions extends ItemOptions { double? maxBarWidth, double? minBarWidth, double startPosition = 0.5, + ItemWidthCalculator? widthCalculator, required this.barItemBuilder}) : super._lerp( padding: padding, multiValuePadding: multiValuePadding, maxBarWidth: maxBarWidth, minBarWidth: minBarWidth, + widthCalculator: widthCalculator, startPosition: startPosition, geometryPainter: barPainter, itemBuilder: barItemBuilder); @@ -64,6 +68,7 @@ class BarItemOptions extends ItemOptions { minBarWidth: lerpDouble(minBarWidth, endValue.minBarWidth, t), startPosition: lerpDouble(startPosition, endValue.startPosition, t) ?? 0.5, + widthCalculator: super.widthCalculator, ); } else { return endValue; diff --git a/lib/chart/model/theme/item_theme/bubble/bubble_item_options.dart b/lib/chart/model/theme/item_theme/bubble/bubble_item_options.dart index 22e0d5f..62fc8c5 100644 --- a/lib/chart/model/theme/item_theme/bubble/bubble_item_options.dart +++ b/lib/chart/model/theme/item_theme/bubble/bubble_item_options.dart @@ -19,6 +19,7 @@ class BubbleItemOptions extends ItemOptions { BubbleItemOptions({ EdgeInsets padding = EdgeInsets.zero, EdgeInsets multiValuePadding = EdgeInsets.zero, + ItemWidthCalculator? widthCalculator, double? maxBarWidth, double? minBarWidth, this.bubbleItemBuilder = _defaultBubbleItem, @@ -28,6 +29,7 @@ class BubbleItemOptions extends ItemOptions { minBarWidth: minBarWidth, maxBarWidth: maxBarWidth, geometryPainter: bubblePainter, + widthCalculator: widthCalculator, itemBuilder: bubbleItemBuilder); BubbleItemOptions._lerp({ @@ -35,6 +37,7 @@ class BubbleItemOptions extends ItemOptions { EdgeInsets multiValuePadding = EdgeInsets.zero, double? maxBarWidth, double? minBarWidth, + ItemWidthCalculator? widthCalculator, required this.bubbleItemBuilder, }) : super._lerp( padding: padding, @@ -43,6 +46,7 @@ class BubbleItemOptions extends ItemOptions { maxBarWidth: maxBarWidth, geometryPainter: bubblePainter, itemBuilder: bubbleItemBuilder, + widthCalculator: widthCalculator, ); final BubbleItemBuilder bubbleItemBuilder; @@ -59,6 +63,7 @@ class BubbleItemOptions extends ItemOptions { EdgeInsets.zero, maxBarWidth: lerpDouble(maxBarWidth, endValue.maxBarWidth, t), minBarWidth: lerpDouble(minBarWidth, endValue.minBarWidth, t), + widthCalculator: widthCalculator, ); } else { return endValue; diff --git a/lib/chart/model/theme/item_theme/item_options.dart b/lib/chart/model/theme/item_theme/item_options.dart index d206d09..fd8a800 100644 --- a/lib/chart/model/theme/item_theme/item_options.dart +++ b/lib/chart/model/theme/item_theme/item_options.dart @@ -8,6 +8,9 @@ typedef ChartGeometryPainter = GeometryPainter Function( ItemOptions itemOptions, DrawDataItem drawDataItem); +typedef ItemWidthCalculator = double Function( + double visibleItems, double calculatedWidth); + /// Options for chart items. You can use this subclasses: [BarItemOptions], [BubbleItemOptions], [WidgetItemOptions] /// /// Required [itemBuilder] parameter is used to provide a data for each item on the chart. @@ -28,8 +31,9 @@ abstract class ItemOptions { this.maxBarWidth, this.minBarWidth, this.startPosition = 0.5, + ItemWidthCalculator? widthCalculator, required this.itemBuilder, - }); + }) : widthCalculator = widthCalculator ?? _defaultWidthCalculator; const ItemOptions._lerp({ required this.geometryPainter, @@ -40,7 +44,11 @@ abstract class ItemOptions { this.startPosition = 0.5, double multiItemStack = 1.0, required this.itemBuilder, - }); + ItemWidthCalculator? widthCalculator, + }) : assert(maxBarWidth == null || + minBarWidth == null || + maxBarWidth >= minBarWidth), + widthCalculator = widthCalculator ?? _defaultWidthCalculator; /// Item padding, if [minBarWidth] and [padding] are more then available space /// [padding] will get ignored @@ -53,7 +61,9 @@ abstract class ItemOptions { final ItemBuilder itemBuilder; - /// Define color for value, this allows different colors for different values + /// Called to specify the desired width of the item + /// in case [visibleItems] in [ChartBehaviour] is not null. + final ItemWidthCalculator widthCalculator; /// Max width of item in the chart final double? maxBarWidth; @@ -78,3 +88,7 @@ abstract class ItemOptions { /// with all available options, otherwise changes in options won't be animated ItemOptions animateTo(ItemOptions endValue, double t); } + +double _defaultWidthCalculator(double visibleItems, double calculatedWidth) { + return calculatedWidth; +} diff --git a/lib/chart/model/theme/item_theme/widget/widget_item_options.dart b/lib/chart/model/theme/item_theme/widget/widget_item_options.dart index 2bf2a56..b5a1a88 100644 --- a/lib/chart/model/theme/item_theme/widget/widget_item_options.dart +++ b/lib/chart/model/theme/item_theme/widget/widget_item_options.dart @@ -42,21 +42,25 @@ class WidgetItemOptions extends ItemOptions { /// Constructor for bar item options, has some extra options just for [BarGeometryPainter] const WidgetItemOptions({ required this.widgetItemBuilder, + ItemWidthCalculator? widthCalculator, EdgeInsets multiValuePadding = EdgeInsets.zero, }) : super( padding: EdgeInsets.zero, multiValuePadding: multiValuePadding, geometryPainter: _emptyPainter, + widthCalculator: widthCalculator, itemBuilder: widgetItemBuilder, ); const WidgetItemOptions._lerp({ required this.widgetItemBuilder, + ItemWidthCalculator? widthCalculator, EdgeInsets multiValuePadding = EdgeInsets.zero, }) : super._lerp( multiValuePadding: multiValuePadding, geometryPainter: _emptyPainter, itemBuilder: widgetItemBuilder, + widthCalculator: widthCalculator, ); final WidgetItemBuilder widgetItemBuilder; @@ -64,11 +68,13 @@ class WidgetItemOptions extends ItemOptions { @override ItemOptions animateTo(ItemOptions endValue, double t) { return WidgetItemOptions._lerp( - widgetItemBuilder: endValue is WidgetItemOptions - ? endValue.widgetItemBuilder - : widgetItemBuilder, - multiValuePadding: - EdgeInsets.lerp(multiValuePadding, endValue.multiValuePadding, t) ?? - EdgeInsets.zero); + widgetItemBuilder: endValue is WidgetItemOptions + ? endValue.widgetItemBuilder + : widgetItemBuilder, + multiValuePadding: + EdgeInsets.lerp(multiValuePadding, endValue.multiValuePadding, t) ?? + EdgeInsets.zero, + widthCalculator: endValue.widthCalculator, + ); } } diff --git a/lib/chart/widgets/chart_widget.dart b/lib/chart/widgets/chart_widget.dart index b854f7f..0cd9c81 100644 --- a/lib/chart/widgets/chart_widget.dart +++ b/lib/chart/widgets/chart_widget.dart @@ -16,34 +16,41 @@ class _ChartWidget extends StatelessWidget { final double? width; final ChartState state; - double _horizontalPadding() { - return state.data.items.foldIndexed(0.0, - (index, double prevValue, _) { + double get _horizontalPadding { + return state.data.items.fold(0.0, (double prevValue, _) { return max(prevValue, state.itemOptions.padding.horizontal); }); } + double get _minBarWidth { + return state.data.items.fold(0.0, (double prevValue, _) { + return max(prevValue, state.itemOptions.minBarWidth ?? 0.0); + }); + } + + double get _maxBarWidth { + return state.data.items.fold(0.0, (double prevValue, _) { + return max(prevValue, state.itemOptions.maxBarWidth ?? 0.0); + }); + } + double _wantedItemWidthNormal() { - return state.data.items.foldIndexed(0.0, - (index, double prevValue, _) { - return max( - prevValue, - max( - state.itemOptions.minBarWidth ?? 0.0, - state.itemOptions.maxBarWidth ?? 0.0, - ), - ); + return state.data.items.fold(0.0, (double prevValue, _) { + return max(prevValue, max(_minBarWidth, _maxBarWidth)); }); } - double _wantedItemWidthForSetVisibleItems(double width) { - final visibleItems = state.behaviour._visibleItems; + double _wantedItemWidthForScrollable(double width) { + final visibleItems = state.behaviour.visibleItems; if (visibleItems == null) { return _wantedItemWidthNormal(); } - final wantedItemWidth = width / visibleItems - _horizontalPadding(); - return max(_wantedItemWidthNormal(), wantedItemWidth); + final itemWidth = width / visibleItems - _horizontalPadding; + final calculatedItemWidth = + state.itemOptions.widthCalculator(visibleItems, itemWidth); + + return min(_maxBarWidth, max(_minBarWidth, calculatedItemWidth)); } @override @@ -57,7 +64,7 @@ class _ChartWidget extends StatelessWidget { // What size does the item want to be? final _wantedItemWidth = state.behaviour.isScrollable - ? _wantedItemWidthForSetVisibleItems(_width) + ? _wantedItemWidthForScrollable(_width) : _wantedItemWidthNormal(); final _listSize = state.data.listSize; From a5ea65ca830f0b39fe5137387c46b1b6b3086f7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Mandi=C4=87?= Date: Sun, 15 Jan 2023 15:41:49 +0100 Subject: [PATCH 04/18] Fix error --- lib/chart/widgets/chart_widget.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/chart/widgets/chart_widget.dart b/lib/chart/widgets/chart_widget.dart index 0cd9c81..fcd072b 100644 --- a/lib/chart/widgets/chart_widget.dart +++ b/lib/chart/widgets/chart_widget.dart @@ -71,7 +71,7 @@ class _ChartWidget extends StatelessWidget { final _size = Size( _width + - (((_wantedItemWidth + _horizontalPadding()) * _listSize) - + (((_wantedItemWidth + _horizontalPadding) * _listSize) - _width) * state.behaviour._isScrollable, _height); From 58eb1ef803723ff7666de63c50352603bea47f4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Mandi=C4=87?= Date: Sun, 15 Jan 2023 17:14:29 +0100 Subject: [PATCH 05/18] Add example, add docs and improve code readability --- ...scrollable_visible_items_chart_screen.dart | 215 ++++++++++++++++++ example/lib/main.dart | 42 ++++ example/lib/widgets/candle_chart.dart | 3 + example/lib/widgets/counter_item.dart | 47 ++++ lib/chart/model/theme/chart_behaviour.dart | 6 +- .../model/theme/item_theme/item_options.dart | 4 + lib/chart/widgets/chart_widget.dart | 68 +++--- 7 files changed, 344 insertions(+), 41 deletions(-) create mode 100644 example/lib/charts/scrollable_visible_items_chart_screen.dart create mode 100644 example/lib/widgets/counter_item.dart diff --git a/example/lib/charts/scrollable_visible_items_chart_screen.dart b/example/lib/charts/scrollable_visible_items_chart_screen.dart new file mode 100644 index 0000000..e4c000f --- /dev/null +++ b/example/lib/charts/scrollable_visible_items_chart_screen.dart @@ -0,0 +1,215 @@ +import 'dart:math'; + +import 'package:charts_painter/chart.dart'; +import 'package:example/widgets/candle_chart.dart'; +import 'package:example/widgets/chart_options.dart'; +import 'package:example/widgets/counter_item.dart'; +import 'package:example/widgets/toggle_item.dart'; +import 'package:flutter/material.dart'; + +class CandleItem { + CandleItem(this.min, this.max); + + final double max; + final double min; +} + +class ScrollableVisibleItemsChartScreen extends StatefulWidget { + ScrollableVisibleItemsChartScreen({Key? key}) : super(key: key); + + @override + _ScrollableVisibleItemsChartScreenState createState() => + _ScrollableVisibleItemsChartScreenState(); +} + +class _ScrollableVisibleItemsChartScreenState + extends State { + List _values = []; + double targetMax = 0; + double targetMin = 0; + + bool _showValues = false; + int minItems = 25; + int? _selected; + bool _isScrollable = true; + int _visibleItems = 6; + + @override + void initState() { + super.initState(); + _updateValues(); + } + + void _updateValues() { + final Random _rand = Random(); + final double _difference = _rand.nextDouble() * 15; + final double _max = 8 + _difference; + + targetMax = _max * 0.75 + (_rand.nextDouble() * _max * 0.25); + targetMin = targetMax - (3 + (_rand.nextDouble() * (_max / 2))); + _values.addAll(List.generate(minItems, (index) { + double _value = 2 + _rand.nextDouble() * _difference; + return CandleItem(_value, _value + 2 + _rand.nextDouble() * 4); + })); + } + + void _addValues() { + _values = List.generate(minItems, (index) { + if (_values.length > index) { + return _values[index]; + } + double _value = 2 + Random().nextDouble() * targetMax; + return CandleItem(_value, _value + 2 + Random().nextDouble() * 4); + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text( + 'Candle chart', + ), + ), + body: Column( + children: [ + Center( + child: Padding( + padding: const EdgeInsets.all(24.0), + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + physics: _isScrollable + ? ScrollPhysics() + : NeverScrollableScrollPhysics(), + child: CandleChart( + data: _values, + height: MediaQuery.of(context).size.height * 0.4, + width: MediaQuery.of(context).size.width - 48, + dataToValue: (CandleItem value) => + CandleValue(value.min, value.max), + chartItemOptions: BarItemOptions( + minBarWidth: 10.0, + padding: EdgeInsets.symmetric(horizontal: 2.0), + barItemBuilder: (_) { + return BarItem( + color: Theme.of(context) + .colorScheme + .primary + .withOpacity(1.0), + radius: BorderRadius.all( + Radius.circular(100.0), + ), + ); + }, + ), + chartBehaviour: ChartBehaviour( + isScrollable: _isScrollable, + visibleItems: _visibleItems.toDouble(), + onItemClicked: (item) { + setState(() { + _selected = item.itemIndex; + }); + }, + ), + backgroundDecorations: [ + GridDecoration( + showHorizontalValues: _showValues, + showVerticalGrid: true, + showVerticalValues: _showValues, + verticalValuesPadding: EdgeInsets.only(left: 8.0), + horizontalAxisStep: 5, + verticalTextAlign: TextAlign.start, + gridColor: Theme.of(context) + .colorScheme + .primaryContainer + .withOpacity(0.2), + textStyle: Theme.of(context) + .textTheme + .caption! + .copyWith(fontSize: 13.0), + ), + ], + foregroundDecorations: [ + ValueDecoration( + textStyle: TextStyle(color: Colors.red), + alignment: Alignment.topCenter, + ), + ValueDecoration( + textStyle: TextStyle(color: Colors.red), + alignment: Alignment.bottomCenter, + valueGenerator: (item) => item.min ?? 0, + ), + SelectedItemDecoration( + _selected, + backgroundColor: Theme.of(context) + .scaffoldBackgroundColor + .withOpacity(0.5), + ), + ], + ), + ), + ), + ), + Flexible( + child: ChartOptionsWidget( + onRefresh: () { + setState(() { + _values.clear(); + _updateValues(); + }); + }, + onAddItems: () { + setState(() { + minItems += 4; + _addValues(); + }); + }, + onRemoveItems: () { + setState(() { + if (_values.length > 4) { + minItems -= 4; + _values.removeRange(_values.length - 4, _values.length); + } + }); + }, + toggleItems: [ + ToggleItem( + title: 'Axis values', + value: _showValues, + onChanged: (value) { + setState(() { + _showValues = value; + }); + }, + ), + ToggleItem( + value: _isScrollable, + title: 'Scrollable', + onChanged: (value) { + setState(() { + _isScrollable = value; + }); + }, + ), + CounterItem( + title: 'Visible items', + value: _visibleItems, + onMinus: () { + setState(() { + _visibleItems = max(1, _visibleItems - 1); + }); + }, + onPlus: () { + setState(() { + _visibleItems = min(36, _visibleItems + 1); + }); + }, + ) + ], + ), + ), + ], + ), + ); + } +} diff --git a/example/lib/main.dart b/example/lib/main.dart index 061a88c..fe5c1ea 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -2,6 +2,7 @@ import 'package:charts_painter/chart.dart'; import 'package:example/chart_types.dart'; import 'package:example/charts/bar_target_chart_screen.dart'; import 'package:example/charts/migration_chart_screen.dart'; +import 'package:example/charts/scrollable_visible_items_chart_screen.dart'; import 'package:example/complex/complex_charts.dart'; import 'package:example/showcase/ios_charts.dart'; import 'package:example/showcase/showcase_charts.dart'; @@ -228,6 +229,47 @@ class ShowList extends StatelessWidget { }, ), Divider(), + ListTile( + title: Text('Scrollable with visible items'), + trailing: Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Container( + width: 100.0, + child: Chart( + state: ChartState( + data: ChartData.fromList( + [1, 3, 4, 2, 7, 6, 2, 5, 4, 2, 9, 10, 2, 4, 8, 7, 7, 6, 1] + .map((e) => + CandleValue(e.toDouble() + 6, e.toDouble())) + .toList(), + axisMax: 15, + ), + itemOptions: BarItemOptions( + barItemBuilder: (_) { + return BarItem( + radius: BorderRadius.all(Radius.circular(12.0)), + color: Theme.of(context).colorScheme.secondary, + ); + }, + padding: const EdgeInsets.symmetric(horizontal: 2.0), + ), + backgroundDecorations: [ + GridDecoration( + verticalAxisStep: 1, + horizontalAxisStep: 3, + gridColor: Theme.of(context).dividerColor, + ), + ], + ), + ), + ), + ), + onTap: () { + Navigator.of(context).push(MaterialPageRoute( + builder: (_) => ScrollableVisibleItemsChartScreen())); + }, + ), + Divider(), Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), child: Text( diff --git a/example/lib/widgets/candle_chart.dart b/example/lib/widgets/candle_chart.dart index 7352a53..0b14527 100644 --- a/example/lib/widgets/candle_chart.dart +++ b/example/lib/widgets/candle_chart.dart @@ -11,6 +11,7 @@ class CandleChart extends StatelessWidget { required this.data, required this.dataToValue, this.height = 240.0, + this.width, this.backgroundDecorations = const [], this.chartBehaviour = const ChartBehaviour(), this.chartItemOptions = const BarItemOptions(), @@ -24,6 +25,7 @@ class CandleChart extends StatelessWidget { final DataToValue dataToValue; final double height; + final double? width; final List backgroundDecorations; final List foregroundDecorations; final ChartBehaviour chartBehaviour; @@ -36,6 +38,7 @@ class CandleChart extends StatelessWidget { Widget build(BuildContext context) { return AnimatedChart( height: height, + width: width, duration: const Duration(milliseconds: 450), state: ChartState( data: ChartData(_mappedValues), diff --git a/example/lib/widgets/counter_item.dart b/example/lib/widgets/counter_item.dart new file mode 100644 index 0000000..9fbda6a --- /dev/null +++ b/example/lib/widgets/counter_item.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; + +class CounterItem extends StatelessWidget { + CounterItem( + {required this.title, + required this.value, + required this.onMinus, + required this.onPlus, + Key? key}) + : super(key: key); + + final int value; + final String title; + final VoidCallback onMinus; + final VoidCallback onPlus; + + @override + Widget build(BuildContext context) { + return Container( + child: ListTile( + title: Text(title), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + InkWell( + onTap: onMinus, + child: AspectRatio( + aspectRatio: 1, + child: Icon(Icons.remove), + ), + ), + InkWell( + onTap: onPlus, + child: AspectRatio( + aspectRatio: 1, + child: Icon(Icons.add), + ), + ), + AspectRatio( + aspectRatio: 1, + child: Center(child: Text('$value')), + ), + ], + ), + )); + } +} diff --git a/lib/chart/model/theme/chart_behaviour.dart b/lib/chart/model/theme/chart_behaviour.dart index 513302f..0208fe7 100644 --- a/lib/chart/model/theme/chart_behaviour.dart +++ b/lib/chart/model/theme/chart_behaviour.dart @@ -12,9 +12,7 @@ class ChartBehaviour { bool isScrollable = false, this.visibleItems, this.onItemClicked, - }) : assert(visibleItems == null || isScrollable, - 'visibleItems are only used when chart is scrollable'), - assert(visibleItems == null || visibleItems > 0, + }) : assert(visibleItems == null || visibleItems > 0, 'visibleItems must be greater than 0'), _isScrollable = isScrollable ? 1.0 : 0.0; @@ -23,6 +21,8 @@ class ChartBehaviour { final double _isScrollable; + /// Number of visible items on the screen. + /// Used when [isScrollable] is true final double? visibleItems; /// Return index of item clicked. Since graph can be multi value, user diff --git a/lib/chart/model/theme/item_theme/item_options.dart b/lib/chart/model/theme/item_theme/item_options.dart index fd8a800..f7b7949 100644 --- a/lib/chart/model/theme/item_theme/item_options.dart +++ b/lib/chart/model/theme/item_theme/item_options.dart @@ -8,6 +8,10 @@ typedef ChartGeometryPainter = GeometryPainter Function( ItemOptions itemOptions, DrawDataItem drawDataItem); +/// Item width calculator, used to get the width of the item. +/// Called with current [visibleItems] value from [ChartBehaviour] +/// and [calculatedWidth] which is value that the item should be in order to fit +/// [visibleItems] number of items on the screen. typedef ItemWidthCalculator = double Function( double visibleItems, double calculatedWidth); diff --git a/lib/chart/widgets/chart_widget.dart b/lib/chart/widgets/chart_widget.dart index fcd072b..cad81f7 100644 --- a/lib/chart/widgets/chart_widget.dart +++ b/lib/chart/widgets/chart_widget.dart @@ -16,68 +16,60 @@ class _ChartWidget extends StatelessWidget { final double? width; final ChartState state; - double get _horizontalPadding { - return state.data.items.fold(0.0, (double prevValue, _) { - return max(prevValue, state.itemOptions.padding.horizontal); - }); - } + double get _horizontalPadding => state.itemOptions.padding.horizontal; - double get _minBarWidth { - return state.data.items.fold(0.0, (double prevValue, _) { - return max(prevValue, state.itemOptions.minBarWidth ?? 0.0); - }); - } + double? get _minBarWidth => state.itemOptions.minBarWidth; - double get _maxBarWidth { - return state.data.items.fold(0.0, (double prevValue, _) { - return max(prevValue, state.itemOptions.maxBarWidth ?? 0.0); - }); - } + double? get _maxBarWidth => state.itemOptions.maxBarWidth; - double _wantedItemWidthNormal() { - return state.data.items.fold(0.0, (double prevValue, _) { - return max(prevValue, max(_minBarWidth, _maxBarWidth)); - }); + double _wantedItemWidthNonScrollable() { + return max(_minBarWidth ?? 0.0, _maxBarWidth ?? 0.0); } - double _wantedItemWidthForScrollable(double width) { + double _wantedItemWidthForScrollable(double frameWidth) { final visibleItems = state.behaviour.visibleItems; if (visibleItems == null) { - return _wantedItemWidthNormal(); + return _wantedItemWidthNonScrollable(); } - final itemWidth = width / visibleItems - _horizontalPadding; - final calculatedItemWidth = - state.itemOptions.widthCalculator(visibleItems, itemWidth); + final itemWidth = frameWidth / visibleItems - _horizontalPadding; + return state.itemOptions.widthCalculator(visibleItems, itemWidth); - return min(_maxBarWidth, max(_minBarWidth, calculatedItemWidth)); + // if (_maxBarWidth == null) { + // return max(_minBarWidth ?? 0.0, calculatedItemWidth); + // } else { + // return min(_maxBarWidth!, max(_minBarWidth ?? 0.0, calculatedItemWidth)); + // } } @override Widget build(BuildContext context) { return LayoutBuilder( builder: (context, constraints) { - final _width = + final frameWidth = constraints.maxWidth.isFinite ? constraints.maxWidth : width!; - final _height = + final frameHeight = constraints.maxHeight.isFinite ? constraints.maxHeight : height!; + final sizeTween = Tween( + begin: _wantedItemWidthNonScrollable(), + end: _wantedItemWidthForScrollable(frameWidth), + ); + // What size does the item want to be? - final _wantedItemWidth = state.behaviour.isScrollable - ? _wantedItemWidthForScrollable(_width) - : _wantedItemWidthNormal(); + final wantedItemWidth = + sizeTween.transform(state.behaviour._isScrollable); + + final listSize = state.data.listSize; - final _listSize = state.data.listSize; + final finalWidth = frameWidth + + (((wantedItemWidth + _horizontalPadding) * listSize) - frameWidth) * + state.behaviour._isScrollable; - final _size = Size( - _width + - (((_wantedItemWidth + _horizontalPadding) * _listSize) - - _width) * - state.behaviour._isScrollable, - _height); + final size = Size(finalWidth, frameHeight); return Container( - constraints: BoxConstraints.tight(_size), + constraints: BoxConstraints.tight(size), child: ChartRenderer(state), ); }, From 5f6fc7af031d4cdd9cc2478ed9c7864c7ec267ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Mandi=C4=87?= Date: Sun, 15 Jan 2023 17:15:19 +0100 Subject: [PATCH 06/18] Add ignore web example to analyzer --- analysis_options.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/analysis_options.yaml b/analysis_options.yaml index 043a519..9198f69 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -41,4 +41,5 @@ analyzer: exclude: - "bin/cache/**" - "lib/assets.dart" -# - "example/**" \ No newline at end of file +# - "example/**" +# - "charts_web/**" From 6e95cd5bc7bb2d90c312c85520d1d6aeefe1f3f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Mandi=C4=87?= Date: Sun, 15 Jan 2023 17:44:15 +0100 Subject: [PATCH 07/18] Update readme --- README.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index efd8bfe..9432f40 100644 --- a/README.md +++ b/README.md @@ -96,7 +96,8 @@ barItemBuilder: (data) { The `data` that’s passed into the builder can be used to build different kind of item based on the item value (`data.item.value`), his index in data (`data.itemIndex`) or based on which data list it belongs to (`data.listIndex`). -Besides builder, the other useful parameters in item options are `maxBarWidth` , `minBarWidth` , `startPosition` and `padding`. +Besides builder, the other useful parameters in item options are `maxBarWidth` , `minBarWidth` , `startPosition` , `padding` and `widthCalculator`. +`widthCalculator` is used in scrollable charts when `visibleItems` is not `null` and it provides a way to control the width of items. If you want to listen to **item taps** you can do it by setting `ChartBehaviour(onItemClicked)` - you can read more about ChartBehaviour below. In case of a WidgetItemOptions, you could also provide GestureDetectors and Buttons and they will all work. @@ -106,8 +107,8 @@ In case of a WidgetItemOptions, you could also provide GestureDetectors and Butt Decorations enhance and complete the look of the chart. Everything that’s drawn on a chart, and it’s not a chart item is considered a decoration. So that means a lot of the chart will be a decoration. Just like with the items, you can use **WidgetDecoration** to draw any kind of the decoration, but the most common cases for decoration are already made on a canvas and ready to be used: | | | | -:------: | :------: | :------: -[![horizontal_decoration] Horizontal decoration](https://github.com/infinum/flutter-charts/blob/master/test/golden/GOLDENS.md#horizontal-decoration-golden) | [![vertical_decoration] Vertical decoration](https://github.com/infinum/flutter-charts/blob/master/test/golden/GOLDENS.md#vertical-decoration-golden) | [![grid_decoration] Grid decoration](https://github.com/infinum/flutter-charts/blob/master/test/golden/GOLDENS.md#grid-decoration-golden) +:------: | :------: | :------: +[![horizontal_decoration] Horizontal decoration](https://github.com/infinum/flutter-charts/blob/master/test/golden/GOLDENS.md#horizontal-decoration-golden) | [![vertical_decoration] Vertical decoration](https://github.com/infinum/flutter-charts/blob/master/test/golden/GOLDENS.md#vertical-decoration-golden) | [![grid_decoration] Grid decoration](https://github.com/infinum/flutter-charts/blob/master/test/golden/GOLDENS.md#grid-decoration-golden) [![sparkline_decoration] Sparkline decoration](https://github.com/infinum/flutter-charts/blob/master/test/golden/GOLDENS.md#sparkline-decoration-golden) | @@ -139,9 +140,10 @@ You can add padding that equals the chart margins which will set you to the star ## Chart behaviour -Chart behaviour has just two parameters: +Chart behaviour has just three parameters: - `isScrollable` - will render a chart that can support scrolling. You still need to wrap it with SingleChildScrollView. +- `visibleItems` - used when `isScrollable` is set to `true`. Indicates how many items should visible on the screen - `onItemClicked` - when set the tap events on items are registered and will invoke this method. If you're using WidgetItemOptions, you could set a gesture detector there, but this works with both BarItemOptions, BubbleItemOptions and WidgetItemOptions. From af7ac4e87ba5862952fc76a22dd2843b3c3ae722 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Mandi=C4=87?= Date: Sun, 15 Jan 2023 20:20:23 +0100 Subject: [PATCH 08/18] Add docs --- lib/chart/widgets/chart_widget.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/chart/widgets/chart_widget.dart b/lib/chart/widgets/chart_widget.dart index cad81f7..8d86cdb 100644 --- a/lib/chart/widgets/chart_widget.dart +++ b/lib/chart/widgets/chart_widget.dart @@ -51,12 +51,12 @@ class _ChartWidget extends StatelessWidget { final frameHeight = constraints.maxHeight.isFinite ? constraints.maxHeight : height!; + // Used for smooth transition between scrollable and non-scrollable chart final sizeTween = Tween( begin: _wantedItemWidthNonScrollable(), end: _wantedItemWidthForScrollable(frameWidth), ); - // What size does the item want to be? final wantedItemWidth = sizeTween.transform(state.behaviour._isScrollable); From 57bd80f871740263b77881ca8d1d6eb7c091a01c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Mandi=C4=87?= Date: Sun, 15 Jan 2023 20:22:12 +0100 Subject: [PATCH 09/18] Fix readme formatting --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9432f40..011fd26 100644 --- a/README.md +++ b/README.md @@ -96,7 +96,7 @@ barItemBuilder: (data) { The `data` that’s passed into the builder can be used to build different kind of item based on the item value (`data.item.value`), his index in data (`data.itemIndex`) or based on which data list it belongs to (`data.listIndex`). -Besides builder, the other useful parameters in item options are `maxBarWidth` , `minBarWidth` , `startPosition` , `padding` and `widthCalculator`. +Besides builder, the other useful parameters in item options are `maxBarWidth` , `minBarWidth` , `startPosition` , `padding` and `widthCalculator`.
`widthCalculator` is used in scrollable charts when `visibleItems` is not `null` and it provides a way to control the width of items. If you want to listen to **item taps** you can do it by setting `ChartBehaviour(onItemClicked)` - you can read more about ChartBehaviour below. From 356bfddb8822f1779f0f77e73cd70d3181d7512d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Mandi=C4=87?= Date: Sun, 15 Jan 2023 21:12:24 +0100 Subject: [PATCH 10/18] Update width calculator declaration and default function --- lib/chart/model/theme/item_theme/item_options.dart | 13 +++++++------ lib/chart/widgets/chart_widget.dart | 12 ++++++++---- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/lib/chart/model/theme/item_theme/item_options.dart b/lib/chart/model/theme/item_theme/item_options.dart index f7b7949..9c03449 100644 --- a/lib/chart/model/theme/item_theme/item_options.dart +++ b/lib/chart/model/theme/item_theme/item_options.dart @@ -9,11 +9,11 @@ typedef ChartGeometryPainter = GeometryPainter Function( DrawDataItem drawDataItem); /// Item width calculator, used to get the width of the item. -/// Called with current [visibleItems] value from [ChartBehaviour] -/// and [calculatedWidth] which is value that the item should be in order to fit -/// [visibleItems] number of items on the screen. +/// Called with current [visibleItems] value from [ChartBehaviour], +/// [padding] only containing horizontal values of [padding] from [ItemOptions] and +/// [frameWidth] which is the visible width of the chart. typedef ItemWidthCalculator = double Function( - double visibleItems, double calculatedWidth); + double visibleItems, EdgeInsets padding, double frameWidth); /// Options for chart items. You can use this subclasses: [BarItemOptions], [BubbleItemOptions], [WidgetItemOptions] /// @@ -93,6 +93,7 @@ abstract class ItemOptions { ItemOptions animateTo(ItemOptions endValue, double t); } -double _defaultWidthCalculator(double visibleItems, double calculatedWidth) { - return calculatedWidth; +double _defaultWidthCalculator( + double visibleItems, EdgeInsets padding, double frameWidth) { + return max(0, frameWidth / visibleItems - padding.horizontal); } diff --git a/lib/chart/widgets/chart_widget.dart b/lib/chart/widgets/chart_widget.dart index 8d86cdb..60ed5eb 100644 --- a/lib/chart/widgets/chart_widget.dart +++ b/lib/chart/widgets/chart_widget.dart @@ -16,7 +16,8 @@ class _ChartWidget extends StatelessWidget { final double? width; final ChartState state; - double get _horizontalPadding => state.itemOptions.padding.horizontal; + EdgeInsets get _horizontalItemPadding => + state.itemOptions.padding.copyWith(top: 0.0, bottom: 0.0); double? get _minBarWidth => state.itemOptions.minBarWidth; @@ -32,8 +33,8 @@ class _ChartWidget extends StatelessWidget { return _wantedItemWidthNonScrollable(); } - final itemWidth = frameWidth / visibleItems - _horizontalPadding; - return state.itemOptions.widthCalculator(visibleItems, itemWidth); + return state.itemOptions + .widthCalculator(visibleItems, _horizontalItemPadding, frameWidth); // if (_maxBarWidth == null) { // return max(_minBarWidth ?? 0.0, calculatedItemWidth); @@ -57,13 +58,16 @@ class _ChartWidget extends StatelessWidget { end: _wantedItemWidthForScrollable(frameWidth), ); + final horizontalItemPadding = state.itemOptions.padding.horizontal; + final wantedItemWidth = sizeTween.transform(state.behaviour._isScrollable); final listSize = state.data.listSize; final finalWidth = frameWidth + - (((wantedItemWidth + _horizontalPadding) * listSize) - frameWidth) * + (((wantedItemWidth + horizontalItemPadding) * listSize) - + frameWidth) * state.behaviour._isScrollable; final size = Size(finalWidth, frameHeight); From a61f8c1b86da6bfc794040aded6ed1c0fbdd4089 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Mandi=C4=87?= Date: Sun, 15 Jan 2023 21:23:51 +0100 Subject: [PATCH 11/18] Remove unnecessary comments --- lib/chart/widgets/chart_widget.dart | 6 ------ 1 file changed, 6 deletions(-) diff --git a/lib/chart/widgets/chart_widget.dart b/lib/chart/widgets/chart_widget.dart index 60ed5eb..98e4291 100644 --- a/lib/chart/widgets/chart_widget.dart +++ b/lib/chart/widgets/chart_widget.dart @@ -35,12 +35,6 @@ class _ChartWidget extends StatelessWidget { return state.itemOptions .widthCalculator(visibleItems, _horizontalItemPadding, frameWidth); - - // if (_maxBarWidth == null) { - // return max(_minBarWidth ?? 0.0, calculatedItemWidth); - // } else { - // return min(_maxBarWidth!, max(_minBarWidth ?? 0.0, calculatedItemWidth)); - // } } @override From a6bbcde22a539c2a57d797ef2799e68d79278604 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Mandi=C4=87?= Date: Sat, 4 Feb 2023 19:11:56 +0100 Subject: [PATCH 12/18] Remove itemWidthCalculator --- .../item_theme/bar/bar_item_options.dart | 5 ---- .../bubble/bubble_item_options.dart | 5 ---- .../model/theme/item_theme/item_options.dart | 25 +++---------------- .../widget/widget_item_options.dart | 5 ---- lib/chart/widgets/chart_widget.dart | 16 +++++++----- 5 files changed, 13 insertions(+), 43 deletions(-) diff --git a/lib/chart/model/theme/item_theme/bar/bar_item_options.dart b/lib/chart/model/theme/item_theme/bar/bar_item_options.dart index 37835fe..5f63327 100644 --- a/lib/chart/model/theme/item_theme/bar/bar_item_options.dart +++ b/lib/chart/model/theme/item_theme/bar/bar_item_options.dart @@ -19,7 +19,6 @@ class BarItemOptions extends ItemOptions { const BarItemOptions({ EdgeInsets padding = EdgeInsets.zero, EdgeInsets multiValuePadding = EdgeInsets.zero, - ItemWidthCalculator? widthCalculator, double? maxBarWidth, double? minBarWidth, double startPosition = 0.5, @@ -30,7 +29,6 @@ class BarItemOptions extends ItemOptions { maxBarWidth: maxBarWidth, minBarWidth: minBarWidth, startPosition: startPosition, - widthCalculator: widthCalculator, geometryPainter: barPainter, itemBuilder: barItemBuilder); @@ -40,14 +38,12 @@ class BarItemOptions extends ItemOptions { double? maxBarWidth, double? minBarWidth, double startPosition = 0.5, - ItemWidthCalculator? widthCalculator, required this.barItemBuilder}) : super._lerp( padding: padding, multiValuePadding: multiValuePadding, maxBarWidth: maxBarWidth, minBarWidth: minBarWidth, - widthCalculator: widthCalculator, startPosition: startPosition, geometryPainter: barPainter, itemBuilder: barItemBuilder); @@ -68,7 +64,6 @@ class BarItemOptions extends ItemOptions { minBarWidth: lerpDouble(minBarWidth, endValue.minBarWidth, t), startPosition: lerpDouble(startPosition, endValue.startPosition, t) ?? 0.5, - widthCalculator: super.widthCalculator, ); } else { return endValue; diff --git a/lib/chart/model/theme/item_theme/bubble/bubble_item_options.dart b/lib/chart/model/theme/item_theme/bubble/bubble_item_options.dart index 62fc8c5..22e0d5f 100644 --- a/lib/chart/model/theme/item_theme/bubble/bubble_item_options.dart +++ b/lib/chart/model/theme/item_theme/bubble/bubble_item_options.dart @@ -19,7 +19,6 @@ class BubbleItemOptions extends ItemOptions { BubbleItemOptions({ EdgeInsets padding = EdgeInsets.zero, EdgeInsets multiValuePadding = EdgeInsets.zero, - ItemWidthCalculator? widthCalculator, double? maxBarWidth, double? minBarWidth, this.bubbleItemBuilder = _defaultBubbleItem, @@ -29,7 +28,6 @@ class BubbleItemOptions extends ItemOptions { minBarWidth: minBarWidth, maxBarWidth: maxBarWidth, geometryPainter: bubblePainter, - widthCalculator: widthCalculator, itemBuilder: bubbleItemBuilder); BubbleItemOptions._lerp({ @@ -37,7 +35,6 @@ class BubbleItemOptions extends ItemOptions { EdgeInsets multiValuePadding = EdgeInsets.zero, double? maxBarWidth, double? minBarWidth, - ItemWidthCalculator? widthCalculator, required this.bubbleItemBuilder, }) : super._lerp( padding: padding, @@ -46,7 +43,6 @@ class BubbleItemOptions extends ItemOptions { maxBarWidth: maxBarWidth, geometryPainter: bubblePainter, itemBuilder: bubbleItemBuilder, - widthCalculator: widthCalculator, ); final BubbleItemBuilder bubbleItemBuilder; @@ -63,7 +59,6 @@ class BubbleItemOptions extends ItemOptions { EdgeInsets.zero, maxBarWidth: lerpDouble(maxBarWidth, endValue.maxBarWidth, t), minBarWidth: lerpDouble(minBarWidth, endValue.minBarWidth, t), - widthCalculator: widthCalculator, ); } else { return endValue; diff --git a/lib/chart/model/theme/item_theme/item_options.dart b/lib/chart/model/theme/item_theme/item_options.dart index 9c03449..7d726e0 100644 --- a/lib/chart/model/theme/item_theme/item_options.dart +++ b/lib/chart/model/theme/item_theme/item_options.dart @@ -8,13 +8,6 @@ typedef ChartGeometryPainter = GeometryPainter Function( ItemOptions itemOptions, DrawDataItem drawDataItem); -/// Item width calculator, used to get the width of the item. -/// Called with current [visibleItems] value from [ChartBehaviour], -/// [padding] only containing horizontal values of [padding] from [ItemOptions] and -/// [frameWidth] which is the visible width of the chart. -typedef ItemWidthCalculator = double Function( - double visibleItems, EdgeInsets padding, double frameWidth); - /// Options for chart items. You can use this subclasses: [BarItemOptions], [BubbleItemOptions], [WidgetItemOptions] /// /// Required [itemBuilder] parameter is used to provide a data for each item on the chart. @@ -35,9 +28,8 @@ abstract class ItemOptions { this.maxBarWidth, this.minBarWidth, this.startPosition = 0.5, - ItemWidthCalculator? widthCalculator, required this.itemBuilder, - }) : widthCalculator = widthCalculator ?? _defaultWidthCalculator; + }); const ItemOptions._lerp({ required this.geometryPainter, @@ -48,11 +40,9 @@ abstract class ItemOptions { this.startPosition = 0.5, double multiItemStack = 1.0, required this.itemBuilder, - ItemWidthCalculator? widthCalculator, - }) : assert(maxBarWidth == null || + }) : assert(maxBarWidth == null || minBarWidth == null || - maxBarWidth >= minBarWidth), - widthCalculator = widthCalculator ?? _defaultWidthCalculator; + maxBarWidth >= minBarWidth); /// Item padding, if [minBarWidth] and [padding] are more then available space /// [padding] will get ignored @@ -65,10 +55,6 @@ abstract class ItemOptions { final ItemBuilder itemBuilder; - /// Called to specify the desired width of the item - /// in case [visibleItems] in [ChartBehaviour] is not null. - final ItemWidthCalculator widthCalculator; - /// Max width of item in the chart final double? maxBarWidth; @@ -92,8 +78,3 @@ abstract class ItemOptions { /// with all available options, otherwise changes in options won't be animated ItemOptions animateTo(ItemOptions endValue, double t); } - -double _defaultWidthCalculator( - double visibleItems, EdgeInsets padding, double frameWidth) { - return max(0, frameWidth / visibleItems - padding.horizontal); -} diff --git a/lib/chart/model/theme/item_theme/widget/widget_item_options.dart b/lib/chart/model/theme/item_theme/widget/widget_item_options.dart index b5a1a88..cec1b2e 100644 --- a/lib/chart/model/theme/item_theme/widget/widget_item_options.dart +++ b/lib/chart/model/theme/item_theme/widget/widget_item_options.dart @@ -42,25 +42,21 @@ class WidgetItemOptions extends ItemOptions { /// Constructor for bar item options, has some extra options just for [BarGeometryPainter] const WidgetItemOptions({ required this.widgetItemBuilder, - ItemWidthCalculator? widthCalculator, EdgeInsets multiValuePadding = EdgeInsets.zero, }) : super( padding: EdgeInsets.zero, multiValuePadding: multiValuePadding, geometryPainter: _emptyPainter, - widthCalculator: widthCalculator, itemBuilder: widgetItemBuilder, ); const WidgetItemOptions._lerp({ required this.widgetItemBuilder, - ItemWidthCalculator? widthCalculator, EdgeInsets multiValuePadding = EdgeInsets.zero, }) : super._lerp( multiValuePadding: multiValuePadding, geometryPainter: _emptyPainter, itemBuilder: widgetItemBuilder, - widthCalculator: widthCalculator, ); final WidgetItemBuilder widgetItemBuilder; @@ -74,7 +70,6 @@ class WidgetItemOptions extends ItemOptions { multiValuePadding: EdgeInsets.lerp(multiValuePadding, endValue.multiValuePadding, t) ?? EdgeInsets.zero, - widthCalculator: endValue.widthCalculator, ); } } diff --git a/lib/chart/widgets/chart_widget.dart b/lib/chart/widgets/chart_widget.dart index 98e4291..2a9c4b5 100644 --- a/lib/chart/widgets/chart_widget.dart +++ b/lib/chart/widgets/chart_widget.dart @@ -33,8 +33,11 @@ class _ChartWidget extends StatelessWidget { return _wantedItemWidthNonScrollable(); } - return state.itemOptions - .widthCalculator(visibleItems, _horizontalItemPadding, frameWidth); + final width = + (frameWidth - state.defaultPadding.horizontal) / visibleItems - + _horizontalItemPadding.horizontal; + + return max(0, width); } @override @@ -58,11 +61,12 @@ class _ChartWidget extends StatelessWidget { sizeTween.transform(state.behaviour._isScrollable); final listSize = state.data.listSize; + final totalItemWidth = wantedItemWidth + horizontalItemPadding; + final listWidth = totalItemWidth * listSize; - final finalWidth = frameWidth + - (((wantedItemWidth + horizontalItemPadding) * listSize) - - frameWidth) * - state.behaviour._isScrollable; + final chartWidth = frameWidth + + (listWidth - frameWidth) * state.behaviour._isScrollable; + final finalWidth = chartWidth + state.defaultPadding.horizontal; final size = Size(finalWidth, frameHeight); From 5237853e7accbf2415c0862cb5c939b42f6a8287 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Mandi=C4=87?= Date: Sat, 4 Feb 2023 19:38:25 +0100 Subject: [PATCH 13/18] Add ScrollSettings class Class for configuring chart scrolling --- .../lib/charts/scrollable_chart_screen.dart | 3 +- ...scrollable_visible_items_chart_screen.dart | 5 ++-- lib/chart.dart | 1 + lib/chart/model/theme/chart_behaviour.dart | 27 ++++------------- lib/chart/model/theme/scroll_settings.dart | 30 +++++++++++++++++++ lib/chart/widgets/chart_widget.dart | 7 +++-- 6 files changed, 46 insertions(+), 27 deletions(-) create mode 100644 lib/chart/model/theme/scroll_settings.dart diff --git a/example/lib/charts/scrollable_chart_screen.dart b/example/lib/charts/scrollable_chart_screen.dart index b6f058e..2913567 100644 --- a/example/lib/charts/scrollable_chart_screen.dart +++ b/example/lib/charts/scrollable_chart_screen.dart @@ -90,7 +90,8 @@ class _ScrollableChartScreenState extends State { }, ), behaviour: ChartBehaviour( - isScrollable: _isScrollable, + scrollSettings: + _isScrollable ? ScrollSettings() : ScrollSettings.none(), onItemClicked: (item) { setState(() { _selected = item.itemIndex; diff --git a/example/lib/charts/scrollable_visible_items_chart_screen.dart b/example/lib/charts/scrollable_visible_items_chart_screen.dart index e4c000f..0c09d18 100644 --- a/example/lib/charts/scrollable_visible_items_chart_screen.dart +++ b/example/lib/charts/scrollable_visible_items_chart_screen.dart @@ -103,8 +103,9 @@ class _ScrollableVisibleItemsChartScreenState }, ), chartBehaviour: ChartBehaviour( - isScrollable: _isScrollable, - visibleItems: _visibleItems.toDouble(), + scrollSettings: _isScrollable + ? ScrollSettings(visibleItems: _visibleItems.toDouble()) + : ScrollSettings.none(), onItemClicked: (item) { setState(() { _selected = item.itemIndex; diff --git a/lib/chart.dart b/lib/chart.dart index dfdb4e0..9dc873d 100644 --- a/lib/chart.dart +++ b/lib/chart.dart @@ -27,6 +27,7 @@ part 'chart/model/geometry/chart_item.dart'; /// Theme part 'chart/model/theme/chart_behaviour.dart'; +part 'chart/model/theme/scroll_settings.dart'; part 'chart/model/theme/item_theme/bar/bar_item_options.dart'; part 'chart/model/theme/item_theme/item_options.dart'; part 'chart/model/theme/item_theme/bar/bar_item.dart'; diff --git a/lib/chart/model/theme/chart_behaviour.dart b/lib/chart/model/theme/chart_behaviour.dart index 0208fe7..53a9e1f 100644 --- a/lib/chart/model/theme/chart_behaviour.dart +++ b/lib/chart/model/theme/chart_behaviour.dart @@ -9,40 +9,25 @@ class ChartBehaviour { /// Default constructor for ChartBehaviour /// If chart is scrollable then it will ignore width limit and it should be wrapped in [SingleChildScrollView] const ChartBehaviour({ - bool isScrollable = false, - this.visibleItems, + this.scrollSettings = const ScrollSettings.none(), this.onItemClicked, - }) : assert(visibleItems == null || visibleItems > 0, - 'visibleItems must be greater than 0'), - _isScrollable = isScrollable ? 1.0 : 0.0; + }); - ChartBehaviour._lerp( - this._isScrollable, this.visibleItems, this.onItemClicked); + ChartBehaviour._lerp(this.scrollSettings, this.onItemClicked); - final double _isScrollable; - - /// Number of visible items on the screen. - /// Used when [isScrollable] is true - final double? visibleItems; + final ScrollSettings scrollSettings; /// Return index of item clicked. Since graph can be multi value, user /// will have to handle clicked index to show data they want to show final ValueChanged? onItemClicked; /// Return true if chart is currently scrollable - bool get isScrollable => _isScrollable > 0.5; + bool get isScrollable => scrollSettings._isScrollable > 0.0; /// Animate Behaviour from one state to other static ChartBehaviour lerp(ChartBehaviour a, ChartBehaviour b, double t) { - // This values should never return null, this is for null-safety - // But if it somehow does occur, then revert to default values - final scrollableLerp = - lerpDouble(a._isScrollable, b._isScrollable, t) ?? 0.0; - final visibleLerp = lerpDouble(a.visibleItems, b.visibleItems, t); - return ChartBehaviour._lerp( - scrollableLerp, - visibleLerp, + ScrollSettings.lerp(a.scrollSettings, b.scrollSettings, t), t > 0.5 ? b.onItemClicked : a.onItemClicked, ); } diff --git a/lib/chart/model/theme/scroll_settings.dart b/lib/chart/model/theme/scroll_settings.dart new file mode 100644 index 0000000..1cf47bf --- /dev/null +++ b/lib/chart/model/theme/scroll_settings.dart @@ -0,0 +1,30 @@ +part of charts_painter; + +class ScrollSettings { + const ScrollSettings({ + this.visibleItems, + }) : assert(visibleItems == null || visibleItems > 0, + 'visibleItems must be greater than 0'), + _isScrollable = 1.0; + + const ScrollSettings.none() + : _isScrollable = 0.0, + visibleItems = null; + + ScrollSettings._lerp(this._isScrollable, this.visibleItems); + + final double _isScrollable; + + /// Number of visible items on the screen. + final double? visibleItems; + + static ScrollSettings lerp(ScrollSettings a, ScrollSettings b, double t) { + // This values should never return null, this is for null-safety + // But if it somehow does occur, then revert to default values + final scrollableLerp = + lerpDouble(a._isScrollable, b._isScrollable, t) ?? 0.0; + final visibleLerp = lerpDouble(a.visibleItems, b.visibleItems, t); + + return ScrollSettings._lerp(scrollableLerp, visibleLerp); + } +} diff --git a/lib/chart/widgets/chart_widget.dart b/lib/chart/widgets/chart_widget.dart index 2a9c4b5..bd8d55b 100644 --- a/lib/chart/widgets/chart_widget.dart +++ b/lib/chart/widgets/chart_widget.dart @@ -28,7 +28,7 @@ class _ChartWidget extends StatelessWidget { } double _wantedItemWidthForScrollable(double frameWidth) { - final visibleItems = state.behaviour.visibleItems; + final visibleItems = state.behaviour.scrollSettings.visibleItems; if (visibleItems == null) { return _wantedItemWidthNonScrollable(); } @@ -58,14 +58,15 @@ class _ChartWidget extends StatelessWidget { final horizontalItemPadding = state.itemOptions.padding.horizontal; final wantedItemWidth = - sizeTween.transform(state.behaviour._isScrollable); + sizeTween.transform(state.behaviour.scrollSettings._isScrollable); final listSize = state.data.listSize; final totalItemWidth = wantedItemWidth + horizontalItemPadding; final listWidth = totalItemWidth * listSize; final chartWidth = frameWidth + - (listWidth - frameWidth) * state.behaviour._isScrollable; + (listWidth - frameWidth) * + state.behaviour.scrollSettings._isScrollable; final finalWidth = chartWidth + state.defaultPadding.horizontal; final size = Size(finalWidth, frameHeight); From 4c33e1b5c30dfdf1657b5a7bfa849ac9f28735e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Mandi=C4=87?= Date: Sat, 4 Feb 2023 19:51:02 +0100 Subject: [PATCH 14/18] Organize code that calculates the widths --- lib/chart/widgets/chart_widget.dart | 81 +++++++++++++++++------------ 1 file changed, 48 insertions(+), 33 deletions(-) diff --git a/lib/chart/widgets/chart_widget.dart b/lib/chart/widgets/chart_widget.dart index bd8d55b..142221f 100644 --- a/lib/chart/widgets/chart_widget.dart +++ b/lib/chart/widgets/chart_widget.dart @@ -16,28 +16,62 @@ class _ChartWidget extends StatelessWidget { final double? width; final ChartState state; - EdgeInsets get _horizontalItemPadding => - state.itemOptions.padding.copyWith(top: 0.0, bottom: 0.0); + double get _horizontalItemPadding => state.itemOptions.padding.horizontal; - double? get _minBarWidth => state.itemOptions.minBarWidth; + double _clampWidth(double width) { + final minBarWidth = state.itemOptions.minBarWidth; + final maxBarWidth = state.itemOptions.maxBarWidth; - double? get _maxBarWidth => state.itemOptions.maxBarWidth; + if (minBarWidth != null) { + return max(minBarWidth, width); + } + + if (maxBarWidth != null) { + return min(maxBarWidth, width); + } + + return width; + } - double _wantedItemWidthNonScrollable() { - return max(_minBarWidth ?? 0.0, _maxBarWidth ?? 0.0); + double _calcItemWidthNonScrollable() { + return max( + state.itemOptions.minBarWidth ?? 0.0, + state.itemOptions.maxBarWidth ?? 0.0, + ); } - double _wantedItemWidthForScrollable(double frameWidth) { + double _calcItemWidthForScrollable(double frameWidth) { final visibleItems = state.behaviour.scrollSettings.visibleItems; if (visibleItems == null) { - return _wantedItemWidthNonScrollable(); + return _calcItemWidthNonScrollable(); } - final width = - (frameWidth - state.defaultPadding.horizontal) / visibleItems - - _horizontalItemPadding.horizontal; + final availableWidth = frameWidth - state.defaultPadding.horizontal; + final width = availableWidth / visibleItems - _horizontalItemPadding; + + return _clampWidth(max(0, width)); + } + + double _calcItemWidth(double frameWidth) { + // Used for smooth transition between scrollable and non-scrollable chart + final sizeTween = Tween( + begin: _calcItemWidthNonScrollable(), + end: _calcItemWidthForScrollable(frameWidth), + ); + + return sizeTween.transform(state.behaviour.scrollSettings._isScrollable); + } + + Size _calcChartSize(double itemWidth, double frameWidth, double frameHeight) { + final listSize = state.data.listSize; + final totalItemWidth = itemWidth + _horizontalItemPadding; + final listWidth = totalItemWidth * listSize; - return max(0, width); + final chartWidth = frameWidth + + (listWidth - frameWidth) * state.behaviour.scrollSettings._isScrollable; + final finalWidth = chartWidth + state.defaultPadding.horizontal; + + return Size(finalWidth, frameHeight); } @override @@ -49,27 +83,8 @@ class _ChartWidget extends StatelessWidget { final frameHeight = constraints.maxHeight.isFinite ? constraints.maxHeight : height!; - // Used for smooth transition between scrollable and non-scrollable chart - final sizeTween = Tween( - begin: _wantedItemWidthNonScrollable(), - end: _wantedItemWidthForScrollable(frameWidth), - ); - - final horizontalItemPadding = state.itemOptions.padding.horizontal; - - final wantedItemWidth = - sizeTween.transform(state.behaviour.scrollSettings._isScrollable); - - final listSize = state.data.listSize; - final totalItemWidth = wantedItemWidth + horizontalItemPadding; - final listWidth = totalItemWidth * listSize; - - final chartWidth = frameWidth + - (listWidth - frameWidth) * - state.behaviour.scrollSettings._isScrollable; - final finalWidth = chartWidth + state.defaultPadding.horizontal; - - final size = Size(finalWidth, frameHeight); + final itemWidth = _calcItemWidth(frameWidth); + final size = _calcChartSize(itemWidth, frameWidth, frameHeight); return Container( constraints: BoxConstraints.tight(size), From b01ec2671b3abf0d9c89f9d0b01af6222d53ad27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Mandi=C4=87?= Date: Sat, 4 Feb 2023 19:52:10 +0100 Subject: [PATCH 15/18] Update gitignore to exclude .vscode folder --- .gitignore | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index e9b2ec3..7859d28 100644 --- a/.gitignore +++ b/.gitignore @@ -15,10 +15,8 @@ *.iws .idea/ -# The .vscode folder contains launch configuration and tasks you configure in -# VS Code which you may wish to be included in version control, so this line -# is commented out by default. -#.vscode/ +# VSCode related +.vscode/ ### Flutter ### # Flutter/Dart/Pub related From 31b3816dbb28b3f0113d2f59792712442920cb4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Mandi=C4=87?= Date: Sat, 4 Feb 2023 19:52:53 +0100 Subject: [PATCH 16/18] Rename method --- lib/chart/widgets/chart_widget.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/chart/widgets/chart_widget.dart b/lib/chart/widgets/chart_widget.dart index 142221f..c6da7fc 100644 --- a/lib/chart/widgets/chart_widget.dart +++ b/lib/chart/widgets/chart_widget.dart @@ -18,7 +18,7 @@ class _ChartWidget extends StatelessWidget { double get _horizontalItemPadding => state.itemOptions.padding.horizontal; - double _clampWidth(double width) { + double _clampItemWidth(double width) { final minBarWidth = state.itemOptions.minBarWidth; final maxBarWidth = state.itemOptions.maxBarWidth; @@ -49,7 +49,7 @@ class _ChartWidget extends StatelessWidget { final availableWidth = frameWidth - state.defaultPadding.horizontal; final width = availableWidth / visibleItems - _horizontalItemPadding; - return _clampWidth(max(0, width)); + return _clampItemWidth(max(0, width)); } double _calcItemWidth(double frameWidth) { From 98a649d1c16e88b7e438973fe7ef438de2a0249f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Mandi=C4=87?= Date: Mon, 6 Feb 2023 09:27:02 +0100 Subject: [PATCH 17/18] fix is scrollable check --- lib/chart/model/theme/chart_behaviour.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/chart/model/theme/chart_behaviour.dart b/lib/chart/model/theme/chart_behaviour.dart index 53a9e1f..e6f9499 100644 --- a/lib/chart/model/theme/chart_behaviour.dart +++ b/lib/chart/model/theme/chart_behaviour.dart @@ -22,7 +22,7 @@ class ChartBehaviour { final ValueChanged? onItemClicked; /// Return true if chart is currently scrollable - bool get isScrollable => scrollSettings._isScrollable > 0.0; + bool get isScrollable => scrollSettings._isScrollable > 0.5; /// Animate Behaviour from one state to other static ChartBehaviour lerp(ChartBehaviour a, ChartBehaviour b, double t) { From c25f740ca6a30523210bf8a5805f9dcadf3ceffb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Mandi=C4=87?= Date: Tue, 14 Feb 2023 14:11:36 +0100 Subject: [PATCH 18/18] Move asserts to correct constructor --- lib/chart/model/theme/item_theme/item_options.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/chart/model/theme/item_theme/item_options.dart b/lib/chart/model/theme/item_theme/item_options.dart index 7d726e0..14ad1cb 100644 --- a/lib/chart/model/theme/item_theme/item_options.dart +++ b/lib/chart/model/theme/item_theme/item_options.dart @@ -29,7 +29,9 @@ abstract class ItemOptions { this.minBarWidth, this.startPosition = 0.5, required this.itemBuilder, - }); + }) : assert(maxBarWidth == null || + minBarWidth == null || + maxBarWidth >= minBarWidth); const ItemOptions._lerp({ required this.geometryPainter, @@ -40,9 +42,7 @@ abstract class ItemOptions { this.startPosition = 0.5, double multiItemStack = 1.0, required this.itemBuilder, - }) : assert(maxBarWidth == null || - minBarWidth == null || - maxBarWidth >= minBarWidth); + }); /// Item padding, if [minBarWidth] and [padding] are more then available space /// [padding] will get ignored