Skip to content

Commit 18b2a4f

Browse files
Release 2.4.1: five answers to the intraday chart feedback
Closes #3. Five changes, one for each point raised in the issue. Each is its own commit. ### Fit a short series to the width — `ChartStyle.fitContent` `pointWidth` is a fixed distance, so a handful of intraday bars bunched up against the left edge and left the rest of the chart empty. Fitting widens the spacing to fill the plot, and widens the candle and volume bars with it so they keep their proportions. It only ever widens, so a series long enough to fill the plot is laid out on `pointWidth` as before and the flag can stay on while history pages in. A fitted series is exactly as wide as the plot, so `getMinTranslateX` holds the scroll at zero rather than leaving the half point the last candle's centre normally needs. ### Keep a price the locked axis cannot reach inside the candle area — bug fix A locked axis holds the range it was given, so a tick beyond it had nowhere of its own to be drawn: the line was painted over the volume and indicator panes, or off the canvas where it was invisible. A horizontal line at such a price is now left out of the plot, and its label is pinned to the edge the price went past and marked with an arrow so the level can still be found. The current-price line, the signal lines and the trading tags are clamped to the same edge — they were clamped to the *window's* extremes, which under a lock are not the axis's. ### Keep the newest candle on a locked axis — `lockedScaleFollowsPrice` Opt-in, so an existing `lockPriceScale: true` chart behaves exactly as before. The locked range grows just enough to cover the newest candle and never shrinks back or refits to the window, so the axis still sits still while the chart is scrolled. Only the newest candle counts, and only while it is in view: growing the axis to swallow the history a scroll moves over would undo the lock a little at a time. ### Write the chart's prices — `priceFormatter` `fixedLength` only said how many decimals a price is written to, so a currency symbol, a thousands separator or a tick size had nowhere to go. The new callback is the counterpart to `dateFormatter` and covers every price the chart itself says: the axis labels, the crosshair's price label, the current-price tag, the high, low and signal tags, and the OHLC legend. An axis that reads out a move rather than a price writes that move itself and does not ask. ### A second price axis — `secondaryPriceAxisScale` The chart had one price axis, so reading a move as a percentage meant giving up the prices. A second axis reads the same candles another way, in a gutter on the side `verticalTextAlignment` left free and sized by `ChartStyle.secondaryPriceAxisWidth`. It marks its own round values rather than labelling the price axis's, the grid stays ruled by the price axis, and the crosshair and price tags keep following `priceAxisScale`. The two gutters are clamped against each other, so between them they can never take more than half the chart's width. ### Also in here - **Background fill fix.** Each band was filled from the canvas edge for the *plot's* width, so a gutter on the right showed whatever was under the widget and a gutter on the left shifted the fill. Every band now spans the canvas. - **Example app.** A toggle for each change, and the intraday demo uses `fitContent` instead of working the spacing out by hand. - **Goldens** for the fitted series (before and after), the second axis, the currency prices and the edge-marked level. - One existing perf test priced its drawings below everything on screen, which no longer resolves an anchor now that an unreachable line is skipped before the lookup. Repriced around the newest candle; its subject is unchanged. ### Release Version 2.4.1, with the changelog entry and the README's install line updated. ### Testing 898 tests pass, `flutter analyze` and `dart format` clean, and the example builds for both entry points. 22 new tests, including three that fail without the out-of-range fix.
2 parents d2f4b3a + 12eb8d1 commit 18b2a4f

27 files changed

Lines changed: 1346 additions & 81 deletions

‎CHANGELOG.md‎

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,68 @@
1+
## 2.4.1
2+
3+
Five changes, one for each point raised in
4+
[#3](https://github.com/CtrlAltDevelop/ohlcv_chart/issues/3) about the intraday
5+
chart.
6+
7+
### Fixed
8+
9+
- **An axis gutter is painted in the chart's own background.** It was filled
10+
from the canvas edge for the plot's width, which left a gutter on the right
11+
showing whatever was under the widget, and shifted the fill when a gutter was
12+
held back on the left. Every band now spans the whole canvas, so a label
13+
drawn in a gutter has the chart behind it.
14+
15+
- **A price the axis does not reach no longer escapes the candle area.** A
16+
locked axis makes that ordinary — the range is held where it was, so a tick
17+
beyond it had nowhere of its own to go and was drawn over the volume and
18+
indicator panes, or off the canvas where it could not be seen at all. A
19+
horizontal line at such a price is now left out of the plot and its label is
20+
pinned to the edge the price went past, marked with an arrow so the level can
21+
still be found. The current-price line, the signal lines and the trading tags
22+
are held to the same edge, and the trading lines already were.
23+
24+
### Price axis
25+
26+
- **New `secondaryPriceAxisScale` draws a second axis down the other side.**
27+
The chart had one price axis, so reading a move as a percentage meant giving
28+
up the prices. A second axis reads the same candles another way —
29+
`PriceAxisScale.percentage` for the change since the oldest candle in view —
30+
in a gutter on the side `verticalTextAlignment` left free, sized by
31+
`ChartStyle.secondaryPriceAxisWidth`. It marks its own round values rather
32+
than labelling the price axis's, the grid stays ruled by the price axis, and
33+
the crosshair and the price tags keep following `priceAxisScale`. The two
34+
gutters share half the chart's width between them, so a second axis cannot
35+
crowd the candles out.
36+
37+
- **New `priceFormatter` writes the prices the chart shows.** `fixedLength`
38+
only said how many decimals to use, so a currency symbol, a thousands
39+
separator or a tick size had nowhere to go. It takes the writing over the way
40+
`dateFormatter` does on the date axis, and covers every price the chart says:
41+
the axis labels, the crosshair's price label, the current-price tag, the
42+
high, low and signal tags, and the OHLC legend. An axis that reads out a move
43+
rather than a price — `percentage`, `indexedTo100` — writes that move itself
44+
and does not ask.
45+
46+
- **New `lockedScaleFollowsPrice` keeps the newest candle on a locked axis.**
47+
A locked axis holds the range it was given, so a market that trades past that
48+
range walked off the top or the bottom of the chart until `resetPriceScale`
49+
was called. With this set the locked range grows just enough to cover the
50+
newest candle, and never shrinks back or refits to the window — so the axis
51+
still sits still while scrolling, which is what the lock is for. Only the
52+
newest candle counts, and only while it is in view: growing the axis to
53+
swallow the history a scroll moves over would undo the lock a little at a
54+
time. Off by default, and does nothing without `lockPriceScale`.
55+
56+
### Layout
57+
58+
- **New `ChartStyle.fitContent` spreads a short series across the whole plot.**
59+
`pointWidth` is a fixed distance, so a handful of intraday bars bunched up
60+
against the left edge and left the rest of the chart empty. With this set the
61+
spacing is widened to whatever fills the plot, and the candle and volume bars
62+
widen with it so they keep their proportions. It only ever widens: a series
63+
long enough to fill the plot, or one zoomed in past it, is laid out on
64+
`pointWidth` as before, so the flag can stay on while history pages in.
65+
166
## 2.4.0
267

368
### A chart that sits still

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ Named for the open-high-low-close-volume bars it renders.
8484

8585
```yaml
8686
dependencies:
87-
ohlcv_chart: ^2.4.0
87+
ohlcv_chart: ^2.4.1
8888
```
8989
9090
## Quick start

‎doc/README.md‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,9 @@ For installation, the feature list and support, see the
2626

2727
- **[Price axis](price-axis.md)** — linear, logarithmic, percentage or indexed
2828
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.
29+
dragging the scale by hand, locking it so scrolling cannot rescale it,
30+
writing the prices yourself with `priceFormatter`, and holding a gutter back
31+
for its labels.
3132
- **[The date axis](date-axis.md)** — round time values, the formats it picks
3233
between, and taking it over with `dateFormatter`.
3334
- **[The legend and the crosshair](legend-and-crosshair.md)** — the OHLC row

‎doc/driving-the-chart.md‎

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -129,9 +129,20 @@ chart.goToIndex(0); // or somewhere particular
129129

130130
`scrollEnabled: false` freezes the window wherever it happens to be, which is
131131
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.
132+
shows one fixed stretch, make the candles fit instead. `ChartStyle.fitContent`
133+
does it without knowing the width: a series too short to fill the plot is
134+
spread over the whole of it, and the candle bodies widen to match.
135+
136+
```dart
137+
chartStyle: ChartStyle(fitContent: true),
138+
```
139+
140+
This only ever widens the spacing. A series long enough to fill the plot on
141+
`ChartStyle.pointWidth` — 8 by default — is laid out on that as before, so the
142+
flag can stay on while history pages in.
143+
144+
Doing the arithmetic yourself works too, and is what to reach for when the
145+
spacing matters more than filling the box:
135146

136147
```dart
137148
chartStyle: ChartStyle(pointWidth: width / candles.length),

‎doc/price-axis.md‎

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,60 @@ The same arithmetic is exported, for a caller drawing an axis of its own beside
5656
the chart: `niceStep`, `niceTicks` and `niceLogTicks` for values, `niceTimeStep`,
5757
`timeBucket` and `startsNewDay` for times.
5858

59+
## A second axis down the other side
60+
61+
`secondaryPriceAxisScale` puts another axis on the side the price axis left
62+
free — the change since the oldest candle in view, next to the prices
63+
themselves:
64+
65+
```dart
66+
KChartWidget(
67+
candles,
68+
ChartColors(),
69+
secondaryPriceAxisScale: PriceAxisScale.percentage,
70+
chartStyle: const ChartStyle(
71+
priceAxisWidth: 56,
72+
secondaryPriceAxisWidth: 56,
73+
),
74+
// ...
75+
)
76+
```
77+
78+
It marks its own round values, so a percentage axis reads +2%, +4%, +6% rather
79+
than whatever percentages the round prices happen to work out at. The grid
80+
stays ruled by the price axis: a second set of lines over one set of candles
81+
would say nothing the second set of labels does not.
82+
83+
The two gutters share half the chart's width between them, so a second axis can
84+
never crowd the candles out, and `secondaryPriceAxisWidth: 0` draws its labels
85+
over the candles the way the price axis is drawn without a gutter.
86+
87+
The crosshair, the current-price tag and the other readouts keep following
88+
`priceAxisScale`. The second axis is an axis, not a second voice for everything
89+
the chart says.
90+
91+
## Writing the prices yourself
92+
93+
`fixedLength` is how many decimals a price is written to. `priceFormatter`
94+
takes the writing over, the way `dateFormatter` does on the date axis:
95+
96+
```dart
97+
KChartWidget(
98+
candles,
99+
ChartColors(),
100+
priceFormatter: (price) => NumberFormat.currency(symbol: r'$').format(price),
101+
// ...
102+
)
103+
```
104+
105+
It writes every price the chart says: the axis labels, the crosshair's price
106+
label, the current-price tag, the high, low and signal tags, and the OHLC
107+
legend. An axis that reads out a move rather than a price — `percentage`,
108+
`indexedTo100` — writes that move itself and does not ask.
109+
110+
Drawings keep their own labels, which are yours to set through each one's
111+
`title`.
112+
59113
## Reading it the other way, and other extras
60114

61115
```dart
@@ -123,6 +177,30 @@ Because the range is held until it is reset, a chart that switches to another
123177
instrument should reset it — a range from one instrument means nothing on
124178
another. Paging in candles and live ticks need nothing, which is the point.
125179

180+
### When the market trades past the locked range
181+
182+
A held range is a range the market can leave. `lockedScaleFollowsPrice` grows
183+
it just enough to keep the newest candle on the chart:
184+
185+
```dart
186+
KChartWidget(
187+
data,
188+
ChartColors(),
189+
lockPriceScale: true,
190+
lockedScaleFollowsPrice: true,
191+
// ...
192+
)
193+
```
194+
195+
It only ever grows, and never refits to the window, so the axis still sits
196+
still while the chart is scrolled. Only the newest candle counts, and only
197+
while it is in view — growing the axis to swallow the history a scroll moves
198+
over would undo the lock a little at a time.
199+
200+
Left off, a price outside the range is not lost either: a level the axis cannot
201+
reach has its label pinned to the edge it went past, marked with an arrow,
202+
rather than being drawn outside the candle area where it cannot be seen.
203+
126204
## Holding a gutter back for it
127205

128206
By default the price labels are drawn over the candles, and the candles scroll

‎example/lib/intraday_demo.dart‎

Lines changed: 30 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ class _IntradayDemoState extends State<IntradayDemo> {
5757
title: const Text('Fit the whole session to the width'),
5858
subtitle: Text(
5959
_fitWidth
60-
? 'pointWidth = width / 78, so all 78 candles show'
60+
? 'ChartStyle.fitContent: all 78 candles, evenly spread'
6161
: 'Default spacing: only part of the session fits',
6262
),
6363
value: _fitWidth,
@@ -67,44 +67,38 @@ class _IntradayDemoState extends State<IntradayDemo> {
6767
Expanded(
6868
child: Padding(
6969
padding: const EdgeInsets.all(12),
70-
child: LayoutBuilder(
71-
builder: (context, constraints) {
72-
// The whole point: each candle gets an equal share of the
73-
// width, so the session fills the box exactly.
74-
final pointWidth = _fitWidth
75-
? constraints.maxWidth / _session.length
76-
: 8.0;
70+
child: KChartWidget(
71+
_session,
72+
ChartColors(),
73+
isTrendLine: false,
74+
watermarkAssetPath: 'assets/none.svg',
75+
timeFrame: const Duration(minutes: 5),
76+
chartType: ChartType.area,
7777

78-
return KChartWidget(
79-
_session,
80-
ChartColors(),
81-
isTrendLine: false,
82-
watermarkAssetPath: 'assets/none.svg',
83-
timeFrame: const Duration(minutes: 5),
84-
chartType: ChartType.area,
78+
// What makes it sit still.
79+
scrollEnabled: !_static,
80+
zoomEnabled: !_static,
8581

86-
// What makes it sit still.
87-
scrollEnabled: !_static,
88-
zoomEnabled: !_static,
82+
// The whole point: each candle takes an equal share of the
83+
// width, so the session fills the box exactly. The chart
84+
// works the spacing out from its own width, so nothing here
85+
// has to know how wide it ended up.
86+
chartStyle: ChartStyle(fitContent: _fitWidth),
87+
xFrontPadding: 0,
8988

90-
chartStyle: ChartStyle(pointWidth: pointWidth),
91-
xFrontPadding: 0,
92-
93-
// Everything else a plain intraday figure does not want.
94-
volHidden: true,
95-
hideGrid: true,
96-
showNowPrice: false,
97-
showInfoDialog: false,
98-
crosshairOnHover: false,
99-
showContextMenu: false,
100-
showScrollToNowButton: false,
101-
priceScaleDrag: false,
102-
// A page that never scrolls has nothing to page in.
103-
onLoadMore: (isRight) => debugPrint(
104-
'onLoadMore($isRight) — should never print while static',
105-
),
106-
);
107-
},
89+
// Everything else a plain intraday figure does not want.
90+
volHidden: true,
91+
hideGrid: true,
92+
showNowPrice: false,
93+
showInfoDialog: false,
94+
crosshairOnHover: false,
95+
showContextMenu: false,
96+
showScrollToNowButton: false,
97+
priceScaleDrag: false,
98+
// A page that never scrolls has nothing to page in.
99+
onLoadMore: (isRight) => debugPrint(
100+
'onLoadMore($isRight) — should never print while static',
101+
),
108102
),
109103
),
110104
),

‎example/lib/src/chart_page.dart‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ class _Chart extends StatelessWidget {
9191
timeFrame: MarketData.timeFrame,
9292
chartStyle: state.style,
9393
lockPriceScale: state.lockPriceScale,
94+
lockedScaleFollowsPrice: state.lockedScaleFollowsPrice,
9495
scrollEnabled: state.scrollEnabled,
9596
zoomEnabled: state.zoomEnabled,
9697
drawingStyle: state.drawingStyle,
@@ -113,6 +114,7 @@ class _Chart extends StatelessWidget {
113114
drawingController: state.drawings,
114115
chartType: state.chartType,
115116
priceAxisScale: state.priceAxisScale,
117+
secondaryPriceAxisScale: state.secondaryPriceAxisScale,
116118
session: state.tradingSession,
117119
candleColor: state.candleColor,
118120
invertPriceAxis: state.invertPriceAxis,
@@ -139,6 +141,7 @@ class _Chart extends StatelessWidget {
139141
fixedLength: state.fixedLength,
140142
timeFormat: TimeFormat.YEAR_MONTH_DAY_WITH_HOUR,
141143
dateFormatter: state.customDateFormat ? state.formatDate : null,
144+
priceFormatter: state.priceFormatter,
142145
baselinePrice: state.baselinePrice,
143146
xFrontPadding: state.frontPadding,
144147
showScrollToNowButton: state.scrollToNowButton,

‎example/lib/src/controls.dart‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,16 @@ class Controls extends StatelessWidget {
313313
},
314314
onChanged: (v) => state.update(() => state.priceAxisScale = v),
315315
),
316+
_Toggle(
317+
label: 'Change % down the other side',
318+
subtitle: 'A second axis reading the move from the oldest candle',
319+
value: state.secondaryPriceAxisScale != null,
320+
onChanged: (v) => state.update(
321+
() => state.secondaryPriceAxisScale = v
322+
? PriceAxisScale.percentage
323+
: null,
324+
),
325+
),
316326
_Toggle(
317327
label: 'Invert the price axis',
318328
subtitle: 'Higher prices lower down',
@@ -429,6 +439,31 @@ class Controls extends StatelessWidget {
429439
value: state.lockPriceScale,
430440
onChanged: (v) => state.update(() => state.lockPriceScale = v),
431441
),
442+
_Toggle(
443+
label: 'Locked axis follows the price',
444+
subtitle: 'Grows the locked range to keep the newest candle on',
445+
value: state.lockedScaleFollowsPrice,
446+
onChanged: (v) =>
447+
state.update(() => state.lockedScaleFollowsPrice = v),
448+
),
449+
_Toggle(
450+
label: 'Level off the axis',
451+
subtitle: 'A line above every price — its label marks the edge',
452+
value: state.levelOffTheAxis,
453+
onChanged: (v) => state.update(() => state.toggleFarLevel(v)),
454+
),
455+
_Toggle(
456+
label: 'Fit the candles to the width',
457+
subtitle: 'Spreads a short series over the whole plot',
458+
value: state.fitContent,
459+
onChanged: (v) => state.update(() => state.fitContent = v),
460+
),
461+
_Toggle(
462+
label: 'Prices as currency',
463+
subtitle: r'priceFormatter writes them as $1234.50',
464+
value: state.currencyPrices,
465+
onChanged: (v) => state.update(() => state.currencyPrices = v),
466+
),
432467
_Toggle(
433468
label: 'Now price and countdown',
434469
value: state.showNowPrice,

0 commit comments

Comments
 (0)