Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 120 additions & 0 deletions doc/adaptive_interval.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
# Adaptive Interval (Auto-Interval) Feature

## Overview

The **adaptive interval** (also called **auto-interval**) feature automatically adjusts the chart's time granularity (interval) based on the current zoom level. This ensures that the chart remains readable and usable at all zoom levels, without requiring the user to manually select the most appropriate interval.

When enabled, the chart will suggest and can switch to the most suitable granularity as the user zooms in or out, based on a configurable mapping between zoom levels and time intervals.

---

## Key Components

- **`AutoIntervalWrapper`**: A widget that observes zoom level changes and manages the logic for suggesting granularity changes.
- **`ZoomLevelObserver`**: An interface for components that need to react to zoom level changes.
- **`AutoIntervalZoomRange`**: A configuration class that maps each granularity (in milliseconds) to its optimal pixel range on the chart.
- **`ChartAxisConfig`**: Holds the configuration for enabling/disabling auto-interval and its zoom ranges.

---

## How It Works

1. **Zoom Level Observation**: The chart's x-axis model notifies the `AutoIntervalWrapper` of zoom level changes (in milliseconds per pixel).
2. **Optimal Granularity Calculation**: The wrapper calculates the optimal granularity for the current zoom level using the configured `AutoIntervalZoomRange` list.
3. **Granularity Suggestion**: If a different granularity is optimal and hasn't already been suggested, the wrapper triggers the `onGranularityChangeRequested` callback.
4. **Granularity Update**: The parent widget or chart controller can then update the chart's granularity, which will fetch and display data at the new interval.

---

## Configuration

- **Enable/Disable**: Set `autoIntervalEnabled` in `ChartAxisConfig` to `true` or `false`.
- **Customize Ranges**: Modify `autoIntervalZoomRanges` in `ChartAxisConfig` to change which granularities are used at which zoom levels.
- **Default Ranges**: The default configuration covers common trading timeframes (1m, 2m, 5m, etc.), each with a pixel range for when it should be used.

Example:
```dart
ChartAxisConfig(
autoIntervalEnabled: true,
autoIntervalZoomRanges: [
AutoIntervalZoomRange(
granularity: 60000, // 1 minute
minPixelsPerInterval: 12,
maxPixelsPerInterval: 24,
),
// ... more ranges ...
],
)
```

---

## Usage Example

Wrap your chart widget with `AutoIntervalWrapper`:

```dart
AutoIntervalWrapper(
enabled: true,
granularity: currentGranularity, // in milliseconds
zoomRanges: autoIntervalRanges, // List<AutoIntervalZoomRange>
onGranularityChangeRequested: (newGranularity) {
// Update chart granularity and fetch new data
},
child: ... // your chart widget
)
```

- The `onGranularityChangeRequested` callback is called when the wrapper suggests a new optimal granularity.
- You are responsible for updating the chart's granularity and fetching new data as needed.

---

## Internal Logic

- The wrapper listens for zoom level changes (ms per pixel) via the `ZoomLevelObserver` interface.
- For each zoom event, it calculates the number of pixels each interval (candle) would occupy at the current zoom.
- It checks all configured `AutoIntervalZoomRange` entries to find which range the current zoom fits into, and selects the one closest to its optimal pixel width.
- If the optimal granularity is different from the current one and hasn't already been suggested, it triggers the callback.

---

## Example Integration

```dart
ChartAxisConfig config = ChartAxisConfig(
autoIntervalEnabled: true,
);

Chart(
chartAxisConfig: config,
granularity: granularity,
onGranularityChangeRequested: (int suggestedGranularity) {
// Convert ms to seconds for API call if needed
final int seconds = suggestedGranularity ~/ 1000;
if (seconds != granularity) {
// Update granularity and fetch new data
setState(() => granularity = seconds);
fetchDataWithGranularity(seconds);
}
},
// ... other chart params ...
)
```

---

## Notes

- The adaptive interval feature is especially useful for financial charts where users frequently zoom in and out to analyze data at different timeframes.
- You can fully customize the mapping between zoom levels and granularities to suit your application's needs.
- The feature is opt-in and can be toggled at runtime.

---

## Related Classes and Files
- `lib/src/deriv_chart/chart/auto_interval/auto_interval_wrapper.dart`
- `lib/src/deriv_chart/chart/auto_interval/zoom_level_observer.dart`
- `lib/src/models/chart_axis_config.dart`
- `lib/src/deriv_chart/chart/x_axis/x_axis_model.dart`
- Example usage: `example/lib/main.dart`
117 changes: 98 additions & 19 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -80,13 +80,16 @@ class _FullscreenChartState extends State<FullscreenChart> {
static const String defaultAppID = '36544';
static const String defaultEndpoint = 'blue.derivws.com';

// Add zoom step constant
static const double zoomStep = 1.2; // 20% zoom in/out per button press

List<Tick> ticks = <Tick>[];
ChartStyle style = ChartStyle.line;
int granularity = 0;

final List<Barrier> _sampleBarriers = <Barrier>[];
HorizontalBarrier? _slBarrier, _tpBarrier;
bool _sl = false, _tp = false;
bool _sl = false, _tp = false, _adaptiveInterval = true;

TickHistorySubscription? _tickHistorySubscription;

Expand Down Expand Up @@ -284,9 +287,11 @@ class _FullscreenChartState extends State<FullscreenChart> {

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: scrollToLastTick is skipped when adaptive interval is enabled, which may affect user experience.

If auto-scrolling to the latest tick is still desired with adaptive interval enabled, consider decoupling these behaviors.

_updateSampleSLAndTP();

WidgetsBinding.instance.addPostFrameCallback(
(Duration timeStamp) => _controller.scrollToLastTick(),
);
if (!_adaptiveInterval) {
WidgetsBinding.instance.addPostFrameCallback(
(Duration timeStamp) => _controller.scrollToLastTick(),
);
}
} on BaseAPIException catch (e) {
dev.log(e.message!, error: e);
} finally {
Expand Down Expand Up @@ -433,6 +438,38 @@ class _FullscreenChartState extends State<FullscreenChart> {
_loadHistory(500);
}
},
chartAxisConfig: ChartAxisConfig(
autoIntervalEnabled: _adaptiveInterval,
autoIntervalTransitionDuration:
Duration(milliseconds: 480),
),
onGranularityChangeRequested: (int suggestedGranularity) {
print(
'Auto-interval suggests granularity: ${suggestedGranularity}ms');

// Only proceed if adaptive interval is enabled
if (!_adaptiveInterval) {
print(
'Adaptive interval is disabled, ignoring suggestion');
return;
}

// Convert ms to seconds for the API call
final int suggestedGranularitySeconds =
suggestedGranularity ~/ 1000;

print(
'Converting to seconds: ${suggestedGranularitySeconds}s');

// Update the granularity if it's different
if (suggestedGranularitySeconds != granularity) {
print(
'Updating granularity from ${granularity}s to ${suggestedGranularitySeconds}s');

// Update the granularity and fetch new data
_onIntervalSelected(suggestedGranularitySeconds);
}
},
),
),
// ignore: unnecessary_null_comparison
Expand All @@ -450,6 +487,20 @@ class _FullscreenChartState extends State<FullscreenChart> {
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Row(
children: [
IconButton(
icon: const Icon(Icons.zoom_out),
onPressed: _zoomOut,
tooltip: 'Zoom Out',
),
IconButton(
icon: const Icon(Icons.zoom_in),
onPressed: _zoomIn,
tooltip: 'Zoom In',
),
],
),
IconButton(
icon: const Icon(Icons.settings),
onPressed: () async {
Expand Down Expand Up @@ -562,20 +613,15 @@ class _FullscreenChartState extends State<FullscreenChart> {
height: 64,
child: Row(
children: <Widget>[
Expanded(
child: CheckboxListTile(
value: _sl,
onChanged: (bool? sl) => setState(() => _sl = sl!),
title: const Text('Stop loss'),
),
),
Expanded(
child: CheckboxListTile(
value: _tp,
onChanged: (bool? tp) => setState(() => _tp = tp!),
title: const Text('Take profit'),
),
),
_buildToggle(
'Stop loss', _sl, () => setState(() => _sl = !_sl)),
_buildToggle(
'Take profit', _tp, () => setState(() => _tp = !_tp)),
_buildToggle(
'Adaptive interval',
_adaptiveInterval,
() => setState(
() => _adaptiveInterval = !_adaptiveInterval)),
],
),
)
Expand Down Expand Up @@ -754,7 +800,9 @@ class _FullscreenChartState extends State<FullscreenChart> {
_requestCompleter = Completer<dynamic>();

setState(() {
ticks.clear();
if (!_adaptiveInterval) {
ticks.clear();
}
_clearMarkers();
_clearBarriers();
});
Expand Down Expand Up @@ -843,4 +891,35 @@ class _FullscreenChartState extends State<FullscreenChart> {
authEndpoint: '',
);
}

Widget _buildToggle(String label, bool value, VoidCallback onToggle) {
return Expanded(
child: CheckboxListTile(
title: Text(
label,
style: const TextStyle(fontSize: 14),
),
value: value,
onChanged: (_) => onToggle(),
controlAffinity: ListTileControlAffinity.leading,
contentPadding: EdgeInsets.zero,
dense: true,
),
);
}

// Add zoom methods
void _zoomIn() {
final double? currentScale = _controller.scale(zoomStep);
if (currentScale != null) {
debugPrint('Zoomed in, new msPerPx: $currentScale');
}
}

void _zoomOut() {
final double? currentScale = _controller.scale(1 / zoomStep);
if (currentScale != null) {
debugPrint('Zoomed out, new msPerPx: $currentScale');
}
}
}
1 change: 1 addition & 0 deletions lib/deriv_chart.dart
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export 'src/add_ons/indicators_ui/zigzag_indicator/zigzag_indicator_config.dart'
export 'src/add_ons/repository.dart';
export 'src/deriv_chart/interactive_layer/interactive_layer_export.dart';
export 'src/deriv_chart/chart/chart.dart';
export 'src/deriv_chart/chart/auto_interval/auto_interval_wrapper.dart';
export 'src/deriv_chart/chart/data_visualization/annotations/barriers/accumulators_barriers/accumulators_active_contract.dart';
export 'src/deriv_chart/chart/data_visualization/annotations/barriers/accumulators_barriers/accumulators_closed_indicator.dart';
export 'src/deriv_chart/chart/data_visualization/annotations/barriers/accumulators_barriers/accumulators_entry_spot_barrier.dart';
Expand Down
Loading
Loading