Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,68 @@
## 2.4.1

Five changes, one for each point raised in
[#3](https://github.com/CtrlAltDevelop/ohlcv_chart/issues/3) about the intraday
chart.

### Fixed

- **An axis gutter is painted in the chart's own background.** It was filled
from the canvas edge for the plot's width, which left a gutter on the right
showing whatever was under the widget, and shifted the fill when a gutter was
held back on the left. Every band now spans the whole canvas, so a label
drawn in a gutter has the chart behind it.

- **A price the axis does not reach no longer escapes the candle area.** A
locked axis makes that ordinary — the range is held where it was, so a tick
beyond it had nowhere of its own to go and was drawn over the volume and
indicator panes, or off the canvas where it could not be seen at all. 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, marked with an arrow so the level can
still be found. The current-price line, the signal lines and the trading tags
are held to the same edge, and the trading lines already were.

### Price axis

- **New `secondaryPriceAxisScale` draws a second axis down the other side.**
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 —
`PriceAxisScale.percentage` for the change since the oldest candle in view —
in a gutter on the side `verticalTextAlignment` left free, 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 the price tags keep following `priceAxisScale`. The two
gutters share half the chart's width between them, so a second axis cannot
crowd the candles out.

- **New `priceFormatter` writes the prices the chart shows.** `fixedLength`
only said how many decimals to use, so a currency symbol, a thousands
separator or a tick size had nowhere to go. It takes the writing over the way
`dateFormatter` does on the date axis, and covers every price the chart 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 — `percentage`, `indexedTo100` — writes that move itself
and does not ask.

- **New `lockedScaleFollowsPrice` keeps the newest candle on a locked axis.**
A locked axis holds the range it was given, so a market that trades past that
range walked off the top or the bottom of the chart until `resetPriceScale`
was called. With this set 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 scrolling, which is what the lock is for. 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. Off by default, and does nothing without `lockPriceScale`.

### Layout

- **New `ChartStyle.fitContent` spreads a short series across the whole plot.**
`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. With this set the
spacing is widened to whatever fills the plot, and the candle and volume bars
widen with it so they keep their proportions. It only ever widens: a series
long enough to fill the plot, or one zoomed in past it, is laid out on
`pointWidth` as before, so the flag can stay on while history pages in.

## 2.4.0

### A chart that sits still
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ Named for the open-high-low-close-volume bars it renders.

```yaml
dependencies:
ohlcv_chart: ^2.4.0
ohlcv_chart: ^2.4.1
```

## Quick start
Expand Down
5 changes: 3 additions & 2 deletions doc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,9 @@ For installation, the feature list and support, see the

- **[Price axis](price-axis.md)** — linear, logarithmic, percentage or indexed
to 100; inverting it, marking the window's high, low and average close,
dragging the scale by hand, locking it so scrolling cannot rescale it, and
holding a gutter back for its labels.
dragging the scale by hand, locking it so scrolling cannot rescale it,
writing the prices yourself with `priceFormatter`, and holding a gutter back
for its labels.
- **[The date axis](date-axis.md)** — round time values, the formats it picks
between, and taking it over with `dateFormatter`.
- **[The legend and the crosshair](legend-and-crosshair.md)** — the OHLC row
Expand Down
17 changes: 14 additions & 3 deletions doc/driving-the-chart.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,9 +129,20 @@ chart.goToIndex(0); // or somewhere particular

`scrollEnabled: false` freezes the window wherever it happens to be, which is
usually at the newest candle with the rest off to the left. For a chart that
shows one fixed stretch, make the candles fit instead: `ChartStyle.pointWidth`
is the space each candle takes — 8 by default — so roughly the chart's width
divided by the number of candles puts the whole series on screen.
shows one fixed stretch, make the candles fit instead. `ChartStyle.fitContent`
does it without knowing the width: a series too short to fill the plot is
spread over the whole of it, and the candle bodies widen to match.

```dart
chartStyle: ChartStyle(fitContent: true),
```

This only ever widens the spacing. A series long enough to fill the plot on
`ChartStyle.pointWidth` — 8 by default — is laid out on that as before, so the
flag can stay on while history pages in.

Doing the arithmetic yourself works too, and is what to reach for when the
spacing matters more than filling the box:

```dart
chartStyle: ChartStyle(pointWidth: width / candles.length),
Expand Down
78 changes: 78 additions & 0 deletions doc/price-axis.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,60 @@ The same arithmetic is exported, for a caller drawing an axis of its own beside
the chart: `niceStep`, `niceTicks` and `niceLogTicks` for values, `niceTimeStep`,
`timeBucket` and `startsNewDay` for times.

## A second axis down the other side

`secondaryPriceAxisScale` puts another axis on the side the price axis left
free — the change since the oldest candle in view, next to the prices
themselves:

```dart
KChartWidget(
candles,
ChartColors(),
secondaryPriceAxisScale: PriceAxisScale.percentage,
chartStyle: const ChartStyle(
priceAxisWidth: 56,
secondaryPriceAxisWidth: 56,
),
// ...
)
```

It marks its own round values, so a percentage axis reads +2%, +4%, +6% rather
than whatever percentages the round prices happen to work out at. The grid
stays ruled by the price axis: a second set of lines over one set of candles
would say nothing the second set of labels does not.

The two gutters share half the chart's width between them, so a second axis can
never crowd the candles out, and `secondaryPriceAxisWidth: 0` draws its labels
over the candles the way the price axis is drawn without a gutter.

The crosshair, the current-price tag and the other readouts keep following
`priceAxisScale`. The second axis is an axis, not a second voice for everything
the chart says.

## Writing the prices yourself

`fixedLength` is how many decimals a price is written to. `priceFormatter`
takes the writing over, the way `dateFormatter` does on the date axis:

```dart
KChartWidget(
candles,
ChartColors(),
priceFormatter: (price) => NumberFormat.currency(symbol: r'$').format(price),
// ...
)
```

It writes every price the chart 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 — `percentage`,
`indexedTo100` — writes that move itself and does not ask.

Drawings keep their own labels, which are yours to set through each one's
`title`.

## Reading it the other way, and other extras

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

### When the market trades past the locked range

A held range is a range the market can leave. `lockedScaleFollowsPrice` grows
it just enough to keep the newest candle on the chart:

```dart
KChartWidget(
data,
ChartColors(),
lockPriceScale: true,
lockedScaleFollowsPrice: true,
// ...
)
```

It only ever grows, and never 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.

Left off, a price outside the range is not lost either: a level the axis cannot
reach has its label pinned to the edge it went past, marked with an arrow,
rather than being drawn outside the candle area where it cannot be seen.

## Holding a gutter back for it

By default the price labels are drawn over the candles, and the candles scroll
Expand Down
66 changes: 30 additions & 36 deletions example/lib/intraday_demo.dart
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ class _IntradayDemoState extends State<IntradayDemo> {
title: const Text('Fit the whole session to the width'),
subtitle: Text(
_fitWidth
? 'pointWidth = width / 78, so all 78 candles show'
? 'ChartStyle.fitContent: all 78 candles, evenly spread'
: 'Default spacing: only part of the session fits',
),
value: _fitWidth,
Expand All @@ -67,44 +67,38 @@ class _IntradayDemoState extends State<IntradayDemo> {
Expanded(
child: Padding(
padding: const EdgeInsets.all(12),
child: LayoutBuilder(
builder: (context, constraints) {
// The whole point: each candle gets an equal share of the
// width, so the session fills the box exactly.
final pointWidth = _fitWidth
? constraints.maxWidth / _session.length
: 8.0;
child: KChartWidget(
_session,
ChartColors(),
isTrendLine: false,
watermarkAssetPath: 'assets/none.svg',
timeFrame: const Duration(minutes: 5),
chartType: ChartType.area,

return KChartWidget(
_session,
ChartColors(),
isTrendLine: false,
watermarkAssetPath: 'assets/none.svg',
timeFrame: const Duration(minutes: 5),
chartType: ChartType.area,
// What makes it sit still.
scrollEnabled: !_static,
zoomEnabled: !_static,

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

chartStyle: ChartStyle(pointWidth: pointWidth),
xFrontPadding: 0,

// Everything else a plain intraday figure does not want.
volHidden: true,
hideGrid: true,
showNowPrice: false,
showInfoDialog: false,
crosshairOnHover: false,
showContextMenu: false,
showScrollToNowButton: false,
priceScaleDrag: false,
// A page that never scrolls has nothing to page in.
onLoadMore: (isRight) => debugPrint(
'onLoadMore($isRight) — should never print while static',
),
);
},
// Everything else a plain intraday figure does not want.
volHidden: true,
hideGrid: true,
showNowPrice: false,
showInfoDialog: false,
crosshairOnHover: false,
showContextMenu: false,
showScrollToNowButton: false,
priceScaleDrag: false,
// A page that never scrolls has nothing to page in.
onLoadMore: (isRight) => debugPrint(
'onLoadMore($isRight) — should never print while static',
),
),
),
),
Expand Down
3 changes: 3 additions & 0 deletions example/lib/src/chart_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ class _Chart extends StatelessWidget {
timeFrame: MarketData.timeFrame,
chartStyle: state.style,
lockPriceScale: state.lockPriceScale,
lockedScaleFollowsPrice: state.lockedScaleFollowsPrice,
scrollEnabled: state.scrollEnabled,
zoomEnabled: state.zoomEnabled,
drawingStyle: state.drawingStyle,
Expand All @@ -113,6 +114,7 @@ class _Chart extends StatelessWidget {
drawingController: state.drawings,
chartType: state.chartType,
priceAxisScale: state.priceAxisScale,
secondaryPriceAxisScale: state.secondaryPriceAxisScale,
session: state.tradingSession,
candleColor: state.candleColor,
invertPriceAxis: state.invertPriceAxis,
Expand All @@ -139,6 +141,7 @@ class _Chart extends StatelessWidget {
fixedLength: state.fixedLength,
timeFormat: TimeFormat.YEAR_MONTH_DAY_WITH_HOUR,
dateFormatter: state.customDateFormat ? state.formatDate : null,
priceFormatter: state.priceFormatter,
baselinePrice: state.baselinePrice,
xFrontPadding: state.frontPadding,
showScrollToNowButton: state.scrollToNowButton,
Expand Down
35 changes: 35 additions & 0 deletions example/lib/src/controls.dart
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,16 @@ class Controls extends StatelessWidget {
},
onChanged: (v) => state.update(() => state.priceAxisScale = v),
),
_Toggle(
label: 'Change % down the other side',
subtitle: 'A second axis reading the move from the oldest candle',
value: state.secondaryPriceAxisScale != null,
onChanged: (v) => state.update(
() => state.secondaryPriceAxisScale = v
? PriceAxisScale.percentage
: null,
),
),
_Toggle(
label: 'Invert the price axis',
subtitle: 'Higher prices lower down',
Expand Down Expand Up @@ -429,6 +439,31 @@ class Controls extends StatelessWidget {
value: state.lockPriceScale,
onChanged: (v) => state.update(() => state.lockPriceScale = v),
),
_Toggle(
label: 'Locked axis follows the price',
subtitle: 'Grows the locked range to keep the newest candle on',
value: state.lockedScaleFollowsPrice,
onChanged: (v) =>
state.update(() => state.lockedScaleFollowsPrice = v),
),
_Toggle(
label: 'Level off the axis',
subtitle: 'A line above every price — its label marks the edge',
value: state.levelOffTheAxis,
onChanged: (v) => state.update(() => state.toggleFarLevel(v)),
),
_Toggle(
label: 'Fit the candles to the width',
subtitle: 'Spreads a short series over the whole plot',
value: state.fitContent,
onChanged: (v) => state.update(() => state.fitContent = v),
),
_Toggle(
label: 'Prices as currency',
subtitle: r'priceFormatter writes them as $1234.50',
value: state.currencyPrices,
onChanged: (v) => state.update(() => state.currencyPrices = v),
),
_Toggle(
label: 'Now price and countdown',
value: state.showNowPrice,
Expand Down
Loading
Loading