Skip to content

Commit d2f4b3a

Browse files
Fix: onLoadMore and the info-dialog crash; add a lockable price axis and a gesture-free chart
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.** An auto-clear-on-instrument-change heuristic was deliberately left out: 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 the gesture-free chart: `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 test` — **868 pass**, from an 821 baseline; 47 added across four files - `flutter analyze` — clean, package and example - `dart format` — 160 files, 0 changed - `pana` — **160/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` - **Checked by hand on iOS**, on device — all four confirmed: `onLoadMore` pages in older candles at the oldest edge, `showInfoDialog` survives being toggled off and on, the locked axis holds its range while scrolling, and the static chart ignores drag, flick and pinch alike
2 parents e3c328f + 77f847a commit d2f4b3a

41 files changed

Lines changed: 2280 additions & 151 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,69 @@
1+
## 2.4.0
2+
3+
### A chart that sits still
4+
5+
- **New `scrollEnabled` and `zoomEnabled` turn the chart's own gestures off**,
6+
for a chart that is meant to show one fixed stretch rather than be navigated
7+
— an intraday session, a thumbnail, a printed figure. With `scrollEnabled`
8+
off a drag neither slides the window nor flings it, and `onLoadMore` is never
9+
asked for more candles, since no edge is ever reached. With `zoomEnabled` off
10+
a pinch does nothing, and the zoom slider — only ever shown on the web and on
11+
desktop, where there is no pinch — is left off too.
12+
- They are worth turning off together. Zooming out makes the candles narrower,
13+
which leaves the window room to scroll into, so a chart that only had
14+
`scrollEnabled` off could be pinched back into a scrollable one.
15+
- Both hold the user back and leave your own code alone, the way
16+
`priceScaleDrag` already did: `zoomIn`, `zoomOut`, `setChartScale`,
17+
`goToIndex`, `fitAll` and the rest of `KChartController` still work.
18+
- For a chart drawn at a fixed position, give `ChartStyle.pointWidth` roughly
19+
the chart's width divided by the number of candles. Once the whole series
20+
fits, there is nowhere to scroll to even before the flag.
21+
22+
### Price axis
23+
24+
- **New `lockPriceScale` keeps the axis still while the chart scrolls.** The
25+
axis fits the candles in the window, so scrolling rescaled it: dragging back
26+
through a trend changed every number on it. Locked, it holds the range it was
27+
already showing and the candles move under a scale that stays where it is —
28+
which is what reading a level off the axis while scrolling needs, and what
29+
paging in history through `onLoadMore` needs in order not to jump. It locks
30+
onto what is already on screen, so turning it on does not move the chart, and
31+
`resetPriceScale` hands the axis back: it refits to the window and holds there
32+
afresh.
33+
- Only the scale is held. The window's own high and low are still measured, so
34+
their markers keep pointing at the candles that set them, and a locked axis
35+
can still be dragged and zoomed — from the range it is held at rather than
36+
the window's. The range is held until it is reset, so a chart that switches to
37+
another instrument should reset it; paging in candles and live ticks need
38+
nothing, which is the point.
39+
- **New `ChartStyle.priceAxisWidth` holds a gutter back for the labels.** It is
40+
taken off whichever side `verticalTextAlignment` puts them on, and the
41+
candles, the grid, the indicator panes and the date axis all stop short of it,
42+
so the labels sit in the gutter on their own instead of candles sliding under
43+
the numbers. The plot is clipped to its own bounds, so nothing spills into the
44+
gutter, and pressing the labels grabs the scale the way pressing the axis
45+
strip always has. Left at 0, the default, nothing changes: the labels are
46+
drawn over the candles exactly as before.
47+
48+
### Fixed
49+
50+
- **`onLoadMore` is called again.** The callback was declared, documented and
51+
accepted, but nothing in the package ever invoked it, so paging in older
52+
candles could not work however it was wired up. It is now asked at both
53+
places the scroll is clamped — dragging and flinging — with the edge latched,
54+
so a drag held against the edge asks once when it arrives rather than on
55+
every frame, and asks again after coming away and going back. The flag is
56+
`true` at the newest candle and `false` at the oldest, as documented.
57+
58+
- **The long-press readout can be turned off and on again.** It listened to a
59+
single-subscription stream from a subtree that is only mounted while
60+
`showInfoDialog` is set. Setting it back to `true` made a second listen on a
61+
stream already listened to, which threw `Bad state: Stream has already been
62+
listened to` as the readout remounted — and because that throw landed while
63+
the enclosing `Stack` was mounting its children, what callers actually saw was
64+
`LateInitializationError: Field '_children' has not been initialized`. The
65+
controller is a broadcast one now.
66+
167
## 2.3.1
268

369
### Performance

README.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ Named for the open-high-low-close-volume bars it renders.
3939
- **Sub-charts**`MACD`, `KDJ`, `RSI`, `WR`, `CCI`, `ATR`, `OBV`, `MFI`, `DMI`, Aroon, Stochastic RSI, `ROC`, `TRIX`, a volume average and the Awesome oscillator, each in its own stacked pane — resizable and reorderable by dragging.
4040
- **Linear, logarithmic, percentage or indexed-to-100 price axis**, so a decade of compounding reads as well as an afternoon — invertible, with the window's high, low and average close markable on it.
4141
- **A price scale you can drag** — pull the labels to stretch or flatten the candles, drag the chart to slide the window, double-tap to fit it back; from code as well, through the controller.
42+
- **A price axis that can be pinned**`lockPriceScale` holds it at one range instead of refitting it to whatever is on screen, so scrolling moves the candles under a scale that stays put rather than rescaling every number on the axis; and `priceAxisWidth` holds a gutter back for the labels, so the candles stop short of them instead of sliding underneath.
4243
- **Axes that read as round numbers** — both axes choose their values first and are ruled where the labels fall, so a price scale steps `69000, 69500, 70000` and an intraday date axis lands on the hour, never on whatever value happened to fall on an evenly spaced pixel.
4344
- **29 drawing tools** — levels and rays, trend lines, arrows, extended lines, boxes, ellipses, triangles, parallel channels, pitchforks, Gann fans and boxes, four Fibonacci tools, a regression trend with its bands, XABCD patterns, multi-leg paths, price and date brackets, a measuring tool, planned positions with their risk-to-reward, notes, callouts, flags and freehand strokes; each placed by tap or drag, then dragged, locked, hidden and removed.
4445
- **A line editor** that opens on selection: colour, opacity, thickness, solid/dashed/dotted stroke, fill, label text and visibility, alerts, lock and delete — every option list, control and pixel of it configurable through `DrawingStyle`.
@@ -54,6 +55,7 @@ Named for the open-high-low-close-volume bars it renders.
5455
- **Higher timeframes** — a daily moving average on a fifteen-minute chart, through `TimeframeIndicator`; each candle reads the last higher-timeframe bar that had closed when it opened, so the line never repaints and never shows a value from its own future.
5556
- **A right-click menu** on the chart and on any drawing — coordinates, duplicate, restack, lock, hide, alert, delete; paste, select all, fit the scale, undo — with your own items added through one builder.
5657
- **Crosshair on hover** and an **OHLC legend** above the chart, which is how a chart reads on a desktop.
58+
- **Or no interaction at all**`scrollEnabled` and `zoomEnabled` turn the chart's own gestures off, for an intraday session, a thumbnail or a figure that is meant to sit still rather than be navigated; your own code can still drive it.
5759
- **Driven from your own code**`KChartController` zooms, scrolls back to the live candle and hands you the chart as a PNG.
5860
- **The visible window, read and set** — which candles are on screen, go-to-date, fit-to-screen, and a callback whenever it moves.
5961
- **An overview strip**`ChartOverview` draws the whole history under the chart with the visible window lit; drag it to scrub, drag its edges to zoom, tap to jump.
@@ -76,11 +78,13 @@ Named for the open-high-low-close-volume bars it renders.
7678

7779
![Two linked charts sharing one crosshair](https://raw.githubusercontent.com/CtrlAltDevelop/ohlcv_chart/main/screenshots/linked-charts.png)
7880

81+
![Bar replay stepping the market forward a candle at a time](https://raw.githubusercontent.com/CtrlAltDevelop/ohlcv_chart/main/screenshots/bar-replay.gif)
82+
7983
## Install
8084

8185
```yaml
8286
dependencies:
83-
ohlcv_chart: ^2.2.0
87+
ohlcv_chart: ^2.4.0
8488
```
8589
8690
## Quick start
@@ -117,7 +121,7 @@ One page per feature, in [`doc/`](doc/README.md):
117121
| [Indicators](doc/indicators.md) | 31 of them as instances, the catalogue, pane scales, chaining, higher timeframes, alerts |
118122
| [Comparing a second instrument](doc/comparison.md) | Other series over the same window, rebased or at their own prices |
119123
| [Chart types](doc/chart-types.md) | Eight ways to draw a series, and six transforms of the candles |
120-
| [Price axis](doc/price-axis.md) | Linear, log, percentage or indexed to 100; inverting and dragging it |
124+
| [Price axis](doc/price-axis.md) | Linear, log, percentage or indexed to 100; inverting, dragging, locking it, and its gutter |
121125
| [The date axis](doc/date-axis.md) | Round time values, and taking the axis over yourself |
122126
| [The legend and the crosshair](doc/legend-and-crosshair.md) | The OHLC row, and the crosshair on hover |
123127
| [The long-press readout](doc/readout.md) | The card over a held candle, and your own builder |
@@ -126,7 +130,7 @@ One page per feature, in [`doc/`](doc/README.md):
126130
| [Orders and positions](doc/orders-and-positions.md) | Live lines from your venue, tagged and draggable |
127131
| [Event marks](doc/event-marks.md) | Earnings, dividends, splits and news under the candles |
128132
| [Sessions and time zones](doc/sessions.md) | Day dividers, display time zone, extended hours, per-bar colour |
129-
| [Driving the chart](doc/driving-the-chart.md) | `KChartController`, and the visible window |
133+
| [Driving the chart](doc/driving-the-chart.md) | `KChartController`, the visible window, and a chart with no gestures |
130134
| [Panes](doc/panes.md) | Stacking, resizing and reordering the indicator panes |
131135
| [Bar replay](doc/bar-replay.md) | Rewind, then step or play the market forward |
132136
| [Sizing](doc/sizing.md) | How the candle area and the panes divide the height |

doc/README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,9 @@ For installation, the feature list and support, see the
2525
## Axes and reading the chart
2626

2727
- **[Price axis](price-axis.md)** — linear, logarithmic, percentage or indexed
28-
to 100; inverting it, marking the window's high, low and average close, and
29-
dragging the scale by hand.
28+
to 100; inverting it, marking the window's high, low and average close,
29+
dragging the scale by hand, locking it so scrolling cannot rescale it, and
30+
holding a gutter back for its labels.
3031
- **[The date axis](date-axis.md)** — round time values, the formats it picks
3132
between, and taking it over with `dateFormatter`.
3233
- **[The legend and the crosshair](legend-and-crosshair.md)** — the OHLC row
@@ -55,7 +56,8 @@ For installation, the feature list and support, see the
5556
## Driving it from your code
5657

5758
- **[Driving the chart](driving-the-chart.md)**`KChartController` for zoom,
58-
scroll and a PNG of the chart, plus reading and setting the visible window.
59+
scroll and a PNG of the chart, reading and setting the visible window, and
60+
turning the chart's own gestures off for one that should sit still.
5961
- **[Panes](panes.md)** — stacking, resizing and reordering the indicator panes.
6062
- **[Bar replay](bar-replay.md)** — rewind to any candle and step or play the
6163
market forward, with the indicators only knowing what has arrived.

doc/bar-replay.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ the chart at a candle in the past: everything after it — the candles, the
55
indicators computed from them, the now-price line and the legend — is as it was
66
at that moment, so a setup can be studied without the answer already on screen.
77

8+
![The replay running, a candle at a time](https://raw.githubusercontent.com/CtrlAltDevelop/ohlcv_chart/main/screenshots/bar-replay.gif)
9+
810
```dart
911
final replay = ChartReplayController(interval: const Duration(milliseconds: 300));
1012
@@ -31,6 +33,8 @@ candle having been picked first, and it gives up on its own at the newest one
3133
drive a transport bar from. It is a `ChangeNotifier`, so those buttons rebuild
3234
themselves; dispose it with the widget that owns it.
3335

36+
![The chart held at the 150th candle of 420, under a transport bar](https://raw.githubusercontent.com/CtrlAltDevelop/ohlcv_chart/main/screenshots/bar-replay.png)
37+
3438
Nothing is thrown away while a replay runs: the candle list is untouched and
3539
the drawings stay where they were placed, including any on candles still to
3640
arrive.

doc/candlestick-chart.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# Candlestick chart
22

3+
![Candles with moving averages, volume and MACD](https://raw.githubusercontent.com/CtrlAltDevelop/ohlcv_chart/main/screenshots/candles.png)
4+
35
Feed it a `List<KLineEntity>`. Indicator values are computed in place by
46
`DataUtil.calculate` before the first paint, and again whenever new candles arrive:
57

@@ -26,6 +28,12 @@ KChartWidget(
2628
`KLineEntity.fromJson` accepts the usual OHLCV shape (`open`, `high`, `low`,
2729
`close`, `vol`, `time`/`id`), or build the entity directly.
2830

31+
`onLoadMore` fires when the scroll lands on an edge — `false` at the oldest
32+
candle, `true` at the newest — once when it arrives rather than on every frame
33+
the drag spends there, and again if the user comes away and goes back. Prepend
34+
the older candles you fetch and hand the chart the longer list; it keeps its
35+
place in the data, so the window does not jump.
36+
2937
---
3038

3139
[← All docs](README.md) · [Package README](../README.md)

doc/date-axis.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ ends and the next begins. Labels that would crowd into each other are dropped
77
rather than printed over one another, and the boundaries follow the clock the
88
chart prints: a `timeZoneOffset` of half an hour still labels round local times.
99

10+
![The axis the chart picks, and the same candles under a dateFormatter](https://raw.githubusercontent.com/CtrlAltDevelop/ohlcv_chart/main/screenshots/date-axis.png)
11+
1012
`ChartStyle.gridColumns` sets the density, read like `gridRows`. Formatting can
1113
be taken over completely — `ChartStyle.dateTimeFormat` for a fixed pattern, or
1214
`dateFormatter` for full control, which is handed each candle along with a flag
@@ -21,7 +23,7 @@ KChartWidget(
2123
timeFrame: const Duration(minutes: 15),
2224
dateFormatter: (candle, longForm) => DateFormat(
2325
longForm ? 'EEE d MMM HH:mm' : 'HH:mm',
24-
).format(DateTime.fromMillisecondsSinceEpoch(candle.time!)),
26+
).format(candle.dateTime!),
2527
xFrontPadding: 120,
2628
);
2729
```

doc/driving-the-chart.md

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,73 @@ KChartWidget(
7676
`indexRangeCovering` and `indexNearest` are exported for working out either
7777
from a list of candles without a chart in hand.
7878

79+
## Turning the gestures off
80+
81+
Some charts are not meant to be navigated: an intraday session, a thumbnail in
82+
a list, a figure in a report. `scrollEnabled` and `zoomEnabled` take the
83+
chart's own gestures away.
84+
85+
```dart
86+
KChartWidget(
87+
sessionCandles,
88+
ChartColors(),
89+
isTrendLine: false,
90+
watermarkAssetPath: 'assets/logo.svg',
91+
timeFrame: const Duration(minutes: 5),
92+
chartType: ChartType.area,
93+
scrollEnabled: false,
94+
zoomEnabled: false,
95+
// 78 candles in a box about 400 wide: 400 / 78 ≈ 5
96+
chartStyle: const ChartStyle(pointWidth: 5),
97+
xFrontPadding: 0,
98+
volHidden: true,
99+
hideGrid: true,
100+
showNowPrice: false,
101+
showInfoDialog: false,
102+
crosshairOnHover: false,
103+
showContextMenu: false,
104+
showScrollToNowButton: false,
105+
priceScaleDrag: false,
106+
);
107+
```
108+
109+
With `scrollEnabled` off a drag neither slides the window nor flings it, and
110+
[`onLoadMore`](candlestick-chart.md) is never asked for more candles — no edge
111+
is ever reached to ask at. With `zoomEnabled` off a pinch does nothing, and the
112+
zoom slider is left off too: that slider only ever appears on the web and on
113+
desktop, standing in for the pinch those platforms do not have.
114+
115+
Turn the two off together. Zooming out makes the candles narrower, which leaves
116+
the window room to scroll into, so a chart with only `scrollEnabled` off can be
117+
pinched back into a scrollable one.
118+
119+
The two flags hold the *user* back and leave your own code alone, the way
120+
`priceScaleDrag` does — so a chart nobody can drag can still be moved from a
121+
toolbar, or fitted once at startup:
122+
123+
```dart
124+
chart.fitAll(); // the whole history in the box
125+
chart.goToIndex(0); // or somewhere particular
126+
```
127+
128+
### Filling the width
129+
130+
`scrollEnabled: false` freezes the window wherever it happens to be, which is
131+
usually at the newest candle with the rest off to the left. For a chart that
132+
shows one fixed stretch, make the candles fit instead: `ChartStyle.pointWidth`
133+
is the space each candle takes — 8 by default — so roughly the chart's width
134+
divided by the number of candles puts the whole series on screen.
135+
136+
```dart
137+
chartStyle: ChartStyle(pointWidth: width / candles.length),
138+
```
139+
140+
Once the series fits there is nowhere to scroll to at all, flag or no flag —
141+
the scroll clamps to zero. `xFrontPadding: 0` gives up the gap the chart
142+
otherwise leaves to the right of the newest candle, so the candles reach the
143+
edge. `fitAll()` is the other way there, and works on any width without the
144+
arithmetic.
145+
79146
## Keeping charts in step
80147

81148
`ChartLink` holds several charts on the same window. Add each one's controller
@@ -156,7 +223,9 @@ chart.hideCrosshair(); // take it down
156223

157224
The price axis is readable and settable the same way — `priceZoom` and
158225
`pricePan` for its stretch and shift, `setPriceZoom`, `setPricePan` and
159-
`resetPriceScale` to move it.
226+
`resetPriceScale` to move it. `resetPriceScale` is also what hands a
227+
[locked axis](price-axis.md#keeping-it-still-while-the-chart-scrolls) back to the
228+
chart, refitting it to the window and holding it there afresh.
160229

161230
`onCrosshairChanged` reports where it moved to, on the same terms as
162231
`onVisibleRangeChanged`: after the frame that moved it, and only when the

doc/legend-and-crosshair.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# The legend and the crosshair
22

3+
![The OHLC legend above the chart, reading from the crosshair](https://raw.githubusercontent.com/CtrlAltDevelop/ohlcv_chart/main/screenshots/legend-and-crosshair.png)
4+
35
With a mouse, the crosshair follows the pointer without waiting for a press —
46
that is `crosshairOnHover`, on by default and irrelevant to a touch screen, which
57
has nothing that hovers. The values then belong above the chart rather than in a

doc/line-editor.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# Customising the line editor
22

3+
![The line editor open on a selected line](https://raw.githubusercontent.com/CtrlAltDevelop/ohlcv_chart/main/screenshots/line-editor.png)
4+
35
`DrawingStyle` decides what the toolbar offers, how it looks, and how close a tap
46
has to land to count. Everything is optional:
57

doc/panes.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# Panes
22

3+
![Three ATR panes stacked under the candles](https://raw.githubusercontent.com/CtrlAltDevelop/ohlcv_chart/main/screenshots/panes.png)
4+
35
An indicator pane can be made taller by dragging its lower edge, and moved up or
46
down the stack by dragging its legend row:
57

0 commit comments

Comments
 (0)