@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 +14 -4
- package/dist/index.cjs +238 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +72 -3
- package/dist/index.d.ts +72 -3
- package/dist/index.js +231 -17
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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 `
|
|
218
|
-
(registered automatically). Session tinting shades pre-market
|
|
219
|
-
and after-hours (16:00–20:00 ET); pattern markers draw a
|
|
220
|
-
each setup's bar (green up for longs, red down for
|
|
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
|
-
|
|
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
|
|
15955
|
-
|
|
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
|
-
|
|
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
|
-
|
|
16653
|
-
|
|
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
|
|