Skip to content

Conversation

behnam-deriv
Copy link
Collaborator

@behnam-deriv behnam-deriv commented Jun 18, 2025

Clickup link: https://app.clickup.com/t/20696747/GRWT-6233

This PR contains the following changes:

  • ✨ New feature (non-breaking change which adds functionality)
  • πŸ› οΈ Bug fix (non-breaking change which fixes an issue)
  • ❌ Breaking change (fix or feature that would cause existing functionality to change)
  • 🧹 Code refactor
  • βœ… Build configuration change
  • πŸ“ Documentation
  • πŸ—‘οΈ Chore

Developers Note (Optional)

Pre-launch Checklist (For PR creator)

As a creator of this PR:

  • ✍️ I have included clickup id and package/app_name in the PR title.
  • πŸ‘οΈ I have gone through the code and removed any temporary changes (commented lines, prints, debug statements etc.).
  • βš’οΈ I have fixed any errors/warnings shown by the analyzer/linter.
  • πŸ“ I have added documentation, comments and logging wherever required.
  • πŸ§ͺ I have added necessary tests for these changes.
  • πŸ”Ž I have ensured all existing tests are passing.

Reviewers

Pre-launch Checklist (For Reviewers)

As a reviewer I ensure that:

  • ✴️ This PR follows the standard PR template.
  • πŸͺ© The information in this PR properly reflects the code changes.
  • πŸ§ͺ All the necessary tests for this PR's are passing.

Pre-launch Checklist (For QA)

  • πŸ‘Œ It passes the acceptance criteria.

Pre-launch Checklist (For Maintainer)

  • [MAINTAINER_NAME] I make sure this PR fulfills its purpose.

Summary by Sourcery

Enable automatic and manual granularity adjustments by introducing an adaptive interval system, wrapping chart components in AutoIntervalWrapper, and integrating new configuration, callbacks, and UI controls to manage zoom-driven interval changes.

New Features:

  • Add adaptive interval feature to automatically adjust chart granularity based on zoom level
  • Introduce AutoIntervalWrapper and ZoomLevelObserver for managing auto-interval logic
  • Expose onGranularityChangeRequested callback to allow parent widgets to handle granularity updates
  • Add manual zoom in/out controls and an Adaptive Interval toggle in the example chart UI

Enhancements:

  • Provide MaybeAnimatedSwitcher for smooth transitions when granularity changes
  • Refactor toggle buttons into a reusable _buildToggle helper
  • Extend ChartAxisConfig with autoInterval settings and default zoom ranges

Documentation:

  • Add detailed documentation for the adaptive interval feature in adaptive_interval.md

Copy link

sourcery-ai bot commented Jun 18, 2025

Reviewer's Guide

This PR implements an adaptive interval (auto-interval) feature that dynamically adjusts chart time granularity based on zoom level. It introduces new configuration and observer interfaces, wraps core chart widgets in an AutoIntervalWrapper, integrates zoom-level callbacks into the XAxisModel and chart lifecycle, adds animated transitions for granularity changes, updates the example app with toggles and zoom controls, and provides documentation and public API exports for the new feature.

Sequence diagram for adaptive interval granularity suggestion flow

sequenceDiagram
    participant User as actor User
    participant Chart
    participant AutoIntervalWrapper
    participant XAxisModel
    participant ParentApp as Parent App
    User->>Chart: Zooms in/out (via UI)
    Chart->>XAxisModel: scale(zoomStep)
    XAxisModel->>AutoIntervalWrapper: onZoomLevelChanged(msPerPx, granularity)
    AutoIntervalWrapper->>ParentApp: onGranularityChangeRequested(suggestedGranularity)
    ParentApp->>Chart: Update granularity
    Chart->>XAxisModel: Update granularity
    Note over Chart,AutoIntervalWrapper: Chart re-renders with new granularity
Loading

Class diagram for adaptive interval feature integration

classDiagram
    class ChartAxisConfig {
        +bool autoIntervalEnabled
        +List<AutoIntervalZoomRange> autoIntervalZoomRanges
        +Duration autoIntervalTransitionDuration
        +copyWith(...)
    }
    class AutoIntervalZoomRange {
        +int granularity
        +double minPixelsPerInterval
        +double maxPixelsPerInterval
        +double optimalPixelsPerInterval
    }
    class AutoIntervalWrapper {
        +bool enabled
        +int granularity
        +List<AutoIntervalZoomRange> zoomRanges
        +void Function(int)? onGranularityChangeRequested
    }
    class ZoomLevelObserver {
        +void onZoomLevelChanged(double msPerPx, int currentGranularity)
    }
    class XAxisModel {
        +bool autoIntervalEnabled
        +ZoomLevelObserver? zoomLevelObserver
        +void scale(double scale)
    }
    class Chart {
        +OnGranularityChangeRequestedCallback? onGranularityChangeRequested
    }
    class DerivChart {
        +void Function(int)? onGranularityChangeRequested
    }
    ChartAxisConfig --> AutoIntervalZoomRange : uses
    AutoIntervalWrapper ..|> ZoomLevelObserver : implements
    XAxisModel --> ZoomLevelObserver : uses
    Chart --> AutoIntervalWrapper : wraps
    DerivChart --> Chart : uses
    Chart --> ChartAxisConfig : uses
    Chart --> XAxisModel : uses
    AutoIntervalWrapper --> AutoIntervalZoomRange : uses
Loading

File-Level Changes

Change Details Files
Introduce adaptive interval infrastructure
  • Define AutoIntervalZoomRange and defaultAutoIntervalRanges
  • Extend ChartAxisConfig with autoIntervalEnabled, zoomRanges, and transition duration
  • Add ZoomLevelObserver interface
  • Implement AutoIntervalWrapper widget for observing zoom levels and suggesting granularity
lib/src/models/chart_axis_config.dart
lib/src/deriv_chart/chart/auto_interval/zoom_level_observer.dart
lib/src/deriv_chart/chart/auto_interval/auto_interval_wrapper.dart
Integrate auto-interval into core chart components
  • Wrap Chart child tree in AutoIntervalWrapper and forward onGranularityChangeRequested
  • Propagate onGranularityChangeRequested through DerivChart
  • Inject ZoomLevelObserver into XAxisModel and XAxisBase
  • Trigger observer callbacks on scale and pan events
lib/src/deriv_chart/chart/chart.dart
lib/src/deriv_chart/deriv_chart.dart
lib/src/deriv_chart/chart/x_axis/x_axis_model.dart
lib/src/deriv_chart/chart/x_axis/widgets/x_axis_base.dart
Animate transitions for granularity changes
  • Introduce MaybeAnimatedSwitcher to conditionally animate child switches
  • Wrap chart CustomPaint in MaybeAnimatedSwitcher keyed by granularity
lib/src/deriv_chart/chart/basic_chart.dart
lib/src/widgets/maybe_animated_switcher.dart
Enhance example app with adaptive interval UI and controls
  • Add zoomStep constant and implement _zoomIn/_zoomOut methods and buttons
  • Add 'Adaptive interval' toggle and refactor checkbox controls into _buildToggle
  • Conditionally skip scrollToLastTick when adaptive interval is enabled
  • Hook onGranularityChangeRequested callback to update granularity
example/lib/main.dart
Update public API exports and add documentation
  • Export AutoIntervalWrapper in public library index
  • Add adaptive_interval.md documentation covering feature overview and usage
lib/deriv_chart.dart
doc/adaptive_interval.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@behnam-deriv
Copy link
Collaborator Author

@sourcery-ai review

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey @behnam-deriv - I've reviewed your changes and they look great!

Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments

### Comment 1
<location> `lib/src/deriv_chart/chart/auto_interval/auto_interval_wrapper.dart:59` </location>
<code_context>
+    implements ZoomLevelObserver {
+  late int _currentGranularity;
+  double? _currentMsPerPx;
+  int? _lastSuggestedGranularity;
+
+  @override
</code_context>

<issue_to_address>
Consider thread-safety and race conditions for _lastSuggestedGranularity.

Ensure the logic for _lastSuggestedGranularity handles rapid zoom changes and multiple post-frame callbacks, as race conditions may occur if the widget is rebuilt or disposed between scheduling and execution.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click πŸ‘ or πŸ‘Ž on each comment and I'll use the feedback to improve your reviews.

implements ZoomLevelObserver {
late int _currentGranularity;
double? _currentMsPerPx;
int? _lastSuggestedGranularity;
Copy link

Choose a reason for hiding this comment

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

issue (bug_risk): Consider thread-safety and race conditions for _lastSuggestedGranularity.

Ensure the logic for _lastSuggestedGranularity handles rapid zoom changes and multiple post-frame callbacks, as race conditions may occur if the widget is rebuilt or disposed between scheduling and execution.

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey @behnam-deriv - I've reviewed your changes - here's some feedback:

  • Consider removing the kIsWeb check in XAxisModel.scale so that zoom-level notifications fire on all platforms rather than only on web.
  • It may be helpful to debounce or throttle the onGranularityChangeRequested callback to avoid rapid successive granularity-change requests during fast or continuous zoom actions.
  • In AutoIntervalWrapper, add a fallback for zoom levels that fall outside the configured ranges so you can default to the nearest valid granularity instead of doing nothing.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- Consider removing the `kIsWeb` check in XAxisModel.scale so that zoom-level notifications fire on all platforms rather than only on web.
- It may be helpful to debounce or throttle the `onGranularityChangeRequested` callback to avoid rapid successive granularity-change requests during fast or continuous zoom actions.
- In AutoIntervalWrapper, add a fallback for zoom levels that fall outside the configured ranges so you can default to the nearest valid granularity instead of doing nothing.

## Individual Comments

### Comment 1
<location> `lib/src/deriv_chart/chart/x_axis/x_axis_model.dart:499` </location>
<code_context>
   void scale(double scale) {
     _msPerPx = (_prevMsPerPx! / scale).clamp(_minMsPerPx, _maxMsPerPx);
     onScale?.call();
+    if (kIsWeb) {
+      zoomLevelObserver?.onZoomLevelChanged(_msPerPx, _granularity);
+    }
     notifyListeners();
</code_context>

<issue_to_address>
Zoom level observer is only notified on web during scale, but not on other platforms.

Currently, zoomLevelObserver is only updated on web. If cross-platform consistency is required, review whether restricting this to kIsWeb is appropriate.
</issue_to_address>

### Comment 2
<location> `example/lib/main.dart:290` </location>
<code_context>
-      WidgetsBinding.instance.addPostFrameCallback(
-        (Duration timeStamp) => _controller.scrollToLastTick(),
-      );
+      if (!_adaptiveInterval) {
+        WidgetsBinding.instance.addPostFrameCallback(
+          (Duration timeStamp) => _controller.scrollToLastTick(),
+        );
</code_context>

<issue_to_address>
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.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click πŸ‘ or πŸ‘Ž on each comment and I'll use the feedback to improve your reviews.

_triggerScrollMomentum(details.velocity);
}

zoomLevelObserver?.onZoomLevelChanged(_msPerPx, _granularity);
Copy link

Choose a reason for hiding this comment

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

issue (bug_risk): Zoom level observer is only notified on web during scale, but not on other platforms.

Currently, zoomLevelObserver is only updated on web. If cross-platform consistency is required, review whether restricting this to kIsWeb is appropriate.


_resetCandlesTo(historyCandles);
}

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.

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey @behnam-deriv - I've reviewed your changes and they look great!

Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments

### Comment 1
<location> `lib/src/deriv_chart/chart/auto_interval/auto_interval_wrapper.dart:59` </location>
<code_context>
+    implements ZoomLevelObserver {
+  late int _currentGranularity;
+  double? _currentMsPerPx;
+  int? _lastSuggestedGranularity;
+
+  @override
</code_context>

<issue_to_address>
Consider debouncing granularity change requests to avoid rapid toggling.

Rapid zoom level changes near thresholds can cause repeated granularity updates. Adding debounce or hysteresis would reduce redundant API calls and enhance user experience.

Suggested implementation:

```
  late int _currentGranularity;
  double? _currentMsPerPx;
  int? _lastSuggestedGranularity;
  Timer? _debounceTimer;

```

```
  int? _lastSuggestedGranularity;

  void _requestGranularityChange(int newGranularity) {
    if (_debounceTimer?.isActive ?? false) {
      _debounceTimer?.cancel();
    }
    _debounceTimer = Timer(const Duration(milliseconds: 200), () {
      if (_currentGranularity != newGranularity) {
        setState(() {
          _currentGranularity = newGranularity;
        });
        // Place any additional logic for granularity change here, e.g., API calls.
      }
    });
  }

  @override
  void dispose() {
    _debounceTimer?.cancel();
    super.dispose();
  }

```

You will need to replace any direct updates to `_currentGranularity` (or wherever granularity is changed in response to zoom/thresholds) with calls to `_requestGranularityChange(newGranularity)`. This ensures all changes are debounced.
</issue_to_address>

### Comment 2
<location> `lib/src/deriv_chart/chart/x_axis/x_axis_model.dart:337` </location>
<code_context>
     _granularity = newGranularity;
-    _msPerPx = _defaultMsPerPx;
-    _scrollTo(_maxRightBoundEpoch);
+    if (!_autoIntervalEnabled) {
+      _msPerPx = _defaultMsPerPx;
+      _scrollTo(_maxRightBoundEpoch);
</code_context>

<issue_to_address>
Clearing ticks only when auto-interval is disabled may cause stale data if granularity changes while enabled.

If granularity changes with auto-interval enabled, ticks aren't cleared, which may cause mismatched data. Please confirm if this is intended or if further handling is required.
</issue_to_address>

### Comment 3
<location> `lib/src/models/chart_axis_config.dart:15` </location>
<code_context>
 const double defaultMaxCurrentTickOffset = 150;

+/// {@template auto_interval_zoom_range}
+/// Configuration for auto-interval zoom ranges.
+/// Maps granularity (in milliseconds) to the optimal pixel range for that interval.
+/// {@endtemplate}
</code_context>

<issue_to_address>
All defaultAutoIntervalRanges use the same min/max pixel values.

Consider adjusting min/max pixel values per granularity to ensure the auto-interval feature differentiates between zoom levels as intended.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click πŸ‘ or πŸ‘Ž on each comment and I'll use the feedback to improve your reviews.

implements ZoomLevelObserver {
late int _currentGranularity;
double? _currentMsPerPx;
int? _lastSuggestedGranularity;
Copy link

Choose a reason for hiding this comment

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

suggestion (performance): Consider debouncing granularity change requests to avoid rapid toggling.

Rapid zoom level changes near thresholds can cause repeated granularity updates. Adding debounce or hysteresis would reduce redundant API calls and enhance user experience.

Suggested implementation:

  late int _currentGranularity;
  double? _currentMsPerPx;
  int? _lastSuggestedGranularity;
  Timer? _debounceTimer;

  int? _lastSuggestedGranularity;

  void _requestGranularityChange(int newGranularity) {
    if (_debounceTimer?.isActive ?? false) {
      _debounceTimer?.cancel();
    }
    _debounceTimer = Timer(const Duration(milliseconds: 200), () {
      if (_currentGranularity != newGranularity) {
        setState(() {
          _currentGranularity = newGranularity;
        });
        // Place any additional logic for granularity change here, e.g., API calls.
      }
    });
  }

  @override
  void dispose() {
    _debounceTimer?.cancel();
    super.dispose();
  }

You will need to replace any direct updates to _currentGranularity (or wherever granularity is changed in response to zoom/thresholds) with calls to _requestGranularityChange(newGranularity). This ensures all changes are debounced.

_granularity = newGranularity;
_msPerPx = _defaultMsPerPx;
_scrollTo(_maxRightBoundEpoch);
if (!_autoIntervalEnabled) {
Copy link

Choose a reason for hiding this comment

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

question (bug_risk): Clearing ticks only when auto-interval is disabled may cause stale data if granularity changes while enabled.

If granularity changes with auto-interval enabled, ticks aren't cleared, which may cause mismatched data. Please confirm if this is intended or if further handling is required.

const double defaultMaxCurrentTickOffset = 150;

/// {@template auto_interval_zoom_range}
/// Configuration for auto-interval zoom ranges.
Copy link

Choose a reason for hiding this comment

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

suggestion: All defaultAutoIntervalRanges use the same min/max pixel values.

Consider adjusting min/max pixel values per granularity to ensure the auto-interval feature differentiates between zoom levels as intended.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant