@rioaxyz/sonrchart 0.1.0 → 0.2.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,10 @@ 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) — `volumeTrend` on `ChartHeader` /
47
+ `SonrChartShell`, or subscribe via `subscribeVolumeTrend` on the chart handle.
44
48
  - **Fast & dependency-light.** The only runtime dependency is KLineChart;
45
49
  React is a peer. The per-frame hot paths are tuned (see
46
50
  [PERFORMANCE.md](PERFORMANCE.md)).
@@ -151,6 +155,7 @@ function TradingChart({ source }: { source: DataSource }) {
151
155
  | `paneHeights` | `Record<string, number>` | Sub-pane pixel heights, keyed by indicator name. |
152
156
  | `sessions` | `boolean \| { premarket?, afterhours? }` | Tint pre/after-hours session backgrounds (ET). |
153
157
  | `patterns` | `PatternMarker[]` | Setup markers (`{ time, kind, label }`) drawn on the price pane. |
158
+ | `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
159
  | `priceLines` | `PriceLine[]` | Static horizontal reference lines (entry / stop / target). |
155
160
  | `orderLines` | `OrderLine[]` | Draggable order/position lines; `onMove(price, phase)` fires as the user drags. |
156
161
  | `priceScale` | `{ type?, invert?, inside? }` | Scale mode — `"normal" \| "percentage" \| "logarithm"`, plus invert / labels-inside. |
@@ -214,10 +219,13 @@ const ref = useRef<SonrChartHandle>(null);
214
219
 
215
220
  ### Sonr custom indicators
216
221
 
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).
222
+ `sessions`, `patterns`, and `halted` are Sonr's own custom KLineChart
223
+ indicators (registered automatically). Session tinting shades pre-market
224
+ (04:00–09:30 ET) and after-hours (16:00–20:00 ET); pattern markers draw a
225
+ labeled triangle at each setup's bar (green up for longs, red down for
226
+ shorts); the halt badge pins a rounded pill to the top-right of the price
227
+ pane while a symbol is trading-halted (drawn on the canvas, so it's included
228
+ in snapshots).
221
229
 
222
230
  ## Develop
223
231
 
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,
@@ -87,6 +90,7 @@ __export(index_exports, {
87
90
  isEditableTarget: () => isEditableTarget,
88
91
  marketStatus: () => marketStatus,
89
92
  mergeLabels: () => mergeLabels,
93
+ normalizeHalt: () => normalizeHalt,
90
94
  parseLocalDateTime: () => parseLocalDateTime,
91
95
  parseResolution: () => parseResolution,
92
96
  priceRangeLabel: () => priceRangeLabel,
@@ -105,7 +109,8 @@ __export(index_exports, {
105
109
  themeStyles: () => themeStyles,
106
110
  timezoneLabel: () => timezoneLabel,
107
111
  toHeikinAshi: () => toHeikinAshi,
108
- tradeBoxFigures: () => tradeBoxFigures
112
+ tradeBoxFigures: () => tradeBoxFigures,
113
+ volumeTrend: () => volumeTrend
109
114
  });
110
115
  module.exports = __toCommonJS(index_exports);
111
116
 
@@ -13776,14 +13781,26 @@ var SESSION_INDICATOR = "SONR_SESSION";
13776
13781
  var PATTERN_INDICATOR = "SONR_PATTERNS";
13777
13782
  var WATERMARK_INDICATOR = "SONR_WATERMARK";
13778
13783
  var SERIES_INDICATOR = "SONR_SERIES";
13784
+ var HALT_INDICATOR = "SONR_HALT";
13779
13785
  var VWAP_INDICATOR = "VWAP";
13780
13786
  var Z_ORDER = {
13781
13787
  watermark: -3,
13782
13788
  session: -2,
13783
13789
  series: -1,
13784
13790
  compare: 0,
13785
- pattern: 10
13791
+ pattern: 10,
13792
+ halt: 11
13786
13793
  };
13794
+ var DEFAULT_HALT_BADGE = { label: "Halt", color: "#2196f3", textColor: "#ffffff" };
13795
+ function normalizeHalt(prop) {
13796
+ if (!prop) return null;
13797
+ if (prop === true) return DEFAULT_HALT_BADGE;
13798
+ return {
13799
+ label: prop.label ?? DEFAULT_HALT_BADGE.label,
13800
+ color: prop.color ?? DEFAULT_HALT_BADGE.color,
13801
+ textColor: prop.textColor ?? DEFAULT_HALT_BADGE.textColor
13802
+ };
13803
+ }
13787
13804
  var VWAP_COLOR = "#a970ff";
13788
13805
  function computeVWAP(dataList) {
13789
13806
  let day = Number.NaN;
@@ -14035,6 +14052,41 @@ function registerSonrIndicators() {
14035
14052
  return false;
14036
14053
  }
14037
14054
  });
14055
+ registerIndicator({
14056
+ name: HALT_INDICATOR,
14057
+ shortName: "",
14058
+ series: "price",
14059
+ figures: [],
14060
+ calc: (dataList) => dataList.map(() => ({})),
14061
+ draw: ({ ctx, bounding, indicator }) => {
14062
+ const cfg = indicator.extendData;
14063
+ if (!cfg || !cfg.label) return false;
14064
+ const padX = 8;
14065
+ const h = 20;
14066
+ const inset = 8;
14067
+ ctx.save();
14068
+ ctx.font = '600 11px ui-sans-serif, -apple-system, "Segoe UI", sans-serif';
14069
+ const w = Math.ceil(ctx.measureText(cfg.label).width) + padX * 2;
14070
+ const x = bounding.width - w - inset;
14071
+ const y = (bounding.top ?? 0) + inset;
14072
+ const r = h / 2;
14073
+ ctx.beginPath();
14074
+ ctx.moveTo(x + r, y);
14075
+ ctx.arcTo(x + w, y, x + w, y + h, r);
14076
+ ctx.arcTo(x + w, y + h, x, y + h, r);
14077
+ ctx.arcTo(x, y + h, x, y, r);
14078
+ ctx.arcTo(x, y, x + w, y, r);
14079
+ ctx.closePath();
14080
+ ctx.fillStyle = cfg.color;
14081
+ ctx.fill();
14082
+ ctx.fillStyle = cfg.textColor;
14083
+ ctx.textAlign = "center";
14084
+ ctx.textBaseline = "middle";
14085
+ ctx.fillText(cfg.label, x + w / 2, y + h / 2 + 0.5);
14086
+ ctx.restore();
14087
+ return false;
14088
+ }
14089
+ });
14038
14090
  registerIndicator({
14039
14091
  name: PATTERN_INDICATOR,
14040
14092
  shortName: "",
@@ -15387,6 +15439,25 @@ function formatWithUnit(text2, quote) {
15387
15439
  return out;
15388
15440
  }
15389
15441
 
15442
+ // src/volumeTrend.ts
15443
+ var DEFAULT_VOLUME_TREND_BARS = 3;
15444
+ function volumeTrend(list, endIndex, bars = DEFAULT_VOLUME_TREND_BARS) {
15445
+ const n = Math.max(1, Math.floor(bars));
15446
+ if (endIndex < n || endIndex >= list.length) return null;
15447
+ let up = 0;
15448
+ let down = 0;
15449
+ for (let i = endIndex - n + 1; i <= endIndex; i++) {
15450
+ const v = list[i]?.volume;
15451
+ const pv = list[i - 1]?.volume;
15452
+ if (typeof v !== "number" || typeof pv !== "number") return null;
15453
+ if (v > pv) up++;
15454
+ else if (v < pv) down++;
15455
+ }
15456
+ if (up === n) return "increasing";
15457
+ if (down === n) return "decreasing";
15458
+ return "mixed";
15459
+ }
15460
+
15390
15461
  // src/SonrChart.tsx
15391
15462
  var import_jsx_runtime = require("react/jsx-runtime");
15392
15463
  registerSonrIndicators();
@@ -15396,7 +15467,7 @@ registerShapeOverlays();
15396
15467
  registerExtendedDrawings();
15397
15468
  registerTradeReview();
15398
15469
  registerTextOverlay();
15399
- var INTERNAL_INDICATORS = /* @__PURE__ */ new Set([SESSION_INDICATOR, PATTERN_INDICATOR, COMPARE_INDICATOR, WATERMARK_INDICATOR, SERIES_INDICATOR]);
15470
+ var INTERNAL_INDICATORS = /* @__PURE__ */ new Set([SESSION_INDICATOR, PATTERN_INDICATOR, COMPARE_INDICATOR, WATERMARK_INDICATOR, SERIES_INDICATOR, HALT_INDICATOR]);
15400
15471
  var isUserIndicator = (name) => !INTERNAL_INDICATORS.has(name);
15401
15472
  function magnetMode(m) {
15402
15473
  if (m === true || m === "weak") return "weak_magnet";
@@ -15496,6 +15567,7 @@ var SonrChart = (0, import_react2.forwardRef)(function SonrChart2({
15496
15567
  sessions: sessionsProp,
15497
15568
  watermark: watermarkProp,
15498
15569
  patterns,
15570
+ halted,
15499
15571
  priceLines,
15500
15572
  orderLines,
15501
15573
  priceScale: priceScaleProp,
@@ -15613,14 +15685,22 @@ var SonrChart = (0, import_react2.forwardRef)(function SonrChart2({
15613
15685
  const legendRef = (0, import_react2.useRef)(legend);
15614
15686
  legendRef.current = legend;
15615
15687
  const headerSubs = (0, import_react2.useRef)(/* @__PURE__ */ new Set());
15688
+ const trendSubs = (0, import_react2.useRef)(/* @__PURE__ */ new Set());
15689
+ const trendBarsRef = (0, import_react2.useRef)(DEFAULT_VOLUME_TREND_BARS);
15690
+ const trendBarsSubs = (0, import_react2.useRef)(/* @__PURE__ */ new Set());
15616
15691
  const crosshairActiveRef = (0, import_react2.useRef)(false);
15692
+ const headerIdxRef = (0, import_react2.useRef)(-1);
15693
+ const emitHeaderAt = (list, idx) => {
15694
+ headerIdxRef.current = idx;
15695
+ const current = idx >= 0 ? list[idx] ?? null : null;
15696
+ const prev = idx > 0 ? list[idx - 1] : null;
15697
+ headerSubs.current.forEach((cb) => cb(current, prev));
15698
+ trendSubs.current.forEach((s) => s.cb(volumeTrend(list, idx, s.bars ?? trendBarsRef.current)));
15699
+ };
15617
15700
  const emitLatestHeader = () => {
15618
15701
  if (crosshairActiveRef.current) return;
15619
15702
  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));
15703
+ emitHeaderAt(list, list.length - 1);
15624
15704
  };
15625
15705
  const haRef = (0, import_react2.useRef)(new HaTransformer());
15626
15706
  const [textEdit, setTextEdit] = (0, import_react2.useState)(null);
@@ -15888,6 +15968,30 @@ var SonrChart = (0, import_react2.forwardRef)(function SonrChart2({
15888
15968
  const n = list.length;
15889
15969
  cb(n ? list[n - 1] : null, n > 1 ? list[n - 2] : null);
15890
15970
  return () => headerSubs.current.delete(cb);
15971
+ },
15972
+ // Volume trend (#79) ---------------------------------------------------
15973
+ subscribeVolumeTrend: (cb, bars) => {
15974
+ const sub = { bars: bars === void 0 ? void 0 : Math.max(1, Math.floor(bars)), cb };
15975
+ trendSubs.current.add(sub);
15976
+ const list = chartRef.current?.getDataList() ?? [];
15977
+ cb(volumeTrend(list, list.length - 1, sub.bars ?? trendBarsRef.current));
15978
+ return () => trendSubs.current.delete(sub);
15979
+ },
15980
+ getVolumeTrendBars: () => trendBarsRef.current,
15981
+ setVolumeTrendBars: (bars) => {
15982
+ const n = Math.max(1, Math.floor(bars));
15983
+ if (!Number.isFinite(n) || n === trendBarsRef.current) return;
15984
+ trendBarsRef.current = n;
15985
+ const list = chartRef.current?.getDataList() ?? [];
15986
+ const idx = crosshairActiveRef.current ? headerIdxRef.current : list.length - 1;
15987
+ trendSubs.current.forEach((s) => {
15988
+ if (s.bars === void 0) s.cb(volumeTrend(list, idx, n));
15989
+ });
15990
+ trendBarsSubs.current.forEach((cb) => cb(n));
15991
+ },
15992
+ subscribeVolumeTrendBars: (cb) => {
15993
+ trendBarsSubs.current.add(cb);
15994
+ return () => trendBarsSubs.current.delete(cb);
15891
15995
  }
15892
15996
  },
15893
15997
  // paneHeights/priceScale captured by closure; recreated per render is fine.
@@ -15951,16 +16055,29 @@ var SonrChart = (0, import_react2.forwardRef)(function SonrChart2({
15951
16055
  chart.setStyles(mergeStyles(themeStyleObj, styles2));
15952
16056
  chart.subscribeAction("onCrosshairChange", (d) => {
15953
16057
  const cross = d;
15954
- const k = cross?.kLineData;
15955
- if (k && typeof cross?.dataIndex === "number") {
16058
+ const list = chart.getDataList();
16059
+ let k = cross?.kLineData ?? null;
16060
+ let idx = typeof cross?.dataIndex === "number" ? cross.dataIndex : -1;
16061
+ if (!k && typeof cross?.x === "number" && list.length > 0) {
16062
+ const [p] = chart.convertFromPixel([{ x: cross.x }], { paneId: CANDLE_PANE });
16063
+ if (typeof p?.dataIndex === "number") {
16064
+ idx = Math.max(0, Math.min(p.dataIndex, list.length - 1));
16065
+ k = list[idx] ?? null;
16066
+ }
16067
+ }
16068
+ if (k && idx >= 0) {
15956
16069
  crosshairActiveRef.current = true;
15957
- const list = chart.getDataList();
15958
- headerSubs.current.forEach((cb) => cb(k, cross.dataIndex > 0 ? list[cross.dataIndex - 1] : null));
16070
+ emitHeaderAt(list, idx);
15959
16071
  } else {
15960
16072
  crosshairActiveRef.current = false;
15961
16073
  emitLatestHeader();
15962
16074
  }
15963
16075
  });
16076
+ const onPointerLeave = () => {
16077
+ crosshairActiveRef.current = false;
16078
+ emitLatestHeader();
16079
+ };
16080
+ el.addEventListener("mouseleave", onPointerLeave);
15964
16081
  applyCandleStyle(chart, candleStyleRef.current, themeStyleObj, baseTheme);
15965
16082
  applyPriceLabels(chart, priceLabelsRef.current);
15966
16083
  applyFormatter(chart);
@@ -16070,6 +16187,7 @@ var SonrChart = (0, import_react2.forwardRef)(function SonrChart2({
16070
16187
  return () => {
16071
16188
  disposed = true;
16072
16189
  unsub();
16190
+ el.removeEventListener("mouseleave", onPointerLeave);
16073
16191
  dispose(idRef.current);
16074
16192
  chartRef.current = null;
16075
16193
  overlayIds.current = [];
@@ -16141,6 +16259,19 @@ var SonrChart = (0, import_react2.forwardRef)(function SonrChart2({
16141
16259
  chart.removeIndicator({ name: PATTERN_INDICATOR });
16142
16260
  }
16143
16261
  }, [patterns, chartEpoch]);
16262
+ (0, import_react2.useEffect)(() => {
16263
+ const chart = chartRef.current;
16264
+ if (!chart) return;
16265
+ const present = chart.getIndicators({ name: HALT_INDICATOR }).length > 0;
16266
+ const badge = normalizeHalt(halted);
16267
+ if (badge && !present) {
16268
+ chart.createIndicator({ name: HALT_INDICATOR, paneId: CANDLE_PANE, zLevel: Z_ORDER.halt, extendData: badge }, true);
16269
+ } else if (badge && present) {
16270
+ chart.overrideIndicator({ name: HALT_INDICATOR, extendData: badge });
16271
+ } else if (!badge && present) {
16272
+ chart.removeIndicator({ name: HALT_INDICATOR });
16273
+ }
16274
+ }, [halted, chartEpoch]);
16144
16275
  (0, import_react2.useEffect)(() => {
16145
16276
  const chart = chartRef.current;
16146
16277
  if (!chart) return;
@@ -16394,7 +16525,7 @@ var CSS = `
16394
16525
  :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
16526
  :where(.sonr-mkt-ticks){grid-column:2;display:flex;justify-content:space-between;font-size:10px;color:#888;}
16396
16527
  :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;}
16528
+ :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
16529
  :where(.sonr-hd-sym){font-weight:700;color:#000;}
16399
16530
  :where(.sonr-hd-ohlc){display:inline-flex;align-items:center;gap:8px;font-variant-numeric:tabular-nums;}
16400
16531
  :where(.sonr-hd-cell){display:inline-flex;align-items:baseline;gap:3px;}
@@ -16402,6 +16533,8 @@ var CSS = `
16402
16533
  :where(.sonr-hd-v){color:#555;}
16403
16534
  :where(.sonr-hd-up){color:#22a06b;}
16404
16535
  :where(.sonr-hd-down){color:#d23c3c;}
16536
+ :where(.sonr-hd-trend){flex-basis:100%;display:inline-flex;align-items:baseline;gap:4px;margin-top:2px;}
16537
+ :where(.sonr-hd-mixed){color:#c98a1b;}
16405
16538
  :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
16539
  :where(.sonr-brand-img){display:block;}
16407
16540
  :where(.sonr-brand-link){pointer-events:auto;color:inherit;text-decoration:none;}
@@ -16577,6 +16710,10 @@ var DEFAULT_LABELS = {
16577
16710
  zoomIn: "Zoom in",
16578
16711
  fitContent: "Fit all data",
16579
16712
  goToRealtime: "Go to realtime",
16713
+ volumeTrend: "Vol trend",
16714
+ volumeTrendIncreasing: "Increasing",
16715
+ volumeTrendDecreasing: "Decreasing",
16716
+ volumeTrendMixed: "Mixed",
16580
16717
  marketPre: "Pre-market",
16581
16718
  marketOpen: "Open",
16582
16719
  marketAfter: "After-hours",
@@ -18095,6 +18232,7 @@ function SettingsModal({
18095
18232
  const [indicators2, setIndicators] = (0, import_react14.useState)([]);
18096
18233
  const [indColors, setIndColors] = (0, import_react14.useState)({});
18097
18234
  const [indParams, setIndParams] = (0, import_react14.useState)({});
18235
+ const [trendBars, setTrendBars] = (0, import_react14.useState)(DEFAULT_VOLUME_TREND_BARS);
18098
18236
  const [watermark, setWatermark] = (0, import_react14.useState)({ symbol: true, interval: true, color: "#e6ebe9", opacity: 0.06 });
18099
18237
  const [brushStyles, setBrushStyles] = (0, import_react14.useState)({
18100
18238
  brush: { color: "#e6ebe9", size: 2 },
@@ -18129,6 +18267,8 @@ function SettingsModal({
18129
18267
  }
18130
18268
  }
18131
18269
  setIndParams(params);
18270
+ setTrendBars(chartRef.current?.getVolumeTrendBars?.() ?? DEFAULT_VOLUME_TREND_BARS);
18271
+ const unsubTrendBars = chartRef.current?.subscribeVolumeTrendBars?.(setTrendBars);
18132
18272
  const readWm = () => {
18133
18273
  const w = chartRef.current?.getWatermark?.();
18134
18274
  if (w) setWatermark(w);
@@ -18171,6 +18311,7 @@ function SettingsModal({
18171
18311
  document.addEventListener("keydown", onKey);
18172
18312
  return () => {
18173
18313
  document.removeEventListener("keydown", onKey);
18314
+ unsubTrendBars?.();
18174
18315
  unsubWm?.();
18175
18316
  unsubBrush?.();
18176
18317
  unsubText?.();
@@ -18279,6 +18420,25 @@ function SettingsModal({
18279
18420
  ) })
18280
18421
  ] }),
18281
18422
  section === "indicators" && /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(import_jsx_runtime15.Fragment, { children: [
18423
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className: "sonr-set-subhead", children: L.volumeTrend }),
18424
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Row, { label: "Bars (lookback)", children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
18425
+ "input",
18426
+ {
18427
+ type: "number",
18428
+ className: "sonr-set-num",
18429
+ min: "1",
18430
+ max: "100",
18431
+ value: trendBars,
18432
+ onChange: (e) => {
18433
+ const v = e.target.valueAsNumber;
18434
+ if (Number.isNaN(v)) return;
18435
+ const n = Math.max(1, Math.min(100, Math.floor(v)));
18436
+ setTrendBars(n);
18437
+ chartRef.current?.setVolumeTrendBars?.(n);
18438
+ }
18439
+ }
18440
+ ) }),
18441
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className: "sonr-set-subhead", children: "Active indicators" }),
18282
18442
  /* @__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
18443
  indicators2.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("p", { className: "sonr-set-empty", children: "No indicators active." }),
18284
18444
  indicators2.map((name) => {
@@ -18908,18 +19068,25 @@ var fmtVolume = (v) => {
18908
19068
  if (n >= 1e3) return `${(n / 1e3).toFixed(2)}K`;
18909
19069
  return `${n}`;
18910
19070
  };
18911
- function ChartHeader({ chartRef, symbol, interval, source, pricePrecision = 2, labels, className }) {
19071
+ function ChartHeader({ chartRef, symbol, interval, source, pricePrecision = 2, volumeTrend: volumeTrend2, labels, className }) {
18912
19072
  (0, import_react17.useEffect)(ensureDefaultStyles, []);
18913
19073
  const L = (0, import_react17.useMemo)(() => mergeLabels(labels), [labels]);
18914
19074
  const [bar, setBar] = (0, import_react17.useState)({ current: null, prev: null });
19075
+ const [trend, setTrend] = (0, import_react17.useState)(null);
19076
+ const trendOn = volumeTrend2 === true || typeof volumeTrend2 === "number" && volumeTrend2 > 0;
18915
19077
  (0, import_react17.useEffect)(() => {
18916
19078
  let cancelled = false;
18917
19079
  let unsub;
19080
+ let unsubTrend;
18918
19081
  const attach = () => {
18919
19082
  if (cancelled) return;
18920
19083
  const h = chartRef.current;
18921
19084
  if (h?.subscribeHeaderBar) {
18922
19085
  unsub = h.subscribeHeaderBar((current, prev) => setBar({ current, prev }));
19086
+ if (trendOn) {
19087
+ if (typeof volumeTrend2 === "number") h.setVolumeTrendBars?.(volumeTrend2);
19088
+ unsubTrend = h.subscribeVolumeTrend?.((t) => setTrend(t));
19089
+ }
18923
19090
  } else {
18924
19091
  setTimeout(attach, 50);
18925
19092
  }
@@ -18928,8 +19095,9 @@ function ChartHeader({ chartRef, symbol, interval, source, pricePrecision = 2, l
18928
19095
  return () => {
18929
19096
  cancelled = true;
18930
19097
  unsub?.();
19098
+ unsubTrend?.();
18931
19099
  };
18932
- }, [chartRef]);
19100
+ }, [chartRef, volumeTrend2, trendOn]);
18933
19101
  const k = bar.current;
18934
19102
  const up = (k?.close ?? 0) >= (k?.open ?? 0);
18935
19103
  const dirClass = up ? "sonr-hd-up" : "sonr-hd-down";
@@ -18970,6 +19138,10 @@ function ChartHeader({ chartRef, symbol, interval, source, pricePrecision = 2, l
18970
19138
  /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: "sonr-hd-k", children: "V" }),
18971
19139
  /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: "sonr-hd-v", children: fmtVolume(k.volume) })
18972
19140
  ] })
19141
+ ] }),
19142
+ trendOn && trend && /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("span", { className: "sonr-hd-trend", children: [
19143
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: "sonr-hd-k", children: L.volumeTrend }),
19144
+ /* @__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
19145
  ] })
18974
19146
  ] });
18975
19147
  }
@@ -19432,6 +19604,7 @@ var SonrChartShell = (0, import_react24.forwardRef)(function SonrChartShell2({
19432
19604
  ranges,
19433
19605
  source,
19434
19606
  exchange = source,
19607
+ volumeTrend: volumeTrend2 = true,
19435
19608
  onIntervalChange,
19436
19609
  onSymbolChange,
19437
19610
  brand,
@@ -19505,7 +19678,7 @@ var SonrChartShell = (0, import_react24.forwardRef)(function SonrChartShell2({
19505
19678
  style: { position: "absolute", inset: 0 }
19506
19679
  }
19507
19680
  ),
19508
- /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("div", { className: "sonr-tile-header", children: /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(ChartHeader, { chartRef, symbol, interval, source }) }),
19681
+ /* @__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
19682
  /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("div", { className: "sonr-tile-nav", children: /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(NavControlBar, { chartRef, labels }) }),
19510
19683
  brand != null && /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(BrandMark, { position: brandCorner, opacity: brandOpacity, children: brand })
19511
19684
  ] }),
@@ -19568,6 +19741,7 @@ function setCSSCustomProperty(name, value, el) {
19568
19741
  ConfirmDialog,
19569
19742
  DEFAULT_CANDLE_STYLES,
19570
19743
  DEFAULT_DRAWING_TOOLS,
19744
+ DEFAULT_HALT_BADGE,
19571
19745
  DEFAULT_INDICATOR_OPTIONS,
19572
19746
  DEFAULT_LABELS,
19573
19747
  DEFAULT_RANGES,
@@ -19575,6 +19749,7 @@ function setCSSCustomProperty(name, value, el) {
19575
19749
  DEFAULT_SESSION_COLORS,
19576
19750
  DEFAULT_TIMEZONE,
19577
19751
  DEFAULT_TIMEZONES,
19752
+ DEFAULT_VOLUME_TREND_BARS,
19578
19753
  DataWindow,
19579
19754
  DrawingTemplateStore,
19580
19755
  DrawingToolbar,
@@ -19582,6 +19757,7 @@ function setCSSCustomProperty(name, value, el) {
19582
19757
  EXTENDED_OVERLAYS,
19583
19758
  FIB_EXTENSION_LEVELS,
19584
19759
  GoToDateDialog,
19760
+ HALT_INDICATOR,
19585
19761
  IndicatorMenu,
19586
19762
  MarketStatus,
19587
19763
  NavControlBar,
@@ -19627,6 +19803,7 @@ function setCSSCustomProperty(name, value, el) {
19627
19803
  isEditableTarget,
19628
19804
  marketStatus,
19629
19805
  mergeLabels,
19806
+ normalizeHalt,
19630
19807
  parseLocalDateTime,
19631
19808
  parseResolution,
19632
19809
  priceRangeLabel,
@@ -19645,7 +19822,8 @@ function setCSSCustomProperty(name, value, el) {
19645
19822
  themeStyles,
19646
19823
  timezoneLabel,
19647
19824
  toHeikinAshi,
19648
- tradeBoxFigures
19825
+ tradeBoxFigures,
19826
+ volumeTrend
19649
19827
  });
19650
19828
  /*! Bundled license information:
19651
19829