@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 +12 -4
- package/dist/index.cjs +194 -16
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +59 -3
- package/dist/index.d.ts +59 -3
- package/dist/index.js +188 -15
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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 `
|
|
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
|
|
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
|
-
|
|
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
|
|
15955
|
-
|
|
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
|
-
|
|
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
|
|