hyperprop-charting-library 0.1.21 → 0.1.22

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
@@ -76,6 +76,55 @@ chart.onCrosshairPriceAction((event) => {
76
76
  });
77
77
  ```
78
78
 
79
+ ## Built-In Indicators
80
+
81
+ Built-ins are part of this package (no extra install):
82
+
83
+ - `volume`
84
+ - `sma`
85
+ - `ema`
86
+ - `rsi`
87
+ - `wma`
88
+ - `vwma`
89
+ - `rma`
90
+ - `hma`
91
+ - `stddev`
92
+ - `atr`
93
+
94
+ Quick usage:
95
+
96
+ ```ts
97
+ const emaId = chart.addIndicator("ema", { length: 34, source: "close" });
98
+ const volumeId = chart.addIndicator("volume", { upOpacity: 0.72, downOpacity: 0.72 }, { paneHeightRatio: 0.16 });
99
+
100
+ // update any indicator instance
101
+ chart.updateIndicator(emaId, { inputs: { length: 55 } });
102
+ chart.updateIndicator(volumeId, { paneHeightRatio: 0.1 });
103
+ ```
104
+
105
+ Volume note:
106
+
107
+ - `volume` and `vwma` require `OhlcDataPoint.v` for best results.
108
+
109
+ ## Custom Indicator Plugins
110
+
111
+ You can register your own indicators in-app:
112
+
113
+ ```ts
114
+ chart.registerIndicator({
115
+ id: "my-line",
116
+ name: "My Line",
117
+ pane: "overlay",
118
+ defaultInputs: { color: "#22d3ee", width: 2 },
119
+ draw: (ctx, rc, inputs) => {
120
+ if (!rc.yFromPrice) return;
121
+ // draw using rc.xFromIndex(...) and rc.yFromPrice(...)
122
+ }
123
+ });
124
+
125
+ const myId = chart.addIndicator("my-line");
126
+ ```
127
+
79
128
  ## Full Documentation
80
129
 
81
130
  - API reference: `docs/API.md`
@@ -92,6 +141,7 @@ chart.onCrosshairPriceAction((event) => {
92
141
  - `chart.setOrderLines(lines)` / `chart.addOrderLine(line)` / `chart.updateOrderLine(id, patch)` / `chart.removeOrderLine(id)`
93
142
  - `chart.onOrderAction(handler)` / `chart.onChartClick(handler)` / `chart.onCrosshairMove(handler)` / `chart.onCrosshairPriceAction(handler)`
94
143
  - `chart.setDoubleClickEnabled(enabled)` / `chart.setDoubleClickAction(action)`
144
+ - `chart.registerIndicator(plugin)` / `chart.addIndicator(type, inputs?, options?)` / `chart.updateIndicator(id, patch)` / `chart.removeIndicator(id)`
95
145
  - `chart.zoomInX()` / `chart.zoomOutX()` / `chart.panX(bars)` / `chart.resetViewport()`
96
146
  - `chart.resize(width, height)` / `chart.destroy()`
97
147
 
@@ -478,16 +478,21 @@ var drawSeparateSeries = (ctx, renderContext, values, color, width, minOverride,
478
478
  var BUILTIN_VOLUME_INDICATOR = {
479
479
  id: "volume",
480
480
  name: "Volume",
481
- pane: "separate",
481
+ pane: "overlay",
482
482
  paneHeightRatio: 0.22,
483
483
  defaultInputs: {
484
484
  upOpacity: 0.7,
485
485
  downOpacity: 0.7,
486
- minBarWidth: 1
486
+ minBarWidth: 1,
487
+ overlayHeightRatio: 0.22
487
488
  },
488
489
  draw: (ctx, renderContext, inputs) => {
489
490
  const { data, startIndex, endIndex, chartTop, chartBottom, candleSpacing, xFromIndex, upColor, downColor } = renderContext;
490
- const paneHeight = Math.max(1, chartBottom - chartTop);
491
+ const fullPaneHeight = Math.max(1, chartBottom - chartTop);
492
+ const overlayMode = renderContext.yFromPrice !== null;
493
+ const overlayHeightRatio = Math.min(0.45, Math.max(0.08, Number(inputs.overlayHeightRatio) || 0.22));
494
+ const paneHeight = overlayMode ? Math.max(20, Math.round(fullPaneHeight * overlayHeightRatio)) : fullPaneHeight;
495
+ const paneBottom = chartBottom;
491
496
  const visiblePoints = data.slice(startIndex, endIndex + 1);
492
497
  const maxVolume = Math.max(1, ...visiblePoints.map((point) => point.v ?? 0));
493
498
  const barWidth = Math.max(
@@ -505,7 +510,7 @@ var BUILTIN_VOLUME_INDICATOR = {
505
510
  const volumeHeight = Math.max(1, Math.round(paneHeight * ratio));
506
511
  const xCenter = xFromIndex(index);
507
512
  const barX = Math.round(xCenter - barWidth / 2);
508
- const barY = Math.round(chartBottom - volumeHeight);
513
+ const barY = Math.round(paneBottom - volumeHeight);
509
514
  const isUp = point.c >= point.o;
510
515
  const opacity = isUp ? upOpacity : downOpacity;
511
516
  ctx.save();
@@ -702,6 +707,7 @@ function createChart(element, options = {}) {
702
707
  type: indicator.type,
703
708
  visible: indicator.visible ?? true,
704
709
  pane: indicator.pane ?? plugin?.pane ?? "overlay",
710
+ ...indicator.paneHeightRatio === void 0 ? {} : { paneHeightRatio: indicator.paneHeightRatio },
705
711
  inputs: {
706
712
  ...defaults,
707
713
  ...indicator.inputs ?? {}
@@ -1360,8 +1366,8 @@ function createChart(element, options = {}) {
1360
1366
  const activeSeparateIndicators = indicators.filter((indicator) => indicator.visible).map((indicator) => ({ indicator, plugin: indicatorRegistry.get(indicator.type) })).filter(
1361
1367
  (value) => value.plugin !== void 0 && (value.indicator.pane ?? value.plugin.pane ?? "overlay") === "separate"
1362
1368
  );
1363
- const separatePaneHeightDefaults = activeSeparateIndicators.map(({ plugin }) => {
1364
- const ratio = Math.min(0.45, Math.max(0.08, plugin.paneHeightRatio ?? 0.22));
1369
+ const separatePaneHeightDefaults = activeSeparateIndicators.map(({ indicator, plugin }) => {
1370
+ const ratio = Math.min(0.45, Math.max(0.08, indicator.paneHeightRatio ?? plugin.paneHeightRatio ?? 0.22));
1365
1371
  return Math.round(fullChartHeight * ratio);
1366
1372
  });
1367
1373
  const separatePaneDesiredTotal = separatePaneHeightDefaults.reduce((sum, value) => sum + value, 0) + Math.max(0, activeSeparateIndicators.length - 1) * separatePaneSpacing;
@@ -1665,7 +1671,6 @@ function createChart(element, options = {}) {
1665
1671
  ctx.lineTo(crisp(chartRight), crisp(paneTop));
1666
1672
  ctx.stroke();
1667
1673
  ctx.restore();
1668
- drawText(plugin.name.toUpperCase(), chartLeft + 6, paneTop + 12, "left", "middle", axis.textColor);
1669
1674
  });
1670
1675
  }
1671
1676
  ctx.strokeStyle = axis.lineColor;
@@ -2535,6 +2540,7 @@ function createChart(element, options = {}) {
2535
2540
  ...indicator,
2536
2541
  visible: patch.visible ?? indicator.visible,
2537
2542
  pane: patch.pane ?? indicator.pane ?? plugin?.pane ?? "overlay",
2543
+ ...patch.paneHeightRatio !== void 0 || indicator.paneHeightRatio !== void 0 ? { paneHeightRatio: patch.paneHeightRatio ?? indicator.paneHeightRatio } : {},
2538
2544
  type: patch.type ?? indicator.type,
2539
2545
  inputs: {
2540
2546
  ...indicator.inputs,
@@ -41,6 +41,7 @@ interface IndicatorInstanceOptions<TInputs extends Record<string, unknown> = Rec
41
41
  type: string;
42
42
  visible?: boolean;
43
43
  pane?: IndicatorPane;
44
+ paneHeightRatio?: number;
44
45
  inputs?: Partial<TInputs>;
45
46
  }
46
47
  interface IndicatorRenderContext {
@@ -454,16 +454,21 @@ var drawSeparateSeries = (ctx, renderContext, values, color, width, minOverride,
454
454
  var BUILTIN_VOLUME_INDICATOR = {
455
455
  id: "volume",
456
456
  name: "Volume",
457
- pane: "separate",
457
+ pane: "overlay",
458
458
  paneHeightRatio: 0.22,
459
459
  defaultInputs: {
460
460
  upOpacity: 0.7,
461
461
  downOpacity: 0.7,
462
- minBarWidth: 1
462
+ minBarWidth: 1,
463
+ overlayHeightRatio: 0.22
463
464
  },
464
465
  draw: (ctx, renderContext, inputs) => {
465
466
  const { data, startIndex, endIndex, chartTop, chartBottom, candleSpacing, xFromIndex, upColor, downColor } = renderContext;
466
- const paneHeight = Math.max(1, chartBottom - chartTop);
467
+ const fullPaneHeight = Math.max(1, chartBottom - chartTop);
468
+ const overlayMode = renderContext.yFromPrice !== null;
469
+ const overlayHeightRatio = Math.min(0.45, Math.max(0.08, Number(inputs.overlayHeightRatio) || 0.22));
470
+ const paneHeight = overlayMode ? Math.max(20, Math.round(fullPaneHeight * overlayHeightRatio)) : fullPaneHeight;
471
+ const paneBottom = chartBottom;
467
472
  const visiblePoints = data.slice(startIndex, endIndex + 1);
468
473
  const maxVolume = Math.max(1, ...visiblePoints.map((point) => point.v ?? 0));
469
474
  const barWidth = Math.max(
@@ -481,7 +486,7 @@ var BUILTIN_VOLUME_INDICATOR = {
481
486
  const volumeHeight = Math.max(1, Math.round(paneHeight * ratio));
482
487
  const xCenter = xFromIndex(index);
483
488
  const barX = Math.round(xCenter - barWidth / 2);
484
- const barY = Math.round(chartBottom - volumeHeight);
489
+ const barY = Math.round(paneBottom - volumeHeight);
485
490
  const isUp = point.c >= point.o;
486
491
  const opacity = isUp ? upOpacity : downOpacity;
487
492
  ctx.save();
@@ -678,6 +683,7 @@ function createChart(element, options = {}) {
678
683
  type: indicator.type,
679
684
  visible: indicator.visible ?? true,
680
685
  pane: indicator.pane ?? plugin?.pane ?? "overlay",
686
+ ...indicator.paneHeightRatio === void 0 ? {} : { paneHeightRatio: indicator.paneHeightRatio },
681
687
  inputs: {
682
688
  ...defaults,
683
689
  ...indicator.inputs ?? {}
@@ -1336,8 +1342,8 @@ function createChart(element, options = {}) {
1336
1342
  const activeSeparateIndicators = indicators.filter((indicator) => indicator.visible).map((indicator) => ({ indicator, plugin: indicatorRegistry.get(indicator.type) })).filter(
1337
1343
  (value) => value.plugin !== void 0 && (value.indicator.pane ?? value.plugin.pane ?? "overlay") === "separate"
1338
1344
  );
1339
- const separatePaneHeightDefaults = activeSeparateIndicators.map(({ plugin }) => {
1340
- const ratio = Math.min(0.45, Math.max(0.08, plugin.paneHeightRatio ?? 0.22));
1345
+ const separatePaneHeightDefaults = activeSeparateIndicators.map(({ indicator, plugin }) => {
1346
+ const ratio = Math.min(0.45, Math.max(0.08, indicator.paneHeightRatio ?? plugin.paneHeightRatio ?? 0.22));
1341
1347
  return Math.round(fullChartHeight * ratio);
1342
1348
  });
1343
1349
  const separatePaneDesiredTotal = separatePaneHeightDefaults.reduce((sum, value) => sum + value, 0) + Math.max(0, activeSeparateIndicators.length - 1) * separatePaneSpacing;
@@ -1641,7 +1647,6 @@ function createChart(element, options = {}) {
1641
1647
  ctx.lineTo(crisp(chartRight), crisp(paneTop));
1642
1648
  ctx.stroke();
1643
1649
  ctx.restore();
1644
- drawText(plugin.name.toUpperCase(), chartLeft + 6, paneTop + 12, "left", "middle", axis.textColor);
1645
1650
  });
1646
1651
  }
1647
1652
  ctx.strokeStyle = axis.lineColor;
@@ -2511,6 +2516,7 @@ function createChart(element, options = {}) {
2511
2516
  ...indicator,
2512
2517
  visible: patch.visible ?? indicator.visible,
2513
2518
  pane: patch.pane ?? indicator.pane ?? plugin?.pane ?? "overlay",
2519
+ ...patch.paneHeightRatio !== void 0 || indicator.paneHeightRatio !== void 0 ? { paneHeightRatio: patch.paneHeightRatio ?? indicator.paneHeightRatio } : {},
2514
2520
  type: patch.type ?? indicator.type,
2515
2521
  inputs: {
2516
2522
  ...indicator.inputs,
package/dist/index.cjs CHANGED
@@ -478,16 +478,21 @@ var drawSeparateSeries = (ctx, renderContext, values, color, width, minOverride,
478
478
  var BUILTIN_VOLUME_INDICATOR = {
479
479
  id: "volume",
480
480
  name: "Volume",
481
- pane: "separate",
481
+ pane: "overlay",
482
482
  paneHeightRatio: 0.22,
483
483
  defaultInputs: {
484
484
  upOpacity: 0.7,
485
485
  downOpacity: 0.7,
486
- minBarWidth: 1
486
+ minBarWidth: 1,
487
+ overlayHeightRatio: 0.22
487
488
  },
488
489
  draw: (ctx, renderContext, inputs) => {
489
490
  const { data, startIndex, endIndex, chartTop, chartBottom, candleSpacing, xFromIndex, upColor, downColor } = renderContext;
490
- const paneHeight = Math.max(1, chartBottom - chartTop);
491
+ const fullPaneHeight = Math.max(1, chartBottom - chartTop);
492
+ const overlayMode = renderContext.yFromPrice !== null;
493
+ const overlayHeightRatio = Math.min(0.45, Math.max(0.08, Number(inputs.overlayHeightRatio) || 0.22));
494
+ const paneHeight = overlayMode ? Math.max(20, Math.round(fullPaneHeight * overlayHeightRatio)) : fullPaneHeight;
495
+ const paneBottom = chartBottom;
491
496
  const visiblePoints = data.slice(startIndex, endIndex + 1);
492
497
  const maxVolume = Math.max(1, ...visiblePoints.map((point) => point.v ?? 0));
493
498
  const barWidth = Math.max(
@@ -505,7 +510,7 @@ var BUILTIN_VOLUME_INDICATOR = {
505
510
  const volumeHeight = Math.max(1, Math.round(paneHeight * ratio));
506
511
  const xCenter = xFromIndex(index);
507
512
  const barX = Math.round(xCenter - barWidth / 2);
508
- const barY = Math.round(chartBottom - volumeHeight);
513
+ const barY = Math.round(paneBottom - volumeHeight);
509
514
  const isUp = point.c >= point.o;
510
515
  const opacity = isUp ? upOpacity : downOpacity;
511
516
  ctx.save();
@@ -702,6 +707,7 @@ function createChart(element, options = {}) {
702
707
  type: indicator.type,
703
708
  visible: indicator.visible ?? true,
704
709
  pane: indicator.pane ?? plugin?.pane ?? "overlay",
710
+ ...indicator.paneHeightRatio === void 0 ? {} : { paneHeightRatio: indicator.paneHeightRatio },
705
711
  inputs: {
706
712
  ...defaults,
707
713
  ...indicator.inputs ?? {}
@@ -1360,8 +1366,8 @@ function createChart(element, options = {}) {
1360
1366
  const activeSeparateIndicators = indicators.filter((indicator) => indicator.visible).map((indicator) => ({ indicator, plugin: indicatorRegistry.get(indicator.type) })).filter(
1361
1367
  (value) => value.plugin !== void 0 && (value.indicator.pane ?? value.plugin.pane ?? "overlay") === "separate"
1362
1368
  );
1363
- const separatePaneHeightDefaults = activeSeparateIndicators.map(({ plugin }) => {
1364
- const ratio = Math.min(0.45, Math.max(0.08, plugin.paneHeightRatio ?? 0.22));
1369
+ const separatePaneHeightDefaults = activeSeparateIndicators.map(({ indicator, plugin }) => {
1370
+ const ratio = Math.min(0.45, Math.max(0.08, indicator.paneHeightRatio ?? plugin.paneHeightRatio ?? 0.22));
1365
1371
  return Math.round(fullChartHeight * ratio);
1366
1372
  });
1367
1373
  const separatePaneDesiredTotal = separatePaneHeightDefaults.reduce((sum, value) => sum + value, 0) + Math.max(0, activeSeparateIndicators.length - 1) * separatePaneSpacing;
@@ -1665,7 +1671,6 @@ function createChart(element, options = {}) {
1665
1671
  ctx.lineTo(crisp(chartRight), crisp(paneTop));
1666
1672
  ctx.stroke();
1667
1673
  ctx.restore();
1668
- drawText(plugin.name.toUpperCase(), chartLeft + 6, paneTop + 12, "left", "middle", axis.textColor);
1669
1674
  });
1670
1675
  }
1671
1676
  ctx.strokeStyle = axis.lineColor;
@@ -2535,6 +2540,7 @@ function createChart(element, options = {}) {
2535
2540
  ...indicator,
2536
2541
  visible: patch.visible ?? indicator.visible,
2537
2542
  pane: patch.pane ?? indicator.pane ?? plugin?.pane ?? "overlay",
2543
+ ...patch.paneHeightRatio !== void 0 || indicator.paneHeightRatio !== void 0 ? { paneHeightRatio: patch.paneHeightRatio ?? indicator.paneHeightRatio } : {},
2538
2544
  type: patch.type ?? indicator.type,
2539
2545
  inputs: {
2540
2546
  ...indicator.inputs,
package/dist/index.d.cts CHANGED
@@ -41,6 +41,7 @@ interface IndicatorInstanceOptions<TInputs extends Record<string, unknown> = Rec
41
41
  type: string;
42
42
  visible?: boolean;
43
43
  pane?: IndicatorPane;
44
+ paneHeightRatio?: number;
44
45
  inputs?: Partial<TInputs>;
45
46
  }
46
47
  interface IndicatorRenderContext {
package/dist/index.d.ts CHANGED
@@ -41,6 +41,7 @@ interface IndicatorInstanceOptions<TInputs extends Record<string, unknown> = Rec
41
41
  type: string;
42
42
  visible?: boolean;
43
43
  pane?: IndicatorPane;
44
+ paneHeightRatio?: number;
44
45
  inputs?: Partial<TInputs>;
45
46
  }
46
47
  interface IndicatorRenderContext {
package/dist/index.js CHANGED
@@ -454,16 +454,21 @@ var drawSeparateSeries = (ctx, renderContext, values, color, width, minOverride,
454
454
  var BUILTIN_VOLUME_INDICATOR = {
455
455
  id: "volume",
456
456
  name: "Volume",
457
- pane: "separate",
457
+ pane: "overlay",
458
458
  paneHeightRatio: 0.22,
459
459
  defaultInputs: {
460
460
  upOpacity: 0.7,
461
461
  downOpacity: 0.7,
462
- minBarWidth: 1
462
+ minBarWidth: 1,
463
+ overlayHeightRatio: 0.22
463
464
  },
464
465
  draw: (ctx, renderContext, inputs) => {
465
466
  const { data, startIndex, endIndex, chartTop, chartBottom, candleSpacing, xFromIndex, upColor, downColor } = renderContext;
466
- const paneHeight = Math.max(1, chartBottom - chartTop);
467
+ const fullPaneHeight = Math.max(1, chartBottom - chartTop);
468
+ const overlayMode = renderContext.yFromPrice !== null;
469
+ const overlayHeightRatio = Math.min(0.45, Math.max(0.08, Number(inputs.overlayHeightRatio) || 0.22));
470
+ const paneHeight = overlayMode ? Math.max(20, Math.round(fullPaneHeight * overlayHeightRatio)) : fullPaneHeight;
471
+ const paneBottom = chartBottom;
467
472
  const visiblePoints = data.slice(startIndex, endIndex + 1);
468
473
  const maxVolume = Math.max(1, ...visiblePoints.map((point) => point.v ?? 0));
469
474
  const barWidth = Math.max(
@@ -481,7 +486,7 @@ var BUILTIN_VOLUME_INDICATOR = {
481
486
  const volumeHeight = Math.max(1, Math.round(paneHeight * ratio));
482
487
  const xCenter = xFromIndex(index);
483
488
  const barX = Math.round(xCenter - barWidth / 2);
484
- const barY = Math.round(chartBottom - volumeHeight);
489
+ const barY = Math.round(paneBottom - volumeHeight);
485
490
  const isUp = point.c >= point.o;
486
491
  const opacity = isUp ? upOpacity : downOpacity;
487
492
  ctx.save();
@@ -678,6 +683,7 @@ function createChart(element, options = {}) {
678
683
  type: indicator.type,
679
684
  visible: indicator.visible ?? true,
680
685
  pane: indicator.pane ?? plugin?.pane ?? "overlay",
686
+ ...indicator.paneHeightRatio === void 0 ? {} : { paneHeightRatio: indicator.paneHeightRatio },
681
687
  inputs: {
682
688
  ...defaults,
683
689
  ...indicator.inputs ?? {}
@@ -1336,8 +1342,8 @@ function createChart(element, options = {}) {
1336
1342
  const activeSeparateIndicators = indicators.filter((indicator) => indicator.visible).map((indicator) => ({ indicator, plugin: indicatorRegistry.get(indicator.type) })).filter(
1337
1343
  (value) => value.plugin !== void 0 && (value.indicator.pane ?? value.plugin.pane ?? "overlay") === "separate"
1338
1344
  );
1339
- const separatePaneHeightDefaults = activeSeparateIndicators.map(({ plugin }) => {
1340
- const ratio = Math.min(0.45, Math.max(0.08, plugin.paneHeightRatio ?? 0.22));
1345
+ const separatePaneHeightDefaults = activeSeparateIndicators.map(({ indicator, plugin }) => {
1346
+ const ratio = Math.min(0.45, Math.max(0.08, indicator.paneHeightRatio ?? plugin.paneHeightRatio ?? 0.22));
1341
1347
  return Math.round(fullChartHeight * ratio);
1342
1348
  });
1343
1349
  const separatePaneDesiredTotal = separatePaneHeightDefaults.reduce((sum, value) => sum + value, 0) + Math.max(0, activeSeparateIndicators.length - 1) * separatePaneSpacing;
@@ -1641,7 +1647,6 @@ function createChart(element, options = {}) {
1641
1647
  ctx.lineTo(crisp(chartRight), crisp(paneTop));
1642
1648
  ctx.stroke();
1643
1649
  ctx.restore();
1644
- drawText(plugin.name.toUpperCase(), chartLeft + 6, paneTop + 12, "left", "middle", axis.textColor);
1645
1650
  });
1646
1651
  }
1647
1652
  ctx.strokeStyle = axis.lineColor;
@@ -2511,6 +2516,7 @@ function createChart(element, options = {}) {
2511
2516
  ...indicator,
2512
2517
  visible: patch.visible ?? indicator.visible,
2513
2518
  pane: patch.pane ?? indicator.pane ?? plugin?.pane ?? "overlay",
2519
+ ...patch.paneHeightRatio !== void 0 || indicator.paneHeightRatio !== void 0 ? { paneHeightRatio: patch.paneHeightRatio ?? indicator.paneHeightRatio } : {},
2514
2520
  type: patch.type ?? indicator.type,
2515
2521
  inputs: {
2516
2522
  ...indicator.inputs,
package/docs/API.md CHANGED
@@ -245,6 +245,7 @@ Connector/fill visuals:
245
245
  - `type: string` (registered indicator id, e.g. `"volume"`)
246
246
  - `visible?: boolean` (default `true`)
247
247
  - `pane?: "overlay" | "separate"`
248
+ - `paneHeightRatio?: number` (for separate panes; `0.08` to `0.45` recommended)
248
249
  - `inputs?: Record<string, unknown>` (plugin-specific parameters)
249
250
 
250
251
  ### `IndicatorPlugin`
@@ -256,9 +257,43 @@ Connector/fill visuals:
256
257
  - `defaultInputs?: Record<string, unknown>`
257
258
  - `draw(ctx, renderContext, inputs): void`
258
259
 
260
+ `IndicatorRenderContext` includes:
261
+
262
+ - shared x-window and visible index range (`startIndex`, `endIndex`, `xStart`, `xSpan`)
263
+ - pane bounds (`chartLeft`, `chartRight`, `chartTop`, `chartBottom`, `chartWidth`, `chartHeight`)
264
+ - `xFromIndex(index)` helper
265
+ - `yFromPrice(price)` helper (available for overlay indicators, `null` for separate-pane indicators)
266
+ - theme colors (`upColor`, `downColor`)
267
+
259
268
  Built-in:
260
269
 
261
- - `"volume"`: histogram in a bottom separate pane (uses `OhlcDataPoint.v`)
270
+ - `"volume"`: separate pane histogram (uses `OhlcDataPoint.v`)
271
+ - `"sma"`: Simple Moving Average (overlay)
272
+ - `"ema"`: Exponential Moving Average (overlay)
273
+ - `"rsi"`: Relative Strength Index (separate pane, 30/50/70 guides)
274
+ - `"wma"`: Weighted Moving Average (overlay)
275
+ - `"vwma"`: Volume Weighted Moving Average (overlay, uses `OhlcDataPoint.v`)
276
+ - `"rma"`: Wilder's Moving Average (overlay)
277
+ - `"hma"`: Hull Moving Average (overlay)
278
+ - `"stddev"`: Standard Deviation (separate pane)
279
+ - `"atr"`: Average True Range (separate pane)
280
+
281
+ Example:
282
+
283
+ ```ts
284
+ const emaId = chart.addIndicator("ema", { length: 34, source: "close" });
285
+ const rsiId = chart.addIndicator("rsi", { length: 14 }, { pane: "separate", paneHeightRatio: 0.18 });
286
+
287
+ // resize pane later (useful for drag-handle UX in frontend)
288
+ chart.updateIndicator(rsiId, { paneHeightRatio: 0.12 });
289
+
290
+ // remove when needed
291
+ chart.removeIndicator(emaId);
292
+ ```
293
+
294
+ Volume/VWMA note:
295
+
296
+ - if your data has no `v`, `volume`/`vwma` will have limited or no output.
262
297
 
263
298
  ---
264
299
 
package/docs/RECIPES.md CHANGED
@@ -91,6 +91,67 @@ const chart = createChart(root, {
91
91
  });
92
92
  ```
93
93
 
94
+ ## Add built-in indicators
95
+
96
+ ```ts
97
+ const emaId = chart.addIndicator("ema", { length: 34, source: "close" });
98
+ const rsiId = chart.addIndicator("rsi", { length: 14 }, { pane: "separate", paneHeightRatio: 0.18 });
99
+ const volumeId = chart.addIndicator("volume", { upOpacity: 0.72, downOpacity: 0.72 });
100
+ ```
101
+
102
+ Available built-ins:
103
+
104
+ - `volume`, `sma`, `ema`, `rsi`, `wma`, `vwma`, `rma`, `hma`, `stddev`, `atr`
105
+
106
+ ## Resize indicator pane from frontend
107
+
108
+ Use `paneHeightRatio` so your app can wire a drag handle or slider:
109
+
110
+ ```ts
111
+ const volumeId = chart.addIndicator("volume", {}, { paneHeightRatio: 0.2 });
112
+
113
+ // e.g. on slider/drag updates:
114
+ chart.updateIndicator(volumeId, { paneHeightRatio: 0.1 });
115
+ ```
116
+
117
+ Recommended range:
118
+
119
+ - `0.08` to `0.45` (library clamps to safe bounds)
120
+
121
+ ## Remove or hide indicators
122
+
123
+ ```ts
124
+ const id = chart.addIndicator("ema", { length: 21 });
125
+ chart.updateIndicator(id, { visible: false }); // hide
126
+ chart.removeIndicator(id); // remove
127
+ ```
128
+
129
+ ## Volume and VWMA data requirement
130
+
131
+ `volume` and `vwma` use `OhlcDataPoint.v`. If `v` is missing, output can be empty/limited.
132
+
133
+ ```ts
134
+ const data = [
135
+ { t: "2026-01-01T00:00:00Z", o: 100, h: 103, l: 99, c: 102, v: 125000 }
136
+ ];
137
+ chart.setData(data);
138
+ ```
139
+
140
+ ## Register a custom indicator plugin
141
+
142
+ ```ts
143
+ chart.registerIndicator({
144
+ id: "my-overlay-line",
145
+ name: "My Overlay Line",
146
+ pane: "overlay",
147
+ defaultInputs: { color: "#22d3ee", width: 2 },
148
+ draw: (ctx, rc, inputs) => {
149
+ if (!rc.yFromPrice) return;
150
+ // draw custom line with rc.xFromIndex + rc.yFromPrice
151
+ }
152
+ });
153
+ ```
154
+
94
155
  ## Add a draggable pending limit line
95
156
 
96
157
  ```ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hyperprop-charting-library",
3
- "version": "0.1.21",
3
+ "version": "0.1.22",
4
4
  "description": "Lightweight TypeScript charting core",
5
5
  "type": "module",
6
6
  "main": "./dist/hyperprop-charting-library.cjs",