Skip to content

Fix onLoadMore and the info-dialog crash; add a lockable price axis and a gesture-free chart - #2

Merged
CtrlAltDevelop merged 9 commits into
mainfrom
several-minor-issues-were-discovered
Sep 10, 2026
Merged

CtrlAltDevelop merged 9 commits into
mainfrom
several-minor-issues-were-discovered

Conversation

@CtrlAltDevelop

Copy link
Copy Markdown
Owner

Closes #1

All three items from the report, plus the intraday follow-up. Released as 2.4.0.

Fixed

onLoadMore was never called. The callback was declared, documented and
accepted, but nothing in lib/ ever invoked it — so paging in older candles
could not work however it was wired up. It is now asked at both places the
scroll is clamped, dragging and flinging, with the edge latched: a drag held
against the edge asks once when it arrives rather than on every frame, and asks
again after coming away and going back. true at the newest candle and false
at the oldest, as documented.

The long-press readout threw when turned off and on again. It listened to a
single-subscription stream from a subtree only mounted while showInfoDialog is
set, so setting it back to true made a second listen and threw
Bad state: Stream has already been listened to. Because that landed while the
enclosing Stack was mounting its children, callers saw
LateInitializationError: Field '_children' has not been initialized instead —
which is what the report pasted. The controller is a broadcast one now.

A ValueNotifier looked like the tidier fix, but the painter emits from inside
paint, so notifying synchronously schedules a build during the frame. The
stream's async delivery was load-bearing.

Added

lockPriceScale holds the price axis at one range instead of refitting it to
the candles in the window. This is what "fixed priceAxis when scroll" turned out
to mean: scrolling rescaled the axis, so dragging back through a trend changed
every number on it.

at right edge : min=609.0 max=699.0
scrolled back : min=463.0 max=577.0

Locked, the candles move under a scale that stays put. It locks onto what is
already on screen, so turning it on does not move the chart, and
resetPriceScale hands the axis back. The lock is applied where the axis scale
is derived, not where the range is measured, so mMainMaxValue/mMainMinValue
stay the honest window fit — which keeps the high/low markers on the candles that
set them, lets a zoom work off the locked range, and means a reset captures a
real fit rather than the value it just cleared.

ChartStyle.priceAxisWidth holds a gutter back for the labels on whichever
side verticalTextAlignment puts them, so candles, grid, panes and the date axis
stop short of it instead of sliding under the numbers. The plot is clipped to its
own bounds, and pressing the labels grabs the scale as pressing the axis strip
always has.

scrollEnabled and zoomEnabled turn the chart's own gestures off, for the
intraday chart asked about in the follow-up — one session, drawn as an area,
sitting still. Both belong off together: zooming out narrows the candles, which
leaves room to scroll into, so a chart with only scrolling off can be pinched
back into a scrollable one. Both hold the user back and leave
KChartController alone, as priceScaleDrag already did.

All four default to off/unchanged, so existing callers are untouched.

Notes

  • priceAxisWidth is the riskiest change here. Making the plot narrower than
    the canvas meant auditing every "left edge is 0" and "right edge is mWidth"
    assumption — 21 sites. One was a real bug I introduced and caught: the
    candle-drawing transform did not include the plot origin, so with the axis on
    the left everything drew shifted by the gutter width while the crosshair and
    drawings used the correct coordinates. Fixed, with a test pinning the transform.
  • A locked axis holds its range until reset. I deliberately dropped an
    auto-clear-on-instrument-change heuristic: the cheap version misclassifies
    paged-in history as a new symbol, and the correct version costs an O(n) scan per
    tick, against the grain of the 2.3.1 performance work. Documented instead — call
    resetPriceScale() when switching instruments.
  • example/lib/intraday_demo.dart is a second entry point for checking the
    gesture-free chart by hand: flutter run -t lib/intraday_demo.dart.
  • The screenshot and doc-figure work (a681cff) came from a separate effort on
    this branch, not from this one — 6 new images and example/tool/screenshots.dart.

Verification

  • flutter test868 pass, from an 821 baseline; 47 added across four files
  • flutter analyze — clean, package and example
  • dart format — 160 files, 0 changed
  • pana160/160 pub points (was 150; one unformatted getter in
    main_renderer.dart was costing 10)
  • Both bug fixes were verified against the pre-fix code: 7 of 9 new tests fail
    there, including the exact Bad state: Stream has already been listened to
  • flutter build ios --simulator — builds, the reporter's platform

…toggled

onLoadMore was declared and documented but never called: nothing in lib/
invoked it, so paging in older candles could not work. It is now asked at
both clamp sites -- the pan handler and the fling listener -- with an edge
latch, so a drag pinned against an edge asks once on arrival rather than on
every frame, and asks again after coming away and back.

The info dialog listened to a single-subscription stream from a subtree that
is only mounted while showInfoDialog is set. Toggling the flag true -> false
-> true made a second listen on a stream already listened to, which threw
'Bad state: Stream has already been listened to' as the dialog remounted;
that throw left the enclosing Stack's children unassigned, surfacing to
callers as a LateInitializationError on '_children'. The controller is now a
broadcast one, which also keeps delivery asynchronous -- the painter emits
from inside paint, so a synchronous notifier would schedule a build during
the frame.
ChartStyle.priceAxisWidth reserves a strip on the price axis side -- the side
verticalTextAlignment puts the labels on -- and the candles, grid, indicator
panes and date axis are all laid out inside what is left. The labels sit in the
gutter on their own, so the axis reads cleanly however far the chart is
scrolled, rather than candles sliding under the numbers.

mWidth is now the width of the plot, with mCanvasWidth for the whole canvas and
mPlotLeft/mPlotRight for its edges. The coordinate transforms carry the plot
origin, so the crosshair, the drawings, alerts and hit-testing follow the
gutter without knowing about it, and the plot is clipped to its own bounds so
nothing spills into the axis. Everything that was measured from the canvas
edges -- the now-price level and its tag, the crosshair readouts, the high and
low callouts, the trading and signal levels, the watermark, the date labels and
the pane a press lands in -- is measured from the plot's instead. Pressing the
labels grabs the scale, as pressing the axis strip always has.

Left at 0, the default, none of this changes what is drawn: the labels stay
over the candles and the plot stays the full canvas, which the existing suite
and the goldens confirm.
The axis fits the candles in the window, so scrolling rescales it: dragging
back through a trend changed every number on it. lockPriceScale holds it at one
range instead, and the candles move under a scale that stays put.

The lock is applied where the axis scale is worked out rather than where the
range is measured, so mMainMaxValue and mMainMinValue stay the honest fit to
the window. That keeps the window's high and low pointing at the candles that
set them, lets a zoom or a drag work from the locked range, and means the range
captured when the lock takes hold is a real fit rather than the last locked
value -- without which resetPriceScale could never refit, since it would
recapture what it had just cleared.

It locks onto the range already on screen, so turning it on does not move the
chart. resetPriceScale hands the axis back: it refits to the window and holds
there afresh. The range is held until reset, so a chart switching instruments
should reset it; paging in candles and live ticks need nothing, which is what
the lock is for.
The screenshot tool now takes films as well as stills: a scene whose
shutter is called over and over, written out as one looping GIF at 1x,
where a still would not show the movement. Bar replay gets one, driven
through a transport bar built the way an app would build it.

The stills it was missing are here too — the date axis, the legend and
crosshair, sizing and theming — and every doc page now opens on the
figure it describes.
A locked price axis and a gutter for its labels, and the two reported bugs --
onLoadMore never being called, and the long-press readout throwing when
showInfoDialog was set back to true.
scrollEnabled and zoomEnabled turn the chart's own gestures off, for one meant
to show a single fixed stretch rather than be navigated -- an intraday session,
a thumbnail, a figure in a report. With scrolling off a drag neither slides the
window nor flings it, and onLoadMore is never asked, since no edge is reached.
With zooming off a pinch does nothing and the web and desktop zoom slider is
left off too.

The two belong together: zooming out narrows the candles, which leaves the
window room to scroll into, so a chart with only scrolling off could be pinched
back into a scrollable one. Both hold the user back and leave the controller
alone, as priceScaleDrag already did.
A second entry point for the example, showing the other kind of chart: one
session drawn as an area, sitting still. scrollEnabled and zoomEnabled off,
and pointWidth set to the width over the candle count so the whole session
fits.

Two switches flip both back on, so the difference can be felt rather than
read -- the pinch especially, since zooming out is what would otherwise hand
back the scrolling that scrollEnabled took away.
The only thing between the package and 160 pub points: pana runs
`dart format --set-exit-if-changed` over the whole package, and this one
getter had drifted out of shape -- written against an older formatter, and
left behind when it changed how it wraps a cascaded `??=`.

Nothing but whitespace and where the lines break.
The benchmark and the render-perf test had drifted out of the formatter's
shape. Neither is scored -- pana only weighs lib/ -- but there is no reason for
the repository to disagree with `dart format` about anything.

The gutter test's paint helper returned a record with an `xs` field that was
always empty and never read, left over from an earlier way of checking where
things landed. It hands back the counting canvas itself now, which is all any
caller wanted.
@CtrlAltDevelop CtrlAltDevelop self-assigned this Sep 10, 2026
@CtrlAltDevelop CtrlAltDevelop added bug Something isn't working documentation Improvements or additions to documentation labels Sep 10, 2026
@CtrlAltDevelop CtrlAltDevelop linked an issue Sep 10, 2026 that may be closed by this pull request
@CtrlAltDevelop
CtrlAltDevelop merged commit d2f4b3a into main Sep 10, 2026
1 check passed
@CtrlAltDevelop
CtrlAltDevelop deleted the several-minor-issues-were-discovered branch September 10, 2026 10:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working documentation Improvements or additions to documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Several minor issues were discovered.

1 participant