Skip to content

fix: quote bound animation for disjoint price range#467

Merged
behnam-deriv merged 1 commit intomasterfrom
adjust-quote-bound-animation-for-disjoint-price-ranges
Mar 10, 2026
Merged

fix: quote bound animation for disjoint price range#467
behnam-deriv merged 1 commit intomasterfrom
adjust-quote-bound-animation-for-disjoint-price-ranges

Conversation

@behnam-deriv
Copy link
Collaborator

@behnam-deriv behnam-deriv commented Mar 9, 2026

Problem

When switching markets (e.g. from EURUSD ~1.08 to XAUUSD ~2000), the chart animates the Y-axis quote bounds and the current spot indicator from the old market's price to the new market's price. Because the price ranges can differ, this creates a jarring long vertical line on screen during the transition.

Two things caused the artifact:

  1. Quote bounds animation_updateQuoteBoundTargets always called animateTo on the bound controllers, even when the new price range had no overlap with the current one.
  2. Current tick animation — the spot indicator (blinking dot + horizontal barrier) interpolates between previousLastEntry and the new last tick on every update. On a market switch the two entries are from completely different price levels, so it slides across a huge vertical gap.

Solution

Added a disjoint-range check in _updateQuoteBoundTargets. Before deciding whether to animate, the incoming [minQuote, maxQuote] range is compared with the current [bottomBoundQuoteTarget, topBoundQuoteTarget]. If the ranges have no overlap at all the data is clearly from a different market, so both transitions are skipped:

  • Quote bound controllers are set directly (.value = …) instead of calling animateTo, snapping the Y-axis immediately.
  • completeCurrentTickAnimation() is called, jumping currentTickPercent to 1.0 so the spot indicator renders at the new price without interpolating from the old one.

Normal same-market tick updates always stay within or near the current range, so they are unaffected and continue to use the smooth animateTo animation.

Old range:  [1.080, 1.090]   (EURUSD)
New range:  [2050,  2100 ]   (XAUUSD)

2050 > 1.090  →  rangeDisjoint = true  →  snap, no animation

Summary by Sourcery

Prevent jarring Y-axis and spot animations when switching between markets with disjoint price ranges.

Bug Fixes:

  • Snap quote bounds and current tick position instead of animating when the new market’s price range does not overlap the current range, avoiding long vertical-line artifacts on market switch.
  • Ignore quote bound updates when min or max quote values are NaN to avoid invalid animations.

@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Mar 9, 2026

Reviewer's guide (collapsed on small PRs)

Reviewer's Guide

Adds a disjoint price-range check to the chart’s quote-bound update logic so that when switching between markets with non-overlapping price ranges, the Y-axis bounds and current tick position snap immediately instead of animating across a large vertical gap, while preserving smooth animations for normal in-range updates.

Sequence diagram for disjoint price range update on market switch

sequenceDiagram
  participant MarketDataStream
  participant BasicChartState
  participant BottomBoundController as BottomBoundAnimationController
  participant TopBoundController as TopBoundAnimationController

  MarketDataStream->>BasicChartState: onNewQuoteRange(minQuote, maxQuote)
  activate BasicChartState
  BasicChartState->>BasicChartState: _updateQuoteBoundTargets(minQuote, maxQuote)
  alt minQuote or maxQuote is NaN
    BasicChartState-->>MarketDataStream: return (no update)
  else rangeDisjoint
    BasicChartState->>BottomBoundController: set value = minQuote
    BasicChartState->>TopBoundController: set value = maxQuote
    BasicChartState->>BasicChartState: completeCurrentTickAnimation()
    BasicChartState-->>MarketDataStream: return (snap, no animation)
  else overlapping ranges
    alt minQuote changed
      BasicChartState->>BottomBoundController: animateTo(minQuote, easeOut)
    end
    alt maxQuote changed
      BasicChartState->>TopBoundController: animateTo(maxQuote, easeOut)
    end
    BasicChartState-->>MarketDataStream: return (smooth animation)
  end
  deactivate BasicChartState
Loading

Class diagram for updated BasicChartState quote bound handling

classDiagram
  class BasicChartState {
    +double bottomBoundQuoteTarget
    +double topBoundQuoteTarget
    +AnimationController bottomBoundQuoteAnimationController
    +AnimationController topBoundQuoteAnimationController
    +double currentTickPercent
    +void _updateQuoteBoundTargets(double minQuote, double maxQuote)
    +void completeCurrentTickAnimation()
  }

  class AnimationController {
    +double value
    +void animateTo(double target, Curve curve)
  }

  class Curve

  BasicChartState --> AnimationController : uses
  BasicChartState --> Curve : uses
Loading

File-Level Changes

Change Details Files
Prevent jarring Y-axis and spot animations when switching between markets whose quote ranges do not overlap.
  • Return early if incoming min/max quotes are NaN to avoid invalid animations.
  • Introduce a disjoint-range check comparing new [minQuote, maxQuote] against current [bottomBoundQuoteTarget, topBoundQuoteTarget].
  • When the ranges are disjoint, snap bottom and top quote targets and their animation controller values directly to the new min/max quotes and complete the current tick animation to render the spot at the new price immediately.
  • For non-disjoint ranges, keep existing behavior of animating the bottom and top quote bounds only when their targets change.
lib/src/deriv_chart/chart/basic_chart.dart

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

@github-actions
Copy link

github-actions bot commented Mar 9, 2026

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

OpenSSF Scorecard

PackageVersionScoreDetails

Scanned Manifest Files

Copy link
Contributor

@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 - I've left some high level feedback:

  • Consider extracting the disjoint-range logic (rangeDisjoint) into a small helper with a brief doc comment so that the intention (market switch vs. normal update) is clearer and reusable if this check is needed elsewhere.
  • When minQuote.isNaN || maxQuote.isNaN you now return early and keep the previous bounds and tick state; double-check whether you should instead reset or clamp the controllers to a safe state to avoid the chart getting stuck with stale bounds if NaNs persist.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Consider extracting the disjoint-range logic (`rangeDisjoint`) into a small helper with a brief doc comment so that the intention (market switch vs. normal update) is clearer and reusable if this check is needed elsewhere.
- When `minQuote.isNaN || maxQuote.isNaN` you now return early and keep the previous bounds and tick state; double-check whether you should instead reset or clamp the controllers to a safe state to avoid the chart getting stuck with stale bounds if NaNs persist.

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.

@claude
Copy link

claude bot commented Mar 10, 2026

Claude finished @behnam-deriv's task —— View job


Code Review Complete

I've reviewed the changes in this PR. The fix is well-implemented and addresses the core issue effectively. Here's my detailed analysis:


High Priority

✅ Core Logic is Sound
The disjoint range check (maxQuote < bottomBoundQuoteTarget || minQuote > topBoundQuoteTarget) correctly identifies when switching to a completely different price range. The solution appropriately snaps bounds and completes the tick animation to avoid visual artifacts.

✅ NaN Guard is Good
The early return for NaN values prevents invalid animations and is placed correctly before the disjoint check.


Medium Priority

1. Consider Extracting Disjoint Logic (Sourcery-ai suggestion)

The inline disjoint check is clear enough for this use case, but if this logic might be reused elsewhere, consider extracting it:

/// Returns true if the new quote range [minQuote, maxQuote] has no overlap
/// with the current target range, indicating a market switch.
bool _isDisjointRange(double minQuote, double maxQuote) {
  return maxQuote < bottomBoundQuoteTarget || minQuote > topBoundQuoteTarget;
}

However, given that this logic appears specific to _updateQuoteBoundTargets, the current inline implementation is acceptable.

2. Edge Case: Touching Boundaries

The disjoint check uses strict inequality (< and >). Consider this edge case:

  • Current range: [1.080, 1.090]
  • New range: [1.090, 1.100]

These ranges touch at 1.090 but don't overlap. The current code will animate this transition smoothly, which is probably the desired behavior. However, if you want to snap when ranges merely touch (not just when fully disjoint), you'd need <= and >=.

Recommendation: The current behavior seems correct for most cases, but document this decision if the touching-range behavior is intentional.

3. Initial Bounds Edge Case

When the chart first loads, topBoundQuoteTarget and bottomBoundQuoteTarget are initialized from chartAxisConfig (lines 563-564). If these initial values are very different from the first data load, the disjoint check will trigger on first render. This is likely fine since it avoids an initial animation, but worth noting.


Low Priority

1. Code Removed NaN Checks From Individual Conditions

The old code checked !minQuote.isNaN and !maxQuote.isNaN before each animateTo call. The new code consolidates this into a single early return. This is cleaner and more efficient. ✅

2. Testing Coverage

I couldn't find unit tests for BasicChartState or _updateQuoteBoundTargets. Given this is a visual bug fix with specific animation behavior, consider adding widget/integration tests that verify:

  • Switching between disjoint markets snaps instantly (no animation)
  • Normal market updates continue to animate smoothly
  • NaN values don't cause crashes or invalid states

3. Documentation

The inline comment on line 329 is clear and explains the "why" well. Good work! ✅


Potential Regressions

None identified. The changes are defensive:

  • NaN guard prevents crashes
  • Disjoint check is additive (only affects market switches)
  • Normal updates (overlapping ranges) follow the same code path as before

Security & Performance

No concerns. This is purely visualization logic with no security implications. Performance impact is negligible (one additional boolean check per update).


Summary

Approve with minor suggestions. The fix is correct and well-implemented. The sourcery-ai suggestions are valid but optional - the current code is clear and maintainable as-is. The most valuable follow-up would be adding tests to prevent regression of this visual bug.

Concrete Actions (Optional):

  1. Consider adding tests for the disjoint range behavior
  2. Document the "touching ranges" edge case behavior if it comes up in the future
  3. If other parts of the codebase need similar disjoint checks, extract the helper method

@behnam-deriv behnam-deriv merged commit 4a0936f into master Mar 10, 2026
7 of 8 checks passed
@behnam-deriv behnam-deriv deleted the adjust-quote-bound-animation-for-disjoint-price-ranges branch March 10, 2026 05:44
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