@rioaxyz/sonrchart 0.1.0 → 0.3.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
@@ -41,6 +41,12 @@ bars come from.
41
41
  - **Snapshots.** Capture the chart to a PNG data URL (or trigger a download).
42
42
  - **Composable chrome.** `ChartToolbar` bundles intervals + indicators +
43
43
  drawing tools + snapshot; add your own buttons via its children slot.
44
+ - **Volume trend readout.** Optional header row classifying the last N bars'
45
+ volume as Increasing / Decreasing / Mixed (lookback editable live in the
46
+ settings dialog, tracks the crosshair). Toggle it from the indicators menu
47
+ ("VOLTREND"), seed it on with `volumeTrend` on `ChartHeader` /
48
+ `SonrChartShell`, drive it via `setVolumeTrendEnabled` on the chart handle,
49
+ or subscribe to the raw trend via `subscribeVolumeTrend`.
44
50
  - **Fast & dependency-light.** The only runtime dependency is KLineChart;
45
51
  React is a peer. The per-frame hot paths are tuned (see
46
52
  [PERFORMANCE.md](PERFORMANCE.md)).
@@ -151,6 +157,7 @@ function TradingChart({ source }: { source: DataSource }) {
151
157
  | `paneHeights` | `Record<string, number>` | Sub-pane pixel heights, keyed by indicator name. |
152
158
  | `sessions` | `boolean \| { premarket?, afterhours? }` | Tint pre/after-hours session backgrounds (ET). |
153
159
  | `patterns` | `PatternMarker[]` | Setup markers (`{ time, kind, label }`) drawn on the price pane. |
160
+ | `halted` | `boolean \| HaltBadge` | Trading-halt badge: a pill pinned to the top of the price pane against the y-axis. `true` shows the default blue "Halt"; pass `{ label?, color?, textColor? }` to customize. |
154
161
  | `priceLines` | `PriceLine[]` | Static horizontal reference lines (entry / stop / target). |
155
162
  | `orderLines` | `OrderLine[]` | Draggable order/position lines; `onMove(price, phase)` fires as the user drags. |
156
163
  | `priceScale` | `{ type?, invert?, inside? }` | Scale mode — `"normal" \| "percentage" \| "logarithm"`, plus invert / labels-inside. |
@@ -214,10 +221,13 @@ const ref = useRef<SonrChartHandle>(null);
214
221
 
215
222
  ### Sonr custom indicators
216
223
 
217
- `sessions` and `patterns` are Sonr's own custom KLineChart indicators
218
- (registered automatically). Session tinting shades pre-market (04:00–09:30 ET)
219
- and after-hours (16:00–20:00 ET); pattern markers draw a labeled triangle at
220
- each setup's bar (green up for longs, red down for shorts).
224
+ `sessions`, `patterns`, and `halted` are Sonr's own custom KLineChart
225
+ indicators (registered automatically). Session tinting shades pre-market
226
+ (04:00–09:30 ET) and after-hours (16:00–20:00 ET); pattern markers draw a
227
+ labeled triangle at each setup's bar (green up for longs, red down for
228
+ shorts); the halt badge pins a rounded pill to the top-right of the price
229
+ pane while a symbol is trading-halted (drawn on the canvas, so it's included
230
+ in snapshots).
221
231
 
222
232
  ## Develop
223
233
 
package/dist/index.cjs CHANGED
@@ -28,6 +28,7 @@ __export(index_exports, {
28
28
  ConfirmDialog: () => ConfirmDialog,
29
29
  DEFAULT_CANDLE_STYLES: () => DEFAULT_CANDLE_STYLES,
30
30
  DEFAULT_DRAWING_TOOLS: () => DEFAULT_DRAWING_TOOLS,
31
+ DEFAULT_HALT_BADGE: () => DEFAULT_HALT_BADGE,
31
32
  DEFAULT_INDICATOR_OPTIONS: () => DEFAULT_INDICATOR_OPTIONS,
32
33
  DEFAULT_LABELS: () => DEFAULT_LABELS,
33
34
  DEFAULT_RANGES: () => DEFAULT_RANGES,
@@ -35,6 +36,7 @@ __export(index_exports, {
35
36
  DEFAULT_SESSION_COLORS: () => DEFAULT_SESSION_COLORS,
36
37
  DEFAULT_TIMEZONE: () => DEFAULT_TIMEZONE,
37
38
  DEFAULT_TIMEZONES: () => DEFAULT_TIMEZONES,
39
+ DEFAULT_VOLUME_TREND_BARS: () => DEFAULT_VOLUME_TREND_BARS,
38
40
  DataWindow: () => DataWindow,
39
41
  DrawingTemplateStore: () => DrawingTemplateStore,
40
42
  DrawingToolbar: () => DrawingToolbar,
@@ -42,6 +44,7 @@ __export(index_exports, {
42
44
  EXTENDED_OVERLAYS: () => EXTENDED_OVERLAYS,
43
45
  FIB_EXTENSION_LEVELS: () => FIB_EXTENSION_LEVELS,
44
46
  GoToDateDialog: () => GoToDateDialog,
47
+ HALT_INDICATOR: () => HALT_INDICATOR,
45
48
  IndicatorMenu: () => IndicatorMenu,
46
49
  MarketStatus: () => MarketStatus,
47
50
  NavControlBar: () => NavControlBar,
@@ -70,6 +73,7 @@ __export(index_exports, {
70
73
  ToolbarButton: () => ToolbarButton,
71
74
  UNIT_MS: () => UNIT_MS,
72
75
  USER_DRAWING_GROUP: () => USER_DRAWING_GROUP,
76
+ VOLUME_TREND_OPTION: () => VOLUME_TREND_OPTION,
73
77
  VWAP_INDICATOR: () => VWAP_INDICATOR,
74
78
  WATERMARK_INDICATOR: () => WATERMARK_INDICATOR,
75
79
  arrowHeadPoints: () => arrowHeadPoints,
@@ -87,6 +91,7 @@ __export(index_exports, {
87
91
  isEditableTarget: () => isEditableTarget,
88
92
  marketStatus: () => marketStatus,
89
93
  mergeLabels: () => mergeLabels,
94
+ normalizeHalt: () => normalizeHalt,
90
95
  parseLocalDateTime: () => parseLocalDateTime,
91
96
  parseResolution: () => parseResolution,
92
97
  priceRangeLabel: () => priceRangeLabel,
@@ -105,7 +110,8 @@ __export(index_exports, {
105
110
  themeStyles: () => themeStyles,
106
111
  timezoneLabel: () => timezoneLabel,
107
112
  toHeikinAshi: () => toHeikinAshi,
108
- tradeBoxFigures: () => tradeBoxFigures
113
+ tradeBoxFigures: () => tradeBoxFigures,
114
+ volumeTrend: () => volumeTrend
109
115
  });
110
116
  module.exports = __toCommonJS(index_exports);
111
117
 
@@ -13776,14 +13782,26 @@ var SESSION_INDICATOR = "SONR_SESSION";
13776
13782
  var PATTERN_INDICATOR = "SONR_PATTERNS";
13777
13783
  var WATERMARK_INDICATOR = "SONR_WATERMARK";
13778
13784
  var SERIES_INDICATOR = "SONR_SERIES";
13785
+ var HALT_INDICATOR = "SONR_HALT";
13779
13786
  var VWAP_INDICATOR = "VWAP";
13780
13787
  var Z_ORDER = {
13781
13788
  watermark: -3,
13782
13789
  session: -2,
13783
13790
  series: -1,
13784
13791
  compare: 0,
13785
- pattern: 10
13792
+ pattern: 10,
13793
+ halt: 11
13786
13794
  };
13795
+ var DEFAULT_HALT_BADGE = { label: "Halt", color: "#2196f3", textColor: "#ffffff" };
13796
+ function normalizeHalt(prop) {
13797
+ if (!prop) return null;
13798
+ if (prop === true) return DEFAULT_HALT_BADGE;
13799
+ return {
13800
+ label: prop.label ?? DEFAULT_HALT_BADGE.label,
13801
+ color: prop.color ?? DEFAULT_HALT_BADGE.color,
13802
+ textColor: prop.textColor ?? DEFAULT_HALT_BADGE.textColor
13803
+ };
13804
+ }
13787
13805
  var VWAP_COLOR = "#a970ff";
13788
13806
  function computeVWAP(dataList) {
13789
13807
  let day = Number.NaN;
@@ -14035,6 +14053,41 @@ function registerSonrIndicators() {
14035
14053
  return false;
14036
14054
  }
14037
14055
  });
14056
+ registerIndicator({
14057
+ name: HALT_INDICATOR,
14058
+ shortName: "",
14059
+ series: "price",
14060
+ figures: [],
14061
+ calc: (dataList) => dataList.map(() => ({})),
14062
+ draw: ({ ctx, bounding, indicator }) => {
14063
+ const cfg = indicator.extendData;
14064
+ if (!cfg || !cfg.label) return false;
14065
+ const padX = 8;
14066
+ const h = 20;
14067
+ const inset = 8;
14068
+ ctx.save();
14069
+ ctx.font = '600 11px ui-sans-serif, -apple-system, "Segoe UI", sans-serif';
14070
+ const w = Math.ceil(ctx.measureText(cfg.label).width) + padX * 2;
14071
+ const x = bounding.width - w - inset;
14072
+ const y = (bounding.top ?? 0) + inset;
14073
+ const r = h / 2;
14074
+ ctx.beginPath();
14075
+ ctx.moveTo(x + r, y);
14076
+ ctx.arcTo(x + w, y, x + w, y + h, r);
14077
+ ctx.arcTo(x + w, y + h, x, y + h, r);
14078
+ ctx.arcTo(x, y + h, x, y, r);
14079
+ ctx.arcTo(x, y, x + w, y, r);
14080
+ ctx.closePath();
14081
+ ctx.fillStyle = cfg.color;
14082
+ ctx.fill();
14083
+ ctx.fillStyle = cfg.textColor;
14084
+ ctx.textAlign = "center";
14085
+ ctx.textBaseline = "middle";
14086
+ ctx.fillText(cfg.label, x + w / 2, y + h / 2 + 0.5);
14087
+ ctx.restore();
14088
+ return false;
14089
+ }
14090
+ });
14038
14091
  registerIndicator({
14039
14092
  name: PATTERN_INDICATOR,
14040
14093
  shortName: "",
@@ -15387,6 +15440,25 @@ function formatWithUnit(text2, quote) {
15387
15440
  return out;
15388
15441
  }
15389
15442
 
15443
+ // src/volumeTrend.ts
15444
+ var DEFAULT_VOLUME_TREND_BARS = 3;
15445
+ function volumeTrend(list, endIndex, bars = DEFAULT_VOLUME_TREND_BARS) {
15446
+ const n = Math.max(1, Math.floor(bars));
15447
+ if (endIndex < n || endIndex >= list.length) return null;
15448
+ let up = 0;
15449
+ let down = 0;
15450
+ for (let i = endIndex - n + 1; i <= endIndex; i++) {
15451
+ const v = list[i]?.volume;
15452
+ const pv = list[i - 1]?.volume;
15453
+ if (typeof v !== "number" || typeof pv !== "number") return null;
15454
+ if (v > pv) up++;
15455
+ else if (v < pv) down++;
15456
+ }
15457
+ if (up === n) return "increasing";
15458
+ if (down === n) return "decreasing";
15459
+ return "mixed";
15460
+ }
15461
+
15390
15462
  // src/SonrChart.tsx
15391
15463
  var import_jsx_runtime = require("react/jsx-runtime");
15392
15464
  registerSonrIndicators();
@@ -15396,7 +15468,7 @@ registerShapeOverlays();
15396
15468
  registerExtendedDrawings();
15397
15469
  registerTradeReview();
15398
15470
  registerTextOverlay();
15399
- var INTERNAL_INDICATORS = /* @__PURE__ */ new Set([SESSION_INDICATOR, PATTERN_INDICATOR, COMPARE_INDICATOR, WATERMARK_INDICATOR, SERIES_INDICATOR]);
15471
+ var INTERNAL_INDICATORS = /* @__PURE__ */ new Set([SESSION_INDICATOR, PATTERN_INDICATOR, COMPARE_INDICATOR, WATERMARK_INDICATOR, SERIES_INDICATOR, HALT_INDICATOR]);
15400
15472
  var isUserIndicator = (name) => !INTERNAL_INDICATORS.has(name);
15401
15473
  function magnetMode(m) {
15402
15474
  if (m === true || m === "weak") return "weak_magnet";
@@ -15496,6 +15568,7 @@ var SonrChart = (0, import_react2.forwardRef)(function SonrChart2({
15496
15568
  sessions: sessionsProp,
15497
15569
  watermark: watermarkProp,
15498
15570
  patterns,
15571
+ halted,
15499
15572
  priceLines,
15500
15573
  orderLines,
15501
15574
  priceScale: priceScaleProp,
@@ -15613,14 +15686,24 @@ var SonrChart = (0, import_react2.forwardRef)(function SonrChart2({
15613
15686
  const legendRef = (0, import_react2.useRef)(legend);
15614
15687
  legendRef.current = legend;
15615
15688
  const headerSubs = (0, import_react2.useRef)(/* @__PURE__ */ new Set());
15689
+ const trendSubs = (0, import_react2.useRef)(/* @__PURE__ */ new Set());
15690
+ const trendBarsRef = (0, import_react2.useRef)(DEFAULT_VOLUME_TREND_BARS);
15691
+ const trendBarsSubs = (0, import_react2.useRef)(/* @__PURE__ */ new Set());
15692
+ const trendOnRef = (0, import_react2.useRef)(false);
15693
+ const trendOnSubs = (0, import_react2.useRef)(/* @__PURE__ */ new Set());
15616
15694
  const crosshairActiveRef = (0, import_react2.useRef)(false);
15695
+ const headerIdxRef = (0, import_react2.useRef)(-1);
15696
+ const emitHeaderAt = (list, idx) => {
15697
+ headerIdxRef.current = idx;
15698
+ const current = idx >= 0 ? list[idx] ?? null : null;
15699
+ const prev = idx > 0 ? list[idx - 1] : null;
15700
+ headerSubs.current.forEach((cb) => cb(current, prev));
15701
+ trendSubs.current.forEach((s) => s.cb(volumeTrend(list, idx, s.bars ?? trendBarsRef.current)));
15702
+ };
15617
15703
  const emitLatestHeader = () => {
15618
15704
  if (crosshairActiveRef.current) return;
15619
15705
  const list = chartRef.current?.getDataList() ?? [];
15620
- const n = list.length;
15621
- const current = n ? list[n - 1] : null;
15622
- const prev = n > 1 ? list[n - 2] : null;
15623
- headerSubs.current.forEach((cb) => cb(current, prev));
15706
+ emitHeaderAt(list, list.length - 1);
15624
15707
  };
15625
15708
  const haRef = (0, import_react2.useRef)(new HaTransformer());
15626
15709
  const [textEdit, setTextEdit] = (0, import_react2.useState)(null);
@@ -15888,6 +15971,40 @@ var SonrChart = (0, import_react2.forwardRef)(function SonrChart2({
15888
15971
  const n = list.length;
15889
15972
  cb(n ? list[n - 1] : null, n > 1 ? list[n - 2] : null);
15890
15973
  return () => headerSubs.current.delete(cb);
15974
+ },
15975
+ // Volume trend (#79) ---------------------------------------------------
15976
+ subscribeVolumeTrend: (cb, bars) => {
15977
+ const sub = { bars: bars === void 0 ? void 0 : Math.max(1, Math.floor(bars)), cb };
15978
+ trendSubs.current.add(sub);
15979
+ const list = chartRef.current?.getDataList() ?? [];
15980
+ cb(volumeTrend(list, list.length - 1, sub.bars ?? trendBarsRef.current));
15981
+ return () => trendSubs.current.delete(sub);
15982
+ },
15983
+ getVolumeTrendBars: () => trendBarsRef.current,
15984
+ setVolumeTrendBars: (bars) => {
15985
+ const n = Math.max(1, Math.floor(bars));
15986
+ if (!Number.isFinite(n) || n === trendBarsRef.current) return;
15987
+ trendBarsRef.current = n;
15988
+ const list = chartRef.current?.getDataList() ?? [];
15989
+ const idx = crosshairActiveRef.current ? headerIdxRef.current : list.length - 1;
15990
+ trendSubs.current.forEach((s) => {
15991
+ if (s.bars === void 0) s.cb(volumeTrend(list, idx, n));
15992
+ });
15993
+ trendBarsSubs.current.forEach((cb) => cb(n));
15994
+ },
15995
+ subscribeVolumeTrendBars: (cb) => {
15996
+ trendBarsSubs.current.add(cb);
15997
+ return () => trendBarsSubs.current.delete(cb);
15998
+ },
15999
+ getVolumeTrendEnabled: () => trendOnRef.current,
16000
+ setVolumeTrendEnabled: (on) => {
16001
+ if (on === trendOnRef.current) return;
16002
+ trendOnRef.current = on;
16003
+ trendOnSubs.current.forEach((cb) => cb(on));
16004
+ },
16005
+ subscribeVolumeTrendEnabled: (cb) => {
16006
+ trendOnSubs.current.add(cb);
16007
+ return () => trendOnSubs.current.delete(cb);
15891
16008
  }
15892
16009
  },
15893
16010
  // paneHeights/priceScale captured by closure; recreated per render is fine.
@@ -15951,16 +16068,29 @@ var SonrChart = (0, import_react2.forwardRef)(function SonrChart2({
15951
16068
  chart.setStyles(mergeStyles(themeStyleObj, styles2));
15952
16069
  chart.subscribeAction("onCrosshairChange", (d) => {
15953
16070
  const cross = d;
15954
- const k = cross?.kLineData;
15955
- if (k && typeof cross?.dataIndex === "number") {
16071
+ const list = chart.getDataList();
16072
+ let k = cross?.kLineData ?? null;
16073
+ let idx = typeof cross?.dataIndex === "number" ? cross.dataIndex : -1;
16074
+ if (!k && typeof cross?.x === "number" && list.length > 0) {
16075
+ const [p] = chart.convertFromPixel([{ x: cross.x }], { paneId: CANDLE_PANE });
16076
+ if (typeof p?.dataIndex === "number") {
16077
+ idx = Math.max(0, Math.min(p.dataIndex, list.length - 1));
16078
+ k = list[idx] ?? null;
16079
+ }
16080
+ }
16081
+ if (k && idx >= 0) {
15956
16082
  crosshairActiveRef.current = true;
15957
- const list = chart.getDataList();
15958
- headerSubs.current.forEach((cb) => cb(k, cross.dataIndex > 0 ? list[cross.dataIndex - 1] : null));
16083
+ emitHeaderAt(list, idx);
15959
16084
  } else {
15960
16085
  crosshairActiveRef.current = false;
15961
16086
  emitLatestHeader();
15962
16087
  }
15963
16088
  });
16089
+ const onPointerLeave = () => {
16090
+ crosshairActiveRef.current = false;
16091
+ emitLatestHeader();
16092
+ };
16093
+ el.addEventListener("mouseleave", onPointerLeave);
15964
16094
  applyCandleStyle(chart, candleStyleRef.current, themeStyleObj, baseTheme);
15965
16095
  applyPriceLabels(chart, priceLabelsRef.current);
15966
16096
  applyFormatter(chart);
@@ -16070,6 +16200,7 @@ var SonrChart = (0, import_react2.forwardRef)(function SonrChart2({
16070
16200
  return () => {
16071
16201
  disposed = true;
16072
16202
  unsub();
16203
+ el.removeEventListener("mouseleave", onPointerLeave);
16073
16204
  dispose(idRef.current);
16074
16205
  chartRef.current = null;
16075
16206
  overlayIds.current = [];
@@ -16141,6 +16272,19 @@ var SonrChart = (0, import_react2.forwardRef)(function SonrChart2({
16141
16272
  chart.removeIndicator({ name: PATTERN_INDICATOR });
16142
16273
  }
16143
16274
  }, [patterns, chartEpoch]);
16275
+ (0, import_react2.useEffect)(() => {
16276
+ const chart = chartRef.current;
16277
+ if (!chart) return;
16278
+ const present = chart.getIndicators({ name: HALT_INDICATOR }).length > 0;
16279
+ const badge = normalizeHalt(halted);
16280
+ if (badge && !present) {
16281
+ chart.createIndicator({ name: HALT_INDICATOR, paneId: CANDLE_PANE, zLevel: Z_ORDER.halt, extendData: badge }, true);
16282
+ } else if (badge && present) {
16283
+ chart.overrideIndicator({ name: HALT_INDICATOR, extendData: badge });
16284
+ } else if (!badge && present) {
16285
+ chart.removeIndicator({ name: HALT_INDICATOR });
16286
+ }
16287
+ }, [halted, chartEpoch]);
16144
16288
  (0, import_react2.useEffect)(() => {
16145
16289
  const chart = chartRef.current;
16146
16290
  if (!chart) return;
@@ -16394,7 +16538,7 @@ var CSS = `
16394
16538
  :where(.sonr-mkt-handle){position:absolute;top:50%;width:12px;height:12px;border-radius:50%;background:#fff;border:2px solid #22a06b;transform:translate(-50%,-50%);}
16395
16539
  :where(.sonr-mkt-ticks){grid-column:2;display:flex;justify-content:space-between;font-size:10px;color:#888;}
16396
16540
  :where(.sonr-mkt-tz){margin-top:12px;font-size:11px;color:#888;text-align:center;}
16397
- :where(.sonr-hd){display:inline-flex;align-items:center;gap:10px;font-family:inherit;font-size:12px;color:#000;white-space:nowrap;}
16541
+ :where(.sonr-hd){display:inline-flex;flex-wrap:wrap;align-items:center;gap:10px;font-family:inherit;font-size:12px;color:#000;white-space:nowrap;}
16398
16542
  :where(.sonr-hd-sym){font-weight:700;color:#000;}
16399
16543
  :where(.sonr-hd-ohlc){display:inline-flex;align-items:center;gap:8px;font-variant-numeric:tabular-nums;}
16400
16544
  :where(.sonr-hd-cell){display:inline-flex;align-items:baseline;gap:3px;}
@@ -16402,6 +16546,8 @@ var CSS = `
16402
16546
  :where(.sonr-hd-v){color:#555;}
16403
16547
  :where(.sonr-hd-up){color:#22a06b;}
16404
16548
  :where(.sonr-hd-down){color:#d23c3c;}
16549
+ :where(.sonr-hd-trend){flex-basis:100%;display:inline-flex;align-items:baseline;gap:4px;margin-top:2px;}
16550
+ :where(.sonr-hd-mixed){color:#c98a1b;}
16405
16551
  :where(.sonr-brand){position:absolute;z-index:3;pointer-events:none;font-family:inherit;font-weight:700;font-size:13px;letter-spacing:0.02em;color:#000;user-select:none;line-height:1;}
16406
16552
  :where(.sonr-brand-img){display:block;}
16407
16553
  :where(.sonr-brand-link){pointer-events:auto;color:inherit;text-decoration:none;}
@@ -16577,6 +16723,10 @@ var DEFAULT_LABELS = {
16577
16723
  zoomIn: "Zoom in",
16578
16724
  fitContent: "Fit all data",
16579
16725
  goToRealtime: "Go to realtime",
16726
+ volumeTrend: "Vol trend",
16727
+ volumeTrendIncreasing: "Increasing",
16728
+ volumeTrendDecreasing: "Decreasing",
16729
+ volumeTrendMixed: "Mixed",
16580
16730
  marketPre: "Pre-market",
16581
16731
  marketOpen: "Open",
16582
16732
  marketAfter: "After-hours",
@@ -16607,6 +16757,7 @@ function mergeLabels(over) {
16607
16757
 
16608
16758
  // src/IndicatorMenu.tsx
16609
16759
  var import_jsx_runtime2 = require("react/jsx-runtime");
16760
+ var VOLUME_TREND_OPTION = "VOLTREND";
16610
16761
  var DEFAULT_INDICATOR_OPTIONS = [
16611
16762
  // Price-pane overlays.
16612
16763
  { name: "MA", label: "Moving Average", pane: "main" },
@@ -16619,6 +16770,7 @@ var DEFAULT_INDICATOR_OPTIONS = [
16619
16770
  { name: "VWAP", label: "Volume Weighted Avg Price", pane: "main" },
16620
16771
  // Sub-pane oscillators & volume studies.
16621
16772
  { name: "VOL", label: "Volume", pane: "new" },
16773
+ { name: VOLUME_TREND_OPTION, label: "Volume Trend (header row)" },
16622
16774
  { name: "MACD", label: "MACD", pane: "new" },
16623
16775
  { name: "RSI", label: "Relative Strength Index", pane: "new" },
16624
16776
  { name: "KDJ", label: "KDJ Stochastic", pane: "new" },
@@ -16649,12 +16801,27 @@ function IndicatorMenu({
16649
16801
  const [active, setActive] = (0, import_react4.useState)([]);
16650
16802
  const { open, toggle: toggleOpen, pos, triggerRef, panelRef } = useDropdown();
16651
16803
  (0, import_react4.useEffect)(() => {
16652
- const t = setTimeout(() => setActive(chartRef.current?.listIndicators() ?? []), 0);
16653
- return () => clearTimeout(t);
16804
+ let unsubOn;
16805
+ const t = setTimeout(() => {
16806
+ const h = chartRef.current;
16807
+ const base = h?.listIndicators() ?? [];
16808
+ setActive(h?.getVolumeTrendEnabled?.() ? [...base, VOLUME_TREND_OPTION] : base);
16809
+ unsubOn = h?.subscribeVolumeTrendEnabled?.(
16810
+ (on) => setActive((a) => on ? a.includes(VOLUME_TREND_OPTION) ? a : [...a, VOLUME_TREND_OPTION] : a.filter((n) => n !== VOLUME_TREND_OPTION))
16811
+ );
16812
+ }, 0);
16813
+ return () => {
16814
+ clearTimeout(t);
16815
+ unsubOn?.();
16816
+ };
16654
16817
  }, [chartRef]);
16655
16818
  const toggle = (opt) => {
16656
16819
  const handle = chartRef.current;
16657
16820
  if (!handle) return;
16821
+ if (opt.name === VOLUME_TREND_OPTION) {
16822
+ handle.setVolumeTrendEnabled?.(!active.includes(opt.name));
16823
+ return;
16824
+ }
16658
16825
  if (active.includes(opt.name)) {
16659
16826
  handle.removeIndicator(opt.name);
16660
16827
  setActive((a) => a.filter((n) => n !== opt.name));
@@ -18095,6 +18262,7 @@ function SettingsModal({
18095
18262
  const [indicators2, setIndicators] = (0, import_react14.useState)([]);
18096
18263
  const [indColors, setIndColors] = (0, import_react14.useState)({});
18097
18264
  const [indParams, setIndParams] = (0, import_react14.useState)({});
18265
+ const [trendBars, setTrendBars] = (0, import_react14.useState)(DEFAULT_VOLUME_TREND_BARS);
18098
18266
  const [watermark, setWatermark] = (0, import_react14.useState)({ symbol: true, interval: true, color: "#e6ebe9", opacity: 0.06 });
18099
18267
  const [brushStyles, setBrushStyles] = (0, import_react14.useState)({
18100
18268
  brush: { color: "#e6ebe9", size: 2 },
@@ -18129,6 +18297,8 @@ function SettingsModal({
18129
18297
  }
18130
18298
  }
18131
18299
  setIndParams(params);
18300
+ setTrendBars(chartRef.current?.getVolumeTrendBars?.() ?? DEFAULT_VOLUME_TREND_BARS);
18301
+ const unsubTrendBars = chartRef.current?.subscribeVolumeTrendBars?.(setTrendBars);
18132
18302
  const readWm = () => {
18133
18303
  const w = chartRef.current?.getWatermark?.();
18134
18304
  if (w) setWatermark(w);
@@ -18171,6 +18341,7 @@ function SettingsModal({
18171
18341
  document.addEventListener("keydown", onKey);
18172
18342
  return () => {
18173
18343
  document.removeEventListener("keydown", onKey);
18344
+ unsubTrendBars?.();
18174
18345
  unsubWm?.();
18175
18346
  unsubBrush?.();
18176
18347
  unsubText?.();
@@ -18279,6 +18450,25 @@ function SettingsModal({
18279
18450
  ) })
18280
18451
  ] }),
18281
18452
  section === "indicators" && /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(import_jsx_runtime15.Fragment, { children: [
18453
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className: "sonr-set-subhead", children: L.volumeTrend }),
18454
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Row, { label: "Bars (lookback)", children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
18455
+ "input",
18456
+ {
18457
+ type: "number",
18458
+ className: "sonr-set-num",
18459
+ min: "1",
18460
+ max: "100",
18461
+ value: trendBars,
18462
+ onChange: (e) => {
18463
+ const v = e.target.valueAsNumber;
18464
+ if (Number.isNaN(v)) return;
18465
+ const n = Math.max(1, Math.min(100, Math.floor(v)));
18466
+ setTrendBars(n);
18467
+ chartRef.current?.setVolumeTrendBars?.(n);
18468
+ }
18469
+ }
18470
+ ) }),
18471
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className: "sonr-set-subhead", children: "Active indicators" }),
18282
18472
  /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("p", { className: "sonr-set-note", children: "An indicator must be active on the chart to change its settings \u2014 add it from the Indicators menu first." }),
18283
18473
  indicators2.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("p", { className: "sonr-set-empty", children: "No indicators active." }),
18284
18474
  indicators2.map((name) => {
@@ -18908,18 +19098,28 @@ var fmtVolume = (v) => {
18908
19098
  if (n >= 1e3) return `${(n / 1e3).toFixed(2)}K`;
18909
19099
  return `${n}`;
18910
19100
  };
18911
- function ChartHeader({ chartRef, symbol, interval, source, pricePrecision = 2, labels, className }) {
19101
+ function ChartHeader({ chartRef, symbol, interval, source, pricePrecision = 2, volumeTrend: volumeTrend2, labels, className }) {
18912
19102
  (0, import_react17.useEffect)(ensureDefaultStyles, []);
18913
19103
  const L = (0, import_react17.useMemo)(() => mergeLabels(labels), [labels]);
18914
19104
  const [bar, setBar] = (0, import_react17.useState)({ current: null, prev: null });
19105
+ const [trend, setTrend] = (0, import_react17.useState)(null);
19106
+ const [trendOn, setTrendOn] = (0, import_react17.useState)(false);
19107
+ const seedOn = volumeTrend2 === true || typeof volumeTrend2 === "number" && volumeTrend2 > 0;
18915
19108
  (0, import_react17.useEffect)(() => {
18916
19109
  let cancelled = false;
18917
19110
  let unsub;
19111
+ let unsubOn;
18918
19112
  const attach = () => {
18919
19113
  if (cancelled) return;
18920
19114
  const h = chartRef.current;
18921
19115
  if (h?.subscribeHeaderBar) {
18922
19116
  unsub = h.subscribeHeaderBar((current, prev) => setBar({ current, prev }));
19117
+ if (seedOn) {
19118
+ if (typeof volumeTrend2 === "number") h.setVolumeTrendBars?.(volumeTrend2);
19119
+ h.setVolumeTrendEnabled?.(true);
19120
+ }
19121
+ setTrendOn(h.getVolumeTrendEnabled?.() ?? seedOn);
19122
+ unsubOn = h.subscribeVolumeTrendEnabled?.(setTrendOn);
18923
19123
  } else {
18924
19124
  setTimeout(attach, 50);
18925
19125
  }
@@ -18928,8 +19128,17 @@ function ChartHeader({ chartRef, symbol, interval, source, pricePrecision = 2, l
18928
19128
  return () => {
18929
19129
  cancelled = true;
18930
19130
  unsub?.();
19131
+ unsubOn?.();
18931
19132
  };
18932
- }, [chartRef]);
19133
+ }, [chartRef, volumeTrend2, seedOn]);
19134
+ (0, import_react17.useEffect)(() => {
19135
+ if (!trendOn) {
19136
+ setTrend(null);
19137
+ return;
19138
+ }
19139
+ const unsubTrend = chartRef.current?.subscribeVolumeTrend?.((t) => setTrend(t));
19140
+ return () => unsubTrend?.();
19141
+ }, [chartRef, trendOn]);
18933
19142
  const k = bar.current;
18934
19143
  const up = (k?.close ?? 0) >= (k?.open ?? 0);
18935
19144
  const dirClass = up ? "sonr-hd-up" : "sonr-hd-down";
@@ -18970,6 +19179,10 @@ function ChartHeader({ chartRef, symbol, interval, source, pricePrecision = 2, l
18970
19179
  /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: "sonr-hd-k", children: "V" }),
18971
19180
  /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: "sonr-hd-v", children: fmtVolume(k.volume) })
18972
19181
  ] })
19182
+ ] }),
19183
+ trendOn && trend && /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("span", { className: "sonr-hd-trend", children: [
19184
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: "sonr-hd-k", children: L.volumeTrend }),
19185
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: trend === "increasing" ? "sonr-hd-up" : trend === "decreasing" ? "sonr-hd-down" : "sonr-hd-mixed", children: trend === "increasing" ? L.volumeTrendIncreasing : trend === "decreasing" ? L.volumeTrendDecreasing : L.volumeTrendMixed })
18973
19186
  ] })
18974
19187
  ] });
18975
19188
  }
@@ -19432,6 +19645,7 @@ var SonrChartShell = (0, import_react24.forwardRef)(function SonrChartShell2({
19432
19645
  ranges,
19433
19646
  source,
19434
19647
  exchange = source,
19648
+ volumeTrend: volumeTrend2 = true,
19435
19649
  onIntervalChange,
19436
19650
  onSymbolChange,
19437
19651
  brand,
@@ -19505,7 +19719,7 @@ var SonrChartShell = (0, import_react24.forwardRef)(function SonrChartShell2({
19505
19719
  style: { position: "absolute", inset: 0 }
19506
19720
  }
19507
19721
  ),
19508
- /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("div", { className: "sonr-tile-header", children: /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(ChartHeader, { chartRef, symbol, interval, source }) }),
19722
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("div", { className: "sonr-tile-header", children: /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(ChartHeader, { chartRef, symbol, interval, source, volumeTrend: volumeTrend2, labels }) }),
19509
19723
  /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("div", { className: "sonr-tile-nav", children: /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(NavControlBar, { chartRef, labels }) }),
19510
19724
  brand != null && /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(BrandMark, { position: brandCorner, opacity: brandOpacity, children: brand })
19511
19725
  ] }),
@@ -19568,6 +19782,7 @@ function setCSSCustomProperty(name, value, el) {
19568
19782
  ConfirmDialog,
19569
19783
  DEFAULT_CANDLE_STYLES,
19570
19784
  DEFAULT_DRAWING_TOOLS,
19785
+ DEFAULT_HALT_BADGE,
19571
19786
  DEFAULT_INDICATOR_OPTIONS,
19572
19787
  DEFAULT_LABELS,
19573
19788
  DEFAULT_RANGES,
@@ -19575,6 +19790,7 @@ function setCSSCustomProperty(name, value, el) {
19575
19790
  DEFAULT_SESSION_COLORS,
19576
19791
  DEFAULT_TIMEZONE,
19577
19792
  DEFAULT_TIMEZONES,
19793
+ DEFAULT_VOLUME_TREND_BARS,
19578
19794
  DataWindow,
19579
19795
  DrawingTemplateStore,
19580
19796
  DrawingToolbar,
@@ -19582,6 +19798,7 @@ function setCSSCustomProperty(name, value, el) {
19582
19798
  EXTENDED_OVERLAYS,
19583
19799
  FIB_EXTENSION_LEVELS,
19584
19800
  GoToDateDialog,
19801
+ HALT_INDICATOR,
19585
19802
  IndicatorMenu,
19586
19803
  MarketStatus,
19587
19804
  NavControlBar,
@@ -19610,6 +19827,7 @@ function setCSSCustomProperty(name, value, el) {
19610
19827
  ToolbarButton,
19611
19828
  UNIT_MS,
19612
19829
  USER_DRAWING_GROUP,
19830
+ VOLUME_TREND_OPTION,
19613
19831
  VWAP_INDICATOR,
19614
19832
  WATERMARK_INDICATOR,
19615
19833
  arrowHeadPoints,
@@ -19627,6 +19845,7 @@ function setCSSCustomProperty(name, value, el) {
19627
19845
  isEditableTarget,
19628
19846
  marketStatus,
19629
19847
  mergeLabels,
19848
+ normalizeHalt,
19630
19849
  parseLocalDateTime,
19631
19850
  parseResolution,
19632
19851
  priceRangeLabel,
@@ -19645,7 +19864,8 @@ function setCSSCustomProperty(name, value, el) {
19645
19864
  themeStyles,
19646
19865
  timezoneLabel,
19647
19866
  toHeikinAshi,
19648
- tradeBoxFigures
19867
+ tradeBoxFigures,
19868
+ volumeTrend
19649
19869
  });
19650
19870
  /*! Bundled license information:
19651
19871