|
| 1 | +import 'dart:math'; |
| 2 | +import 'dart:ui'; |
| 3 | + |
| 4 | +import 'package:flutter/material.dart'; |
| 5 | +import 'package:flutter/scheduler.dart'; |
| 6 | +import 'package:syncfusion_flutter_charts/charts.dart' hide LabelPlacement; |
| 7 | +import 'package:syncfusion_flutter_core/core.dart'; |
| 8 | +import 'package:syncfusion_flutter_core/theme.dart'; |
| 9 | +import 'package:syncfusion_flutter_sliders/sliders.dart' |
| 10 | + hide EdgeLabelPlacement; |
| 11 | + |
| 12 | +import 'chart_data.dart'; |
| 13 | + |
| 14 | +class ScrollbarAxesChart extends StatefulWidget { |
| 15 | + const ScrollbarAxesChart({super.key}); |
| 16 | + |
| 17 | + @override |
| 18 | + State<ScrollbarAxesChart> createState() => _ScrollbarAxesChartState(); |
| 19 | +} |
| 20 | + |
| 21 | +class _ScrollbarAxesChartState extends State<ScrollbarAxesChart> { |
| 22 | + late List<ChartData> _chartData; |
| 23 | + late DateTime _xScrollbarStartRange; |
| 24 | + late DateTime _xScrollbarEndRange; |
| 25 | + late NumericAxisController _yAxisController; |
| 26 | + late num _yAxisActualMin; |
| 27 | + late num _yAxisActualMax; |
| 28 | + final ValueNotifier<SfRangeValues> _yScrollbarSelectedValues = |
| 29 | + ValueNotifier(const SfRangeValues(0, 1)); |
| 30 | + |
| 31 | + final Random _random = Random(); |
| 32 | + final int _dataCount = daysInYear(2020); |
| 33 | + final ZoomPanBehavior _zoomPanBehavior = ZoomPanBehavior( |
| 34 | + enablePanning: true, |
| 35 | + enablePinching: true, |
| 36 | + enableMouseWheelZooming: true, |
| 37 | + enableSelectionZooming: true, |
| 38 | + ); |
| 39 | + |
| 40 | + RangeController? _xScrollbarController; |
| 41 | + double _baseValue = 75; |
| 42 | + Size _scrollbarSize = Size.zero; |
| 43 | + |
| 44 | + Offset _horizontalScrollbarStart = Offset.zero; |
| 45 | + Offset _verticalScrollbarStart = Offset.zero; |
| 46 | + |
| 47 | + void _updateScrollBarSize(Size size) { |
| 48 | + SchedulerBinding.instance.addPostFrameCallback((Duration timeStamp) { |
| 49 | + if (size != _scrollbarSize) { |
| 50 | + setState(() { |
| 51 | + _scrollbarSize = size; |
| 52 | + _horizontalScrollbarStart = Offset(0, size.height); |
| 53 | + _verticalScrollbarStart = Offset(size.width, size.height); |
| 54 | + }); |
| 55 | + } |
| 56 | + }); |
| 57 | + } |
| 58 | + |
| 59 | + static int daysInYear(int year) { |
| 60 | + if (isLeapYear(year)) { |
| 61 | + return 366; |
| 62 | + } else { |
| 63 | + return 365; |
| 64 | + } |
| 65 | + } |
| 66 | + |
| 67 | + static bool isLeapYear(int year) { |
| 68 | + if (year % 4 == 0) { |
| 69 | + if (year % 100 == 0) { |
| 70 | + return year % 400 == 0; |
| 71 | + } else { |
| 72 | + return true; |
| 73 | + } |
| 74 | + } else { |
| 75 | + return false; |
| 76 | + } |
| 77 | + } |
| 78 | + |
| 79 | + num _yValue() { |
| 80 | + if (_random.nextDouble() > 0.5) { |
| 81 | + _baseValue += _random.nextDouble(); |
| 82 | + return _baseValue; |
| 83 | + } else { |
| 84 | + _baseValue -= _random.nextDouble(); |
| 85 | + return _baseValue; |
| 86 | + } |
| 87 | + } |
| 88 | + |
| 89 | + @override |
| 90 | + void initState() { |
| 91 | + DateTime date = DateTime(2020); |
| 92 | + _chartData = List.generate(_dataCount + 1, (int index) { |
| 93 | + final List<num> values = [_yValue(), _yValue(), _yValue(), _yValue()]; |
| 94 | + values.sort(); |
| 95 | + return ChartData( |
| 96 | + x: date.add(Duration(days: index)), |
| 97 | + high: values[0], |
| 98 | + low: values[3], |
| 99 | + open: values[1], |
| 100 | + close: values[2], |
| 101 | + ); |
| 102 | + }); |
| 103 | + _xScrollbarStartRange = _chartData[0].x; |
| 104 | + _xScrollbarEndRange = _chartData[_dataCount].x; |
| 105 | + _xScrollbarController = RangeController( |
| 106 | + start: _xScrollbarStartRange, |
| 107 | + end: _xScrollbarEndRange, |
| 108 | + ); |
| 109 | + super.initState(); |
| 110 | + } |
| 111 | + |
| 112 | + @override |
| 113 | + void dispose() { |
| 114 | + _yScrollbarSelectedValues.dispose(); |
| 115 | + super.dispose(); |
| 116 | + } |
| 117 | + |
| 118 | + @override |
| 119 | + Widget build(BuildContext context) { |
| 120 | + return Scaffold( |
| 121 | + backgroundColor: Theme.of(context).colorScheme.onPrimary, |
| 122 | + appBar: AppBar(title: const Text('Scrollbar On Chart Axes')), |
| 123 | + body: Padding( |
| 124 | + padding: const EdgeInsets.all(15.0), |
| 125 | + child: SfCartesianChart( |
| 126 | + margin: EdgeInsets.zero, |
| 127 | + primaryXAxis: DateTimeAxis( |
| 128 | + rangeController: _xScrollbarController, |
| 129 | + edgeLabelPlacement: EdgeLabelPlacement.shift, |
| 130 | + ), |
| 131 | + primaryYAxis: NumericAxis( |
| 132 | + opposedPosition: true, |
| 133 | + rangePadding: ChartRangePadding.round, |
| 134 | + onRendererCreated: (NumericAxisController controller) { |
| 135 | + _yAxisController = controller; |
| 136 | + }, |
| 137 | + ), |
| 138 | + series: <CartesianSeries<ChartData, DateTime>>[ |
| 139 | + HiloOpenCloseSeries( |
| 140 | + dataSource: _chartData, |
| 141 | + xValueMapper: (ChartData data, int index) => data.x, |
| 142 | + highValueMapper: (ChartData data, int index) => data.high, |
| 143 | + lowValueMapper: (ChartData data, int index) => data.low, |
| 144 | + openValueMapper: (ChartData data, int index) => data.open, |
| 145 | + closeValueMapper: (ChartData data, int index) => data.close, |
| 146 | + onCreateRenderer: (ChartSeries<ChartData, DateTime> series) { |
| 147 | + return _HiloOpenCloseSeriesRenderer(this); |
| 148 | + }, |
| 149 | + ), |
| 150 | + ], |
| 151 | + zoomPanBehavior: _zoomPanBehavior, |
| 152 | + onActualRangeChanged: (ActualRangeChangedArgs args) { |
| 153 | + if (args.axisName == 'primaryYAxis') { |
| 154 | + _yAxisActualMin = args.actualMin; |
| 155 | + _yAxisActualMax = args.actualMax; |
| 156 | + SchedulerBinding.instance |
| 157 | + .addPostFrameCallback((Duration timeStamp) { |
| 158 | + final num actualRange = args.actualMax - args.actualMin; |
| 159 | + double visibleMinNormalized = |
| 160 | + (args.visibleMin - args.actualMin) / actualRange; |
| 161 | + double visibleMaxNormalized = |
| 162 | + (args.visibleMax - args.actualMin) / actualRange; |
| 163 | + _yScrollbarSelectedValues.value = |
| 164 | + SfRangeValues(visibleMinNormalized, visibleMaxNormalized); |
| 165 | + }); |
| 166 | + } |
| 167 | + }, |
| 168 | + annotations: [ |
| 169 | + CartesianChartAnnotation( |
| 170 | + x: _horizontalScrollbarStart.dx, |
| 171 | + y: _horizontalScrollbarStart.dy, |
| 172 | + coordinateUnit: CoordinateUnit.logicalPixel, |
| 173 | + horizontalAlignment: ChartAlignment.near, |
| 174 | + widget: SizedBox( |
| 175 | + width: _scrollbarSize.width, |
| 176 | + child: SfRangeSelectorTheme( |
| 177 | + data: const SfRangeSelectorThemeData( |
| 178 | + thumbRadius: 0, |
| 179 | + overlayRadius: 0, |
| 180 | + ), |
| 181 | + child: SfRangeSelector( |
| 182 | + min: _xScrollbarStartRange, |
| 183 | + max: _xScrollbarEndRange, |
| 184 | + controller: _xScrollbarController, |
| 185 | + child: const SizedBox(height: 0), |
| 186 | + ), |
| 187 | + ), |
| 188 | + ), |
| 189 | + ), |
| 190 | + CartesianChartAnnotation( |
| 191 | + x: _verticalScrollbarStart.dx, |
| 192 | + y: _verticalScrollbarStart.dy, |
| 193 | + coordinateUnit: CoordinateUnit.logicalPixel, |
| 194 | + verticalAlignment: ChartAlignment.far, |
| 195 | + widget: SizedBox( |
| 196 | + width: 6, |
| 197 | + height: _scrollbarSize.height, |
| 198 | + child: ValueListenableBuilder<SfRangeValues>( |
| 199 | + valueListenable: _yScrollbarSelectedValues, |
| 200 | + builder: (BuildContext context, SfRangeValues values, |
| 201 | + Widget? child) { |
| 202 | + return SfRangeSliderTheme( |
| 203 | + data: const SfRangeSliderThemeData( |
| 204 | + thumbRadius: 0, |
| 205 | + overlayRadius: 0, |
| 206 | + ), |
| 207 | + child: SfRangeSlider.vertical( |
| 208 | + min: 0, |
| 209 | + max: 1, |
| 210 | + values: values, |
| 211 | + onChanged: (SfRangeValues newValues) { |
| 212 | + _yAxisController.visibleMinimum = lerpDouble( |
| 213 | + _yAxisActualMin, |
| 214 | + _yAxisActualMax, |
| 215 | + newValues.start); |
| 216 | + _yAxisController.visibleMaximum = lerpDouble( |
| 217 | + _yAxisActualMin, _yAxisActualMax, newValues.end); |
| 218 | + }, |
| 219 | + ), |
| 220 | + ); |
| 221 | + }, |
| 222 | + ), |
| 223 | + ), |
| 224 | + ), |
| 225 | + ], |
| 226 | + ), |
| 227 | + ), |
| 228 | + ); |
| 229 | + } |
| 230 | +} |
| 231 | + |
| 232 | +class _HiloOpenCloseSeriesRenderer |
| 233 | + extends HiloOpenCloseSeriesRenderer<ChartData, DateTime> { |
| 234 | + _HiloOpenCloseSeriesRenderer(this._state); |
| 235 | + |
| 236 | + final _ScrollbarAxesChartState _state; |
| 237 | + |
| 238 | + @override |
| 239 | + void performLayout() { |
| 240 | + super.performLayout(); |
| 241 | + _state._updateScrollBarSize(size); |
| 242 | + } |
| 243 | +} |
0 commit comments