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
83 changes: 83 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Publishes the package to pub.dev when a release is created.
#
# Creating a GitHub release from main tags it, and the tag is what starts this:
# `v3.1.0` publishes version 3.1.0. Nothing is published unless the tag is on
# main, matches the version in pubspec.yaml and has its CHANGELOG entry.
#
# One-off setup on pub.dev (package Admin tab > Automated publishing):
# - enable publishing from GitHub Actions for CtrlAltDevelop/ohlcv_chart
# - tag pattern: v{{version}}
# - require the GitHub Actions environment: pub.dev
# and create an environment named `pub.dev` in the repository settings, with
# a required reviewer if every publish should be approved by hand.

name: Publish

on:
push:
tags: ["v[0-9]+.[0-9]+.[0-9]+*"]

permissions: {}

jobs:
verify:
name: Verify the release
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0

- name: The tag is on main
run: |
if ! git merge-base --is-ancestor "$GITHUB_SHA" origin/main; then
echo "::error::$GITHUB_REF_NAME is not on main; release from main."
exit 1
fi

- name: The tag matches the pubspec version
run: |
version=$(sed -n 's/^version: *//p' pubspec.yaml | tr -d '\r')
if [ "v$version" != "$GITHUB_REF_NAME" ]; then
echo "::error::Tag $GITHUB_REF_NAME does not match pubspec version $version."
exit 1
fi

- name: The CHANGELOG has an entry for it
run: |
version="${GITHUB_REF_NAME#v}"
if ! grep -q "^## $version\b" CHANGELOG.md; then
echo "::error::CHANGELOG.md has no '## $version' entry."
exit 1
fi

publish:
name: Publish to pub.dev
needs: verify
runs-on: ubuntu-latest
environment: pub.dev
permissions:
contents: read
# Lets pub.dev trust this run without a stored secret.
id-token: write
steps:
- uses: actions/checkout@v7

# Sets up the credentials pub.dev issues to this run; the Flutter
# setup below brings the SDK `flutter pub publish` needs.
- uses: dart-lang/setup-dart@v1

- uses: subosito/flutter-action@v2
with:
channel: stable
cache: true

- run: flutter pub get

- name: Dry run
run: flutter pub publish --dry-run

- name: Publish
run: flutter pub publish --force
53 changes: 53 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,56 @@
## 3.1.0 - 2026-10-06

### Added

- **Sizing the panes from your code.** `KChartWidget.paneHeights` sets the
indicator panes' heights and `volumeHeight` the volume pane's. Heights are
used as given — past `ChartStyle.maxPaneHeight` if asked — and cut back only
when a pane would squeeze the candles out of the box. Null leaves the heights
to the chart, as before. While a list is given the host owns the heights: a
drag or a controller call is reported through `onPaneHeightsChanged`, and the
pane moves when the list is passed back.
- **Maximizing a pane.** `KChartController.maximizePane`, `maximizeVolume`,
`restorePanes` and `toggleMaximizePane` stretch one pane over the chart, the
candles keeping a strip above it, for a "maximize this indicator" button. The
stretch follows the chart's size and its own indicator through a reorder.
`setPaneHeight`, `resetPaneHeights`, `paneHeights`, `maximizedPane` and
`isVolumeMaximized` complete the set.
- **`KChartWidget.paneSizeMode`** chooses how the chart's parts are sized.
`PaneSizeMode.heights` is the default and the behaviour so far.
`PaneSizeMode.ratios` splits the height by `paneRatios` — the candles, the
volume pane, then each indicator pane, so `[3, 1, 2]` is half, a sixth and a
third — and holds the split as the chart is resized. `PaneSizeMode.custom`
lets the user lay the chart out: `KChartController.editPanes` shows a line
between each two parts to drag between the smallest and largest height each
may take, the layout is kept as proportions, and `onPaneRatiosChanged`
reports it for saving. `finishEditingPanes`, `toggleEditPanes` and
`isEditingPanes` complete the set. Both modes need a bounded height and no
`mBaseHeight`.
- **`ChartStyle.gridColumnMode`** chooses where the grid's vertical lines go.
`GridColumnMode.dateTicks` (the default, and the behaviour so far) rules a line
wherever a candle crosses a time bucket; with only a few candles in view that
can leave the lines bunched together. `GridColumnMode.evenlySpaced` divides the
chart width into `gridColumns` equal bands instead, so the grid spans the whole
chart however sparse the data. The date labels keep their round times.
- **`ChartStyle.showGridRows` and `showGridColumns`** switch the horizontal and
vertical grid lines on and off independently. `hideGrid` still hides both, and
the pane borders stay either way.
- **`ChartStyle.gridDashPattern`** dashes the grid lines with a list of on/off
lengths, e.g. `[4, 3]`. Null is solid; an empty list or a non-positive length
is read as solid.
- The example app has switches for the grid options, a sizing chooser for the
panes, an Edit layout chip, and a chip to maximize each pane and the volume.

### Changed

- Dragged pane heights move with their pane when `onReorderPane` reorders the
indicators, instead of staying at their position.
- `ChartStyle.minPaneHeight` is held at 32 or more when a pane is dragged or
maximized around, so a pane can never be made so small that its edge cannot be
grabbed to make it bigger again.
- `KChartHost` has nine new members for the above. Only an app that implements
it itself, such as a test double, needs to add them.

## 3.0.0 - 2026-09-19

### Breaking
Expand Down
14 changes: 14 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,20 @@ their shape rather than inventing a new one.
page too. A new top-level feature gets a new page, linked from both
[`README.md`](README.md) and [`doc/README.md`](doc/README.md).

## Releasing

Maintainers only. Releases are published to pub.dev automatically:

1. Merge `develop` into `main` with the version bumped in `pubspec.yaml` and
the `## Unreleased` heading in `CHANGELOG.md` renamed to
`## <version> - <date>`.
2. Create a GitHub release from `main` with the tag `v<version>`.

The tag starts [`publish.yml`](.github/workflows/publish.yml), which checks the
tag is on `main`, matches the pubspec version and has its changelog entry, and
then publishes. It needs automated publishing enabled on pub.dev for this
repository, with the tag pattern `v{{version}}` and the `pub.dev` environment.

## Reporting a bug or requesting a feature

Use the issue templates — they ask for the couple of things that are always
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,7 @@ indicator and drawing tool is included; there is no paid tier.
```yaml
dependencies:
material_ui: ">=1.0.0 <2.0.0"
ohlcv_chart: ">=3.0.0 <4.0.0"
ohlcv_chart: ">=3.1.0 <4.0.0"
```

Every widget here is built on the `material_ui` package rather than the
Expand Down
29 changes: 29 additions & 0 deletions doc/date-axis.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,35 @@ Like the price axis, the date axis selects round values before placing labels.
`ChartStyle.gridColumns` controls label density, in the same way as `gridRows`
on the price axis.

By default the grid's vertical lines rule wherever a candle crosses a time
bucket (`GridColumnMode.dateTicks`), so each line lines up with a round time.
With only a handful of candles in view — a sparse intraday window, say —
there may be only one or two such crossings, leaving most of the chart
without a line. Set `ChartStyle.gridColumnMode` to
`GridColumnMode.evenlySpaced` to divide the chart width into `gridColumns`
equal bands instead, so the grid always spans the chart regardless of candle
count — at the cost of the lines no longer landing on round times:

```dart
ChartStyle(
gridColumnMode: GridColumnMode.evenlySpaced,
);
```

The grid can also be trimmed or dashed, each part on its own:

```dart
ChartStyle(
showGridRows: false, // vertical lines only
showGridColumns: true,
gridDashPattern: [4, 3], // 4px dash, 3px gap; null is solid
);
```

`hideGrid` on the widget still hides the whole grid. The date labels keep
their round times in either column mode, so with `evenlySpaced` a line and a
label need not meet.

To control formatting, use `ChartStyle.dateTimeFormat` for a fixed pattern, or
`dateFormatter` for full control. `dateFormatter` receives each candle and a
flag indicating whether the long form (used by the crosshair) is required:
Expand Down
147 changes: 144 additions & 3 deletions doc/panes.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,157 @@ KChartWidget(

## Height and order

Pane heights are managed by the chart, constrained between
`ChartStyle.minPaneHeight` and `maxPaneHeight`, and reset when the set of panes
changes.
By default pane heights are managed by the chart: 100 each, or whatever the user
dragged them to, constrained between `ChartStyle.minPaneHeight` and
`maxPaneHeight`, and reset when panes are added or removed. A reorder carries
each height along with its pane.

Pane order is owned by your indicator list. The chart reports the move through
`onReorderPane`, and your code applies it.

## Sizing panes from your code

Two ways to set heights from outside — a "maximize this indicator" button is
the usual reason.

**`KChartController`** takes commands:

```dart
chart.maximizePane(1); // pane 1 fills the chart, candles shrink to a strip
chart.restorePanes(); // …and back
chart.toggleMaximizePane(1); // one button for both
chart.maximizeVolume(); // the volume pane instead
chart.setPaneHeight(0, 220); // any single pane, any positive height
chart.resetPaneHeights(); // every pane back to the standard height
chart.paneHeights; // what is drawn now
```

![Three panes at the standard height](https://raw.githubusercontent.com/CtrlAltDevelop/ohlcv_chart/main/screenshots/panes-default.jpg)
![The RSI pane maximized](https://raw.githubusercontent.com/CtrlAltDevelop/ohlcv_chart/main/screenshots/panes-maximized-rsi.jpg)
![The OBV pane maximized](https://raw.githubusercontent.com/CtrlAltDevelop/ohlcv_chart/main/screenshots/panes-maximized-obv.jpg)

Only one pane — or the volume pane — is maximized at a time. A maximized pane follows the chart's size and moves with its indicator when you
reorder; dragging any pane lets go of it. It needs the chart to size its own
candle area, so it has no effect when `mBaseHeight` is set.

**`KChartWidget.paneHeights`** is the same thing as state:

```dart
KChartWidget(
candles,
ChartColors(),
indicators: indicators,
paneHeights: maximized ? [60, 500] : null, // null: the chart's own layout
volumeHeight: 80, // the volume pane, default 60
onPaneHeightsChanged: (heights) => setState(() => saved = heights),
);
```

- Heights set from code are used as given, not held to `minPaneHeight` and
`maxPaneHeight`, so a pane can be taller than a user could drag it. A height
too big for the box is cut back so the candles keep a strip above it.
- A pane missing from a shorter list, or given a height that is not a positive
number, gets the standard height.
- While a list is given you own the heights, like a controlled text field. A
drag (with `resizablePanes` on) or `setPaneHeight` does not move a pane by
itself: it is reported through `onPaneHeightsChanged`, and the pane moves when
you pass the new list back. Pass `null` to give the heights back to the chart.
Maximizing through the controller still works over a list, and lets go of
whenever a height changes.
- Panes in `paneHeights` are matched by position. When `onReorderPane` moves
one, move its height with it.

`ChartStyle.paneResizeTolerance` and `paneGrabHeight` set the size of the
resize and reorder hit areas.

## Sizing modes

`paneSizeMode` chooses how the heights of the candles, the volume pane and the
indicator panes are worked out:

| Mode | Heights come from | Who changes them |
|---|---|---|
| `PaneSizeMode.heights` (default) | pixels: 100 per pane, 60 for volume, or `paneHeights` / `volumeHeight` | you, or the user dragging a pane's lower edge with `resizablePanes` |
| `PaneSizeMode.ratios` | proportions in `paneRatios` | you |
| `PaneSizeMode.custom` | the user's own layout | the user, dragging lines in edit mode |

`ratios` and `custom` need the box to bound the chart's height and `mBaseHeight`
to be left off; otherwise the pixel heights are used.

### Proportions

```dart
KChartWidget(
candles,
ChartColors(),
indicators: [RsiIndicator()],
paneSizeMode: PaneSizeMode.ratios,
paneRatios: [3, 1, 2], // 6 units: candles 3, volume 1, RSI 2
);
```

![Candles 3, volume 1, MACD 2 and RSI 2](https://raw.githubusercontent.com/CtrlAltDevelop/ohlcv_chart/main/screenshots/panes-ratios.jpg)

- The list runs top to bottom: the candles, the volume pane, then each indicator
pane. The height left after the legend rows above the candles is split by
those units, and the split holds as the box is resized.
- With `volHidden` the volume's number is left out, so the list is one shorter.
- A part missing from a shorter list, or given a number that is not above zero,
counts as 1. Without `paneRatios` the mode has nothing to split by, and the
pixel heights are used.
- You own the proportions, as with `paneHeights`. With `resizablePanes` on, a
drag moves room between the two parts either side of the edge — the last pane
takes from the part above — and `onPaneRatiosChanged` reports the new list,
keeping its total. Pass it back for the drag to take effect:

```dart
paneRatios: ratios,
resizablePanes: true,
onPaneRatiosChanged: (next) => setState(() => ratios = next),
```

### Custom layout

The user lays the chart out themselves. Put it in `custom` mode and turn editing
on from the controller; a line appears between each two parts, the candles and
volume included, to drag up or down:

```dart
final chart = KChartController();

KChartWidget(
candles,
ChartColors(),
controller: chart,
indicators: [RsiIndicator()],
paneSizeMode: PaneSizeMode.custom,
onPaneRatiosChanged: (ratios) => saved = ratios, // optional
);

chart.editPanes(); // show the lines
chart.finishEditingPanes(); // put them away, keeping the layout
chart.toggleEditPanes(); // one button for both
chart.resetPaneHeights(); // back to the standard layout
```

![Edit mode: a line between each two parts](https://raw.githubusercontent.com/CtrlAltDevelop/ohlcv_chart/main/screenshots/panes-custom-edit.jpg)
![The line between MACD and RSI dragged up](https://raw.githubusercontent.com/CtrlAltDevelop/ohlcv_chart/main/screenshots/panes-custom-dragged.jpg)

- A line moves room between the two parts either side of it, and stops where
either reaches its limit: `ChartStyle.minPaneHeight` and `maxPaneHeight` for
the volume and the panes, a 60 px floor for the candles, which have no
ceiling because they are what is left.
- However small `minPaneHeight` is set, a pane or the volume pane is held at 32
px or more when dragged or maximized around, so there is always an edge or
line left to pull it back out.
- The chart owns the layout and keeps it as proportions, so it holds through a
resize. It starts from `paneRatios` when you give some, or from the standard
heights, and passing a different `paneRatios` later starts it over from that.
- `onPaneRatiosChanged` reports every change in `paneRatios` order, which is
what to save and pass back as `paneRatios` next time.
- The lines are not shown while a pane is maximized, and `resizablePanes` has no
effect in this mode; the lines replace it.

---

[← All docs](README.md) · [Package README](../README.md)
4 changes: 4 additions & 0 deletions example/lib/src/chart_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,10 @@ class _Chart extends StatelessWidget {
crosshairOnHover: state.crosshairOnHover,
timeZoneOffset: state.timeZoneOffset,
resizablePanes: state.resizablePanes,
paneSizeMode: state.paneMode,
paneRatios: state.paneRatios,
onPaneRatiosChanged: (ratios) =>
state.update(() => state.paneRatios = ratios),
reorderablePanes: state.reorderablePanes,
onReorderPane: state.reorderPane,
selectAfterDrawing: !state.keepToolArmed,
Expand Down
Loading
Loading