wick-charts 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -19,6 +19,7 @@ npm install wick-charts
19
19
  - [Candle data](#candle-data)
20
20
  - [Line charts](#line-charts)
21
21
  - [Styling](#styling)
22
+ - [Inverting the value axis](#inverting-the-value-axis)
22
23
  - [Reading chart state](#reading-chart-state)
23
24
  - [Loading more history on demand](#loading-more-history-on-demand)
24
25
  - [Extending: plugins](#extending-plugins)
@@ -227,6 +228,39 @@ type-checks `style` against
227
228
  `CandlestickStyle`; the more general `new WickChart(canvas, { type: 'candlestick', style })`
228
229
  also works but doesn't — see "Series types" below for why, if you're curious.
229
230
 
231
+ ### Inverting the value axis
232
+
233
+ `invertValueAxis` mirrors the value axis top-to-bottom — every pane's higher values render
234
+ lower on screen instead of higher, useful for a "what if this series had moved the opposite
235
+ way" view:
236
+
237
+ ```ts
238
+ const chart = createCandlestickChart(canvas, { invertValueAxis: true });
239
+ ```
240
+
241
+ Unlike the style options above, it's meant to be flipped live rather than fixed at
242
+ construction — `setInvertValueAxis(boolean)`/`isValueAxisInverted()` let a UI toggle it on an
243
+ existing chart without losing the current pan/zoom position or manual value-range override,
244
+ the same way `setPluginVisible` toggles a plugin without losing its state:
245
+
246
+ ```ts
247
+ toggleButton.addEventListener('click', () => {
248
+ chart.setInvertValueAxis(!chart.isValueAxisInverted());
249
+ });
250
+ ```
251
+
252
+ Only where each value renders is mirrored — the underlying data isn't. A candle's open/close
253
+ relationship (and therefore its up/down color) still reflects the real values, `formatLegend`
254
+ still shows the real OHLC numbers, and dragging the price axis or panning vertically still
255
+ feels like "grab and slide" in the same screen direction as before; only the sign of what that
256
+ drag does to the value range flips internally to keep it feeling that way. Applies to every
257
+ pane in the stack (see "Multi-pane indicators" below) consistently, not just the main one.
258
+
259
+ One known exception: candlestick's volume bars aren't mapped through the value-axis scale at
260
+ all (they're drawn in a fixed-height strip anchored to the bottom of the pane, independent of
261
+ price — see "Series types" under Architecture), so they stay bottom-anchored regardless of
262
+ `invertValueAxis` rather than flipping to the top with everything else.
263
+
230
264
  ### Reading chart state
231
265
 
232
266
  Useful for building UI around the canvas (a legend, a toolbar, a "jump to latest" button)
@@ -644,8 +678,9 @@ concrete drawing tool ships yet, only the mechanism a trend line or similar woul
644
678
  on. `addPane`/`removePane` let a plugin-drawn indicator (RSI, MACD, ...) reserve its own
645
679
  horizontal strip with an independent value axis — see "Multi-pane indicators" above; volume
646
680
  still shares the candlestick pane rather than getting its own, since it draws through the
647
- series itself, not a pane-targeted plugin. See [CHANGELOG.md](./CHANGELOG.md) for what shipped
648
- in each release.
681
+ series itself, not a pane-targeted plugin. `invertValueAxis`/`setInvertValueAxis` mirror the
682
+ whole stack's value axis top-to-bottom without touching the underlying data — see "Inverting
683
+ the value axis" above. See [CHANGELOG.md](./CHANGELOG.md) for what shipped in each release.
649
684
 
650
685
  ## License
651
686
 
package/dist/index.d.ts CHANGED
@@ -76,6 +76,11 @@ export declare class WickChart<TPoint extends SeriesPoint = Candle> {
76
76
  * every threshold crossing until `setData` resets it (a fresh dataset
77
77
  * may come from a different source that does have more). */
78
78
  private exhausted;
79
+ /** Mirrors `this.renderer`'s own copy (set at construction, kept in sync
80
+ * by `setInvertValueAxis`) — needed here too since pointer-event value
81
+ * conversion and price-axis drag direction happen outside `render()`,
82
+ * where only `WickChart` (not `ChartRenderer`) is involved. */
83
+ private invertValueAxis;
79
84
  constructor(canvas: HTMLCanvasElement, options?: WickChartOptions);
80
85
  setData(points: TPoint[]): this;
81
86
  /**
@@ -157,6 +162,17 @@ export declare class WickChart<TPoint extends SeriesPoint = Candle> {
157
162
  /** The point currently under the cursor (crosshair/legend target), or
158
163
  * `null` when nothing is hovered. */
159
164
  getHoveredPoint(): TPoint | null;
165
+ /**
166
+ * Mirrors the value axis top-to-bottom (or restores it) and re-renders —
167
+ * see `WickChartOptions.invertValueAxis` for what this actually changes.
168
+ * A live toggle, unlike most other style options: pan/zoom/valueRangeOverride
169
+ * state is untouched, so a "flip" button can call this on an existing
170
+ * chart without losing the user's current view, the same way
171
+ * `setPluginVisible` toggles a plugin without losing its state.
172
+ */
173
+ setInvertValueAxis(inverted: boolean): this;
174
+ /** Whether the value axis is currently mirrored — see `setInvertValueAxis`. */
175
+ isValueAxisInverted(): boolean;
160
176
  /** Removes all attached listeners. Call on unmount — the mouseup
161
177
  * listener is on `window` (so drags don't get stuck if the cursor
162
178
  * leaves the canvas mid-drag) and won't be garbage-collected on its own. */
package/dist/index.js CHANGED
@@ -3,6 +3,7 @@ import { computePaneLayout } from './paneLayout.js';
3
3
  import { ChartRenderer } from './renderer.js';
4
4
  import { getSeries } from './series/registry.js';
5
5
  import { toUnixSeconds } from './time.js';
6
+ import { pixelToValue, valueToPixel } from './valueAxis.js';
6
7
  import { Viewport } from './viewport.js';
7
8
  import { loadWasm } from './wasm.js';
8
9
  import { importRealWasm } from './wasmImporter.js';
@@ -302,6 +303,7 @@ export class WickChart {
302
303
  };
303
304
  this.seriesDefinition = getSeries(options?.type ?? 'candlestick');
304
305
  this.renderer = new ChartRenderer(canvas, this.seriesDefinition, options);
306
+ this.invertValueAxis = options?.invertValueAxis ?? false;
305
307
  this.viewport = new Viewport(0);
306
308
  // Without this, a touch drag on the canvas also scrolls/zooms the page
307
309
  // underneath it — the browser's native touch gestures and this class's
@@ -464,6 +466,24 @@ export class WickChart {
464
466
  getHoveredPoint() {
465
467
  return this.hoverIndex === null ? null : (this.sorted[this.hoverIndex] ?? null);
466
468
  }
469
+ /**
470
+ * Mirrors the value axis top-to-bottom (or restores it) and re-renders —
471
+ * see `WickChartOptions.invertValueAxis` for what this actually changes.
472
+ * A live toggle, unlike most other style options: pan/zoom/valueRangeOverride
473
+ * state is untouched, so a "flip" button can call this on an existing
474
+ * chart without losing the user's current view, the same way
475
+ * `setPluginVisible` toggles a plugin without losing its state.
476
+ */
477
+ setInvertValueAxis(inverted) {
478
+ this.invertValueAxis = inverted;
479
+ this.renderer.setInvertValueAxis(inverted);
480
+ this.scheduleRender();
481
+ return this;
482
+ }
483
+ /** Whether the value axis is currently mirrored — see `setInvertValueAxis`. */
484
+ isValueAxisInverted() {
485
+ return this.invertValueAxis;
486
+ }
467
487
  /** Removes all attached listeners. Call on unmount — the mouseup
468
488
  * listener is on `window` (so drags don't get stuck if the cursor
469
489
  * leaves the canvas mid-drag) and won't be garbage-collected on its own. */
@@ -527,7 +547,13 @@ export class WickChart {
527
547
  // Dragging down moves the visible value window down (content
528
548
  // follows the cursor), matching the horizontal drag's "grab and
529
549
  // slide" feel — see the pan call above for the mirrored X case.
530
- this.viewport.panValueRange(deltaYDevice * valuePerPixel);
550
+ // invertValueAxis flips which value-space direction "down the
551
+ // screen" corresponds to (see src/valueAxis.ts), so the sign here
552
+ // has to flip too or an inverted chart would pan backwards relative
553
+ // to the drag — negated, not re-derived, since the *magnitude* is
554
+ // identical either way.
555
+ const directionSign = this.invertValueAxis ? -1 : 1;
556
+ this.viewport.panValueRange(deltaYDevice * valuePerPixel * directionSign);
531
557
  }
532
558
  this.scheduleRender();
533
559
  }
@@ -674,7 +700,7 @@ export class WickChart {
674
700
  const chartHeight = this.mainPaneHeight();
675
701
  if (!range || chartHeight <= 0)
676
702
  return null;
677
- return range.min + (1 - y / chartHeight) * (range.max - range.min);
703
+ return pixelToValue(y, range.min, range.max, chartHeight, this.invertValueAxis);
678
704
  }
679
705
  /** x pixel -> global (possibly fractional) index — the exact inverse of
680
706
  * the renderer's own `xForIndex`, so a pointer event lines up with
@@ -702,7 +728,7 @@ export class WickChart {
702
728
  const chartHeight = this.mainPaneHeight();
703
729
  if (!range || chartHeight <= 0)
704
730
  return null;
705
- return chartHeight * (1 - (value - range.min) / (range.max - range.min));
731
+ return valueToPixel(value, range.min, range.max, chartHeight, this.invertValueAxis);
706
732
  }
707
733
  pointerEventAt(x, y) {
708
734
  return {
@@ -48,7 +48,14 @@ export declare class ChartRenderer<TPoint extends SeriesPoint> {
48
48
  private axis;
49
49
  private crosshair;
50
50
  private legend;
51
+ /** Unlike the style groups above, mutable after construction — see
52
+ * `setInvertValueAxis`. A live toggle, not a one-time style choice, is
53
+ * the whole point of this option (a "what if this series moved the
54
+ * opposite way" view a user flips on and off), so it doesn't get the
55
+ * "resolved once in the constructor" treatment those get. */
56
+ private invertValueAxis;
51
57
  constructor(canvas: HTMLCanvasElement, seriesDefinition: SeriesDefinition<TPoint, unknown>, options?: WickChartOptions);
58
+ setInvertValueAxis(inverted: boolean): void;
52
59
  /** Pixel width of the point-plotting area — excludes the price-axis
53
60
  * strip on the right. Exposed so `WickChart` can convert cursor pixel
54
61
  * positions to point indices / values for hit-testing and dragging. */
package/dist/renderer.js CHANGED
@@ -2,6 +2,7 @@ import { formatAxisLabel, formatHoverTime, pickTickIndices } from './axis.js';
2
2
  import { createScale } from './hybridScale.js';
3
3
  import { computePaneLayout } from './paneLayout.js';
4
4
  import { formatPrice, niceTicks } from './priceAxis.js';
5
+ import { pixelToValue, valueAxisPixelRange } from './valueAxis.js';
5
6
  const DEFAULT_BACKGROUND = 'transparent';
6
7
  const DEFAULT_FONT = {
7
8
  family: 'sans-serif',
@@ -61,6 +62,10 @@ export class ChartRenderer {
61
62
  this.axis = { ...DEFAULT_AXIS, ...options.axis };
62
63
  this.crosshair = { ...DEFAULT_CROSSHAIR, ...options.crosshair };
63
64
  this.legend = { ...DEFAULT_LEGEND, ...options.legend };
65
+ this.invertValueAxis = options.invertValueAxis ?? false;
66
+ }
67
+ setInvertValueAxis(inverted) {
68
+ this.invertValueAxis = inverted;
64
69
  }
65
70
  /** Pixel width of the point-plotting area — excludes the price-axis
66
71
  * strip on the right. Exposed so `WickChart` can convert cursor pixel
@@ -112,16 +117,20 @@ export class ChartRenderer {
112
117
  // JS below a few hundred points, WASM above — see hybridScale.ts.
113
118
  // Whichever it picks, `dispose()` must run once we're done reading
114
119
  // from it (a no-op on the JS path, a real WASM memory free otherwise).
115
- const { scale: yScale, dispose: disposeYScale } = createScale(valueMin, valueMax, chartHeight, 0, visible.length);
120
+ // valueAxisPixelRange picks which pixel end is the domain minimum —
121
+ // the one thing invertValueAxis actually changes about this call.
122
+ const { scale: yScale, dispose: disposeYScale } = createScale(valueMin, valueMax, ...valueAxisPixelRange(chartHeight, this.invertValueAxis), visible.length);
116
123
  // Every indicator pane gets the exact same treatment as the main pane
117
124
  // — its own value domain (from `PaneOptions.getValueRange`) and its
118
125
  // own JS/WASM scale over its own pixel height — kept alive for the
119
126
  // whole frame alongside `yScale`, since plugins targeting a pane draw
120
- // only after every pane's axis has already been rendered.
127
+ // only after every pane's axis has already been rendered. Inverted the
128
+ // same way as the main pane so a chart's invertValueAxis option
129
+ // mirrors its whole stack consistently, not just the price pane.
121
130
  const paneScales = paneRects.map((rect, i) => {
122
131
  const pane = panes[i];
123
132
  const { min, max } = pane.getValueRange();
124
- const { scale, dispose } = createScale(min, max, rect.height, 0, visible.length);
133
+ const { scale, dispose } = createScale(min, max, ...valueAxisPixelRange(rect.height, this.invertValueAxis), visible.length);
125
134
  return { pane, rect, min, max, scale, dispose };
126
135
  });
127
136
  // Flipped in `finally`, right before every scale above frees its WASM
@@ -249,9 +258,10 @@ export class ChartRenderer {
249
258
  },
250
259
  indexForX,
251
260
  // Exact inverse of the mapping above: subtract the pane's top offset
252
- // before inverting the same [valueMin, valueMax] -> [rect.height, 0]
253
- // mapping createScale set up for it.
254
- valueForY: (y) => valueMin + (1 - (y - rect.top) / rect.height) * (valueMax - valueMin),
261
+ // before inverting the same value<->pixel mapping createScale set up
262
+ // for it (see src/valueAxis.ts — same invertValueAxis flag, so this
263
+ // stays consistent with whichever direction the pane actually drew in).
264
+ valueForY: (y) => pixelToValue(y - rect.top, valueMin, valueMax, rect.height, this.invertValueAxis),
255
265
  visibleStartIndex,
256
266
  visibleEndIndex,
257
267
  allPoints,
@@ -358,8 +368,9 @@ export class ChartRenderer {
358
368
  ctx.restore();
359
369
  if (priceLineVisible) {
360
370
  // Exact inverse of the value->y mapping createScale set up for this
361
- // frame — same formula as PluginRenderApi.valueForY.
362
- const value = valueMin + (1 - hoverY / chartHeight) * (valueMax - valueMin);
371
+ // frame — same helper (and same invertValueAxis flag) as
372
+ // PluginRenderApi.valueForY, see src/valueAxis.ts.
373
+ const value = pixelToValue(hoverY, valueMin, valueMax, chartHeight, this.invertValueAxis);
363
374
  this.renderPriceLabelChip(formatPrice(value, priceStep), hoverY, chartWidth);
364
375
  }
365
376
  this.renderTimeLabelChip(formatHoverTime(timeSeconds), x, chartHeight, canvas.width);
package/dist/types.d.ts CHANGED
@@ -173,9 +173,9 @@ export interface WickChartOptions {
173
173
  /**
174
174
  * Which registered series type to render this chart as (see
175
175
  * `registerSeries` in `src/series/registry.ts`). Defaults to
176
- * `'candlestick'`, the only type built into the library today — adding a
177
- * new one is a matter of implementing `SeriesDefinition` and registering
178
- * it, without changing `WickChart` or `ChartRenderer` at all.
176
+ * `'candlestick'`; `'line'` is the other type built into the library —
177
+ * adding a new one is a matter of implementing `SeriesDefinition` and
178
+ * registering it, without changing `WickChart` or `ChartRenderer` at all.
179
179
  */
180
180
  type?: string;
181
181
  /** Background color of the canvas. Defaults to transparent. Chart-wide
@@ -199,4 +199,15 @@ export interface WickChartOptions {
199
199
  crosshair?: ChartCrosshairOptions;
200
200
  /** Hover legend coloring. Merged over the built-in defaults field by field. */
201
201
  legend?: ChartLegendOptions;
202
+ /**
203
+ * Mirrors the value axis top-to-bottom — every pane's higher values
204
+ * render lower on screen instead of higher, with no change to the
205
+ * underlying data (a candle's open/close relationship, and therefore
206
+ * its up/down color, is unaffected). Defaults to `false`. Useful for a
207
+ * "what if this series had moved the opposite way" view. Can also be
208
+ * toggled after construction via `WickChart.setInvertValueAxis` without
209
+ * losing pan/zoom state — see `src/valueAxis.ts` for the mapping this
210
+ * flips.
211
+ */
212
+ invertValueAxis?: boolean;
202
213
  }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Pure pixel<->value mapping helpers for a chart's value axis, factored out
3
+ * so `WickChartOptions.invertValueAxis` (see `src/index.ts`) has exactly
4
+ * one place to change the y-direction convention instead of several
5
+ * independently re-derived copies of the same formula. Before this file
6
+ * existed, `ChartRenderer`'s hover-crosshair value readout, its per-pane
7
+ * `PluginRenderApi.valueForY`, and `WickChart`'s own pointer-event
8
+ * `value`/`yForValue` each inlined the same "pixel -> value" arithmetic
9
+ * separately — harmless while there was only ever one direction, but
10
+ * exactly the kind of duplication that turns "add an invert option" into
11
+ * "find and fix four copies of a formula, hope none were missed."
12
+ *
13
+ * The forward, per-point-in-a-frame hot path stays on `Scale`
14
+ * (`src/hybridScale.ts`, JS or WASM) for its own reasons — batched
15
+ * `mapMany`, no per-point allocation. `valueAxisPixelRange` only decides
16
+ * which pixel end a `Scale` should treat as the domain minimum, so
17
+ * inverting is a one-line change to how a `Scale` gets constructed rather
18
+ * than a second rendering path. `valueToPixel`/`pixelToValue` below are for
19
+ * the comparatively rare user-gesture paths (hover crosshair, pointer
20
+ * events, price-axis dragging) where a `Scale` instance either doesn't
21
+ * exist yet (these happen between frames) or isn't worth constructing for
22
+ * a single one-off conversion.
23
+ */
24
+ /**
25
+ * The `[rangeMin, rangeMax]` pair to construct a value-axis `Scale` with:
26
+ * `createScale(domainMin, domainMax, ...valueAxisPixelRange(pixelSpan, inverted))`.
27
+ * Not inverted (the default for every chart): the domain maximum renders
28
+ * at pixel 0 (the top of the pane) and the minimum at `pixelSpan` (the
29
+ * bottom). Inverted: the same two pixels, swapped — every value renders
30
+ * mirrored top-to-bottom, with no change to the underlying data.
31
+ */
32
+ export declare function valueAxisPixelRange(pixelSpan: number, inverted: boolean): [number, number];
33
+ /**
34
+ * value -> pixel, the exact forward direction `valueAxisPixelRange` sets a
35
+ * `Scale` up for. `pixelSpan` is the pane's own height (or a candidate
36
+ * one — this has no dependency on `Scale` or a live frame).
37
+ */
38
+ export declare function valueToPixel(value: number, valueMin: number, valueMax: number, pixelSpan: number, inverted: boolean): number;
39
+ /**
40
+ * pixel -> value, the exact inverse of `valueToPixel` above —
41
+ * `valueToPixel(pixelToValue(pixel, ...), ...) === pixel` for any `pixel`
42
+ * in `[0, pixelSpan]`.
43
+ */
44
+ export declare function pixelToValue(pixel: number, valueMin: number, valueMax: number, pixelSpan: number, inverted: boolean): number;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Pure pixel<->value mapping helpers for a chart's value axis, factored out
3
+ * so `WickChartOptions.invertValueAxis` (see `src/index.ts`) has exactly
4
+ * one place to change the y-direction convention instead of several
5
+ * independently re-derived copies of the same formula. Before this file
6
+ * existed, `ChartRenderer`'s hover-crosshair value readout, its per-pane
7
+ * `PluginRenderApi.valueForY`, and `WickChart`'s own pointer-event
8
+ * `value`/`yForValue` each inlined the same "pixel -> value" arithmetic
9
+ * separately — harmless while there was only ever one direction, but
10
+ * exactly the kind of duplication that turns "add an invert option" into
11
+ * "find and fix four copies of a formula, hope none were missed."
12
+ *
13
+ * The forward, per-point-in-a-frame hot path stays on `Scale`
14
+ * (`src/hybridScale.ts`, JS or WASM) for its own reasons — batched
15
+ * `mapMany`, no per-point allocation. `valueAxisPixelRange` only decides
16
+ * which pixel end a `Scale` should treat as the domain minimum, so
17
+ * inverting is a one-line change to how a `Scale` gets constructed rather
18
+ * than a second rendering path. `valueToPixel`/`pixelToValue` below are for
19
+ * the comparatively rare user-gesture paths (hover crosshair, pointer
20
+ * events, price-axis dragging) where a `Scale` instance either doesn't
21
+ * exist yet (these happen between frames) or isn't worth constructing for
22
+ * a single one-off conversion.
23
+ */
24
+ /**
25
+ * The `[rangeMin, rangeMax]` pair to construct a value-axis `Scale` with:
26
+ * `createScale(domainMin, domainMax, ...valueAxisPixelRange(pixelSpan, inverted))`.
27
+ * Not inverted (the default for every chart): the domain maximum renders
28
+ * at pixel 0 (the top of the pane) and the minimum at `pixelSpan` (the
29
+ * bottom). Inverted: the same two pixels, swapped — every value renders
30
+ * mirrored top-to-bottom, with no change to the underlying data.
31
+ */
32
+ export function valueAxisPixelRange(pixelSpan, inverted) {
33
+ return inverted ? [0, pixelSpan] : [pixelSpan, 0];
34
+ }
35
+ /**
36
+ * value -> pixel, the exact forward direction `valueAxisPixelRange` sets a
37
+ * `Scale` up for. `pixelSpan` is the pane's own height (or a candidate
38
+ * one — this has no dependency on `Scale` or a live frame).
39
+ */
40
+ export function valueToPixel(value, valueMin, valueMax, pixelSpan, inverted) {
41
+ const t = (value - valueMin) / (valueMax - valueMin);
42
+ return inverted ? t * pixelSpan : (1 - t) * pixelSpan;
43
+ }
44
+ /**
45
+ * pixel -> value, the exact inverse of `valueToPixel` above —
46
+ * `valueToPixel(pixelToValue(pixel, ...), ...) === pixel` for any `pixel`
47
+ * in `[0, pixelSpan]`.
48
+ */
49
+ export function pixelToValue(pixel, valueMin, valueMax, pixelSpan, inverted) {
50
+ const t = inverted ? pixel / pixelSpan : 1 - pixel / pixelSpan;
51
+ return valueMin + t * (valueMax - valueMin);
52
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wick-charts",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "An open-source financial charting library — WASM (Rust) for compute, Canvas2D for rendering.",
5
5
  "license": "MIT",
6
6
  "author": "eatnows",