hyperprop-charting-library 0.1.144 → 0.1.146
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/dist/hyperprop-charting-library.cjs +564 -4
- package/dist/hyperprop-charting-library.d.ts +28 -1
- package/dist/hyperprop-charting-library.js +564 -4
- package/dist/index.cjs +564 -4
- package/dist/index.d.cts +28 -1
- package/dist/index.d.ts +28 -1
- package/dist/index.js +564 -4
- package/docs/API.md +12 -0
- package/package.json +2 -2
|
@@ -2071,6 +2071,493 @@ var BUILTIN_DONCHIAN_INDICATOR = {
|
|
|
2071
2071
|
return rangeOfSeries([upper, lower], startIndex, endIndex, skipIndex);
|
|
2072
2072
|
}
|
|
2073
2073
|
};
|
|
2074
|
+
var computeVolumeProfile = (data, fromIndex, toIndex, rowCount, valueAreaPercent) => {
|
|
2075
|
+
const from = Math.max(0, fromIndex);
|
|
2076
|
+
const to = Math.min(data.length - 1, toIndex);
|
|
2077
|
+
if (to < from) return null;
|
|
2078
|
+
let low = Number.POSITIVE_INFINITY;
|
|
2079
|
+
let high = Number.NEGATIVE_INFINITY;
|
|
2080
|
+
for (let i = from; i <= to; i += 1) {
|
|
2081
|
+
const bar = data[i];
|
|
2082
|
+
if (!bar) continue;
|
|
2083
|
+
if (bar.l < low) low = bar.l;
|
|
2084
|
+
if (bar.h > high) high = bar.h;
|
|
2085
|
+
}
|
|
2086
|
+
if (!Number.isFinite(low) || !Number.isFinite(high)) return null;
|
|
2087
|
+
if (high - low < Number.EPSILON) {
|
|
2088
|
+
const pad = Math.max(Math.abs(high) * 1e-6, 1e-6);
|
|
2089
|
+
low -= pad;
|
|
2090
|
+
high += pad;
|
|
2091
|
+
}
|
|
2092
|
+
const rows = Math.max(4, Math.min(500, Math.floor(rowCount) || 24));
|
|
2093
|
+
const rowSize = (high - low) / rows;
|
|
2094
|
+
const buckets = Array.from({ length: rows }, (_, index) => ({
|
|
2095
|
+
low: low + index * rowSize,
|
|
2096
|
+
high: low + (index + 1) * rowSize,
|
|
2097
|
+
up: 0,
|
|
2098
|
+
down: 0,
|
|
2099
|
+
total: 0
|
|
2100
|
+
}));
|
|
2101
|
+
let totalVolume = 0;
|
|
2102
|
+
for (let i = from; i <= to; i += 1) {
|
|
2103
|
+
const bar = data[i];
|
|
2104
|
+
if (!bar) continue;
|
|
2105
|
+
const volume = Math.max(0, bar.v ?? 0);
|
|
2106
|
+
if (volume <= 0) continue;
|
|
2107
|
+
const firstRow = Math.max(0, Math.min(rows - 1, Math.floor((bar.l - low) / rowSize)));
|
|
2108
|
+
const lastRow = Math.max(0, Math.min(rows - 1, Math.floor((bar.h - low) / rowSize)));
|
|
2109
|
+
const share = volume / (lastRow - firstRow + 1);
|
|
2110
|
+
const up = bar.c >= bar.o;
|
|
2111
|
+
for (let row = firstRow; row <= lastRow; row += 1) {
|
|
2112
|
+
const bucket = buckets[row];
|
|
2113
|
+
if (up) bucket.up += share;
|
|
2114
|
+
else bucket.down += share;
|
|
2115
|
+
bucket.total += share;
|
|
2116
|
+
}
|
|
2117
|
+
totalVolume += volume;
|
|
2118
|
+
}
|
|
2119
|
+
if (totalVolume <= 0) return null;
|
|
2120
|
+
let pocIndex = 0;
|
|
2121
|
+
let maxRowVolume = 0;
|
|
2122
|
+
for (let row = 0; row < rows; row += 1) {
|
|
2123
|
+
const total = buckets[row].total;
|
|
2124
|
+
if (total > maxRowVolume) {
|
|
2125
|
+
maxRowVolume = total;
|
|
2126
|
+
pocIndex = row;
|
|
2127
|
+
}
|
|
2128
|
+
}
|
|
2129
|
+
const target = totalVolume * Math.min(0.99, Math.max(0.1, valueAreaPercent / 100));
|
|
2130
|
+
let valueAreaFrom = pocIndex;
|
|
2131
|
+
let valueAreaTo = pocIndex;
|
|
2132
|
+
let accumulated = buckets[pocIndex].total;
|
|
2133
|
+
while (accumulated < target && (valueAreaFrom > 0 || valueAreaTo < rows - 1)) {
|
|
2134
|
+
const above = valueAreaTo < rows - 1 ? buckets[valueAreaTo + 1].total : -1;
|
|
2135
|
+
const below = valueAreaFrom > 0 ? buckets[valueAreaFrom - 1].total : -1;
|
|
2136
|
+
if (above < 0 && below < 0) break;
|
|
2137
|
+
if (above >= below) {
|
|
2138
|
+
valueAreaTo += 1;
|
|
2139
|
+
accumulated += above;
|
|
2140
|
+
} else {
|
|
2141
|
+
valueAreaFrom -= 1;
|
|
2142
|
+
accumulated += below;
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
return { rows: buckets, rowSize, low, high, maxRowVolume, totalVolume, pocIndex, valueAreaFrom, valueAreaTo };
|
|
2146
|
+
};
|
|
2147
|
+
var sessionSegments = (renderContext, fromIndex, toIndex) => {
|
|
2148
|
+
const session = renderContext.session;
|
|
2149
|
+
if (!session) return [{ from: fromIndex, to: toIndex }];
|
|
2150
|
+
const segments = [];
|
|
2151
|
+
let start = fromIndex;
|
|
2152
|
+
let day = session.dayAt(fromIndex);
|
|
2153
|
+
for (let index = fromIndex + 1; index <= toIndex; index += 1) {
|
|
2154
|
+
const current = session.dayAt(index);
|
|
2155
|
+
if (current !== day) {
|
|
2156
|
+
segments.push({ from: start, to: index - 1 });
|
|
2157
|
+
start = index;
|
|
2158
|
+
day = current;
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
segments.push({ from: start, to: toIndex });
|
|
2162
|
+
return segments;
|
|
2163
|
+
};
|
|
2164
|
+
var withAlpha = (color, alpha) => {
|
|
2165
|
+
const clamped = Math.max(0, Math.min(1, alpha));
|
|
2166
|
+
if (color.startsWith("#") && (color.length === 7 || color.length === 4)) {
|
|
2167
|
+
const hex = color.length === 4 ? `#${color[1]}${color[1]}${color[2]}${color[2]}${color[3]}${color[3]}` : color;
|
|
2168
|
+
const r = Number.parseInt(hex.slice(1, 3), 16);
|
|
2169
|
+
const g = Number.parseInt(hex.slice(3, 5), 16);
|
|
2170
|
+
const b = Number.parseInt(hex.slice(5, 7), 16);
|
|
2171
|
+
return `rgba(${r},${g},${b},${clamped})`;
|
|
2172
|
+
}
|
|
2173
|
+
if (color.startsWith("rgba(")) return color;
|
|
2174
|
+
if (color.startsWith("rgb(")) return color.replace("rgb(", "rgba(").replace(")", `,${clamped})`);
|
|
2175
|
+
return color;
|
|
2176
|
+
};
|
|
2177
|
+
var BUILTIN_VOLUME_PROFILE_INDICATOR = {
|
|
2178
|
+
id: "volume-profile",
|
|
2179
|
+
name: "Volume Profile",
|
|
2180
|
+
pane: "overlay",
|
|
2181
|
+
defaultInputs: {
|
|
2182
|
+
// "visible" profiles everything on screen and follows pan/zoom; "session"
|
|
2183
|
+
// draws one profile per trading day inside that day's bars.
|
|
2184
|
+
scope: "visible",
|
|
2185
|
+
rows: 40,
|
|
2186
|
+
widthPercent: 28,
|
|
2187
|
+
placement: "right",
|
|
2188
|
+
valueAreaPercent: 70,
|
|
2189
|
+
upColor: "#26a69a",
|
|
2190
|
+
downColor: "#ef5350",
|
|
2191
|
+
valueAreaOpacity: 0.55,
|
|
2192
|
+
outsideOpacity: 0.22,
|
|
2193
|
+
showPoc: true,
|
|
2194
|
+
pocColor: "#f7a600",
|
|
2195
|
+
showValueArea: true,
|
|
2196
|
+
valueAreaColor: "#7e8aa3",
|
|
2197
|
+
showLabels: true
|
|
2198
|
+
},
|
|
2199
|
+
draw: (ctx, renderContext, inputs) => {
|
|
2200
|
+
const { yFromPrice, chartLeft, chartRight, chartTop, chartBottom, data } = renderContext;
|
|
2201
|
+
if (!yFromPrice || data.length === 0) return;
|
|
2202
|
+
const from = Math.max(0, renderContext.startIndex);
|
|
2203
|
+
const to = Math.min(data.length - 1, renderContext.endIndex);
|
|
2204
|
+
if (to < from) return;
|
|
2205
|
+
const rows = Math.max(4, Math.min(500, Math.floor(Number(inputs.rows)) || 40));
|
|
2206
|
+
const valueAreaPercent = Math.max(10, Math.min(99, Number(inputs.valueAreaPercent) || 70));
|
|
2207
|
+
const perSession = inputs.scope === "session" && renderContext.session !== void 0;
|
|
2208
|
+
const segments = perSession ? sessionSegments(renderContext, from, to) : [{ from, to }];
|
|
2209
|
+
const widthRatio = Math.max(0.02, Math.min(0.9, (Number(inputs.widthPercent) || 28) / 100));
|
|
2210
|
+
const placeLeft = inputs.placement === "left";
|
|
2211
|
+
ctx.save();
|
|
2212
|
+
ctx.beginPath();
|
|
2213
|
+
ctx.rect(chartLeft, chartTop, chartRight - chartLeft, chartBottom - chartTop);
|
|
2214
|
+
ctx.clip();
|
|
2215
|
+
for (const segment of segments) {
|
|
2216
|
+
const profile = withCachedComputation(
|
|
2217
|
+
`volume-profile|${perSession ? "s" : "v"}|${segment.from}|${segment.to}|${rows}|${valueAreaPercent}`,
|
|
2218
|
+
data,
|
|
2219
|
+
() => computeVolumeProfile(data, segment.from, segment.to, rows, valueAreaPercent)
|
|
2220
|
+
);
|
|
2221
|
+
if (!profile || profile.maxRowVolume <= 0) continue;
|
|
2222
|
+
const segmentLeft = renderContext.xFromIndex(segment.from) - renderContext.candleSpacing / 2;
|
|
2223
|
+
const segmentRight = renderContext.xFromIndex(segment.to) + renderContext.candleSpacing / 2;
|
|
2224
|
+
const available = perSession ? Math.max(0, Math.min(segmentRight, chartRight) - Math.max(segmentLeft, chartLeft)) : chartRight - chartLeft;
|
|
2225
|
+
const maxWidth = available * (perSession ? Math.min(1, widthRatio * 2.2) : widthRatio);
|
|
2226
|
+
if (maxWidth < (perSession ? 10 : 2)) continue;
|
|
2227
|
+
const anchorX = perSession ? Math.max(segmentLeft, chartLeft) : placeLeft ? chartLeft : chartRight;
|
|
2228
|
+
const direction = perSession ? 1 : placeLeft ? 1 : -1;
|
|
2229
|
+
for (let row = 0; row < profile.rows.length; row += 1) {
|
|
2230
|
+
const bucket = profile.rows[row];
|
|
2231
|
+
if (bucket.total <= 0) continue;
|
|
2232
|
+
const yTop = yFromPrice(bucket.high);
|
|
2233
|
+
const yBottom = yFromPrice(bucket.low);
|
|
2234
|
+
const height = Math.max(1, Math.abs(yBottom - yTop) - 1);
|
|
2235
|
+
const top = Math.min(yTop, yBottom);
|
|
2236
|
+
if (top > chartBottom || top + height < chartTop) continue;
|
|
2237
|
+
const inValueArea = row >= profile.valueAreaFrom && row <= profile.valueAreaTo;
|
|
2238
|
+
const alpha = inValueArea ? Math.max(0, Math.min(1, Number(inputs.valueAreaOpacity) || 0.55)) : Math.max(0, Math.min(1, Number(inputs.outsideOpacity) || 0.22));
|
|
2239
|
+
const rowWidth = bucket.total / profile.maxRowVolume * maxWidth;
|
|
2240
|
+
const upWidth = bucket.total > 0 ? rowWidth * (bucket.up / bucket.total) : 0;
|
|
2241
|
+
ctx.fillStyle = withAlpha(inputs.upColor ?? "#26a69a", alpha);
|
|
2242
|
+
ctx.fillRect(direction > 0 ? anchorX : anchorX - upWidth, top, upWidth, height);
|
|
2243
|
+
ctx.fillStyle = withAlpha(inputs.downColor ?? "#ef5350", alpha);
|
|
2244
|
+
const downWidth = rowWidth - upWidth;
|
|
2245
|
+
ctx.fillRect(
|
|
2246
|
+
direction > 0 ? anchorX + upWidth : anchorX - rowWidth,
|
|
2247
|
+
top,
|
|
2248
|
+
downWidth,
|
|
2249
|
+
height
|
|
2250
|
+
);
|
|
2251
|
+
}
|
|
2252
|
+
const lineFrom = direction > 0 ? anchorX : anchorX - maxWidth;
|
|
2253
|
+
const lineTo = direction > 0 ? anchorX + maxWidth : anchorX;
|
|
2254
|
+
const drawLevel = (price, color, label, dashed) => {
|
|
2255
|
+
const y = yFromPrice(price);
|
|
2256
|
+
if (y < chartTop || y > chartBottom) return;
|
|
2257
|
+
ctx.save();
|
|
2258
|
+
ctx.strokeStyle = color;
|
|
2259
|
+
ctx.lineWidth = 1;
|
|
2260
|
+
ctx.setLineDash(dashed ? [4, 3] : []);
|
|
2261
|
+
ctx.beginPath();
|
|
2262
|
+
ctx.moveTo(lineFrom, Math.round(y) + 0.5);
|
|
2263
|
+
ctx.lineTo(lineTo, Math.round(y) + 0.5);
|
|
2264
|
+
ctx.stroke();
|
|
2265
|
+
ctx.restore();
|
|
2266
|
+
if (inputs.showLabels && !perSession) {
|
|
2267
|
+
ctx.save();
|
|
2268
|
+
ctx.fillStyle = color;
|
|
2269
|
+
ctx.font = "10px system-ui, -apple-system, Segoe UI, Roboto, sans-serif";
|
|
2270
|
+
ctx.textAlign = direction > 0 ? "left" : "right";
|
|
2271
|
+
ctx.textBaseline = "bottom";
|
|
2272
|
+
ctx.fillText(label, direction > 0 ? lineFrom + 3 : lineTo - 3, y - 1);
|
|
2273
|
+
ctx.restore();
|
|
2274
|
+
}
|
|
2275
|
+
};
|
|
2276
|
+
const poc = profile.rows[profile.pocIndex];
|
|
2277
|
+
if (inputs.showValueArea) {
|
|
2278
|
+
const areaHigh = profile.rows[profile.valueAreaTo];
|
|
2279
|
+
const areaLow = profile.rows[profile.valueAreaFrom];
|
|
2280
|
+
const areaColor = inputs.valueAreaColor ?? "#7e8aa3";
|
|
2281
|
+
drawLevel(areaHigh.high, areaColor, "VAH", true);
|
|
2282
|
+
drawLevel(areaLow.low, areaColor, "VAL", true);
|
|
2283
|
+
}
|
|
2284
|
+
if (inputs.showPoc) {
|
|
2285
|
+
drawLevel((poc.low + poc.high) / 2, inputs.pocColor ?? "#f7a600", "POC", false);
|
|
2286
|
+
}
|
|
2287
|
+
}
|
|
2288
|
+
ctx.restore();
|
|
2289
|
+
}
|
|
2290
|
+
};
|
|
2291
|
+
var parseMinutes = (value, fallback) => {
|
|
2292
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
2293
|
+
const match = /^(\d{1,2}):?(\d{2})$/.exec(String(value ?? "").trim());
|
|
2294
|
+
if (!match) return fallback;
|
|
2295
|
+
const hour = Number(match[1]);
|
|
2296
|
+
const minute = Number(match[2]);
|
|
2297
|
+
if (!Number.isFinite(hour) || !Number.isFinite(minute)) return fallback;
|
|
2298
|
+
return hour * 60 + minute;
|
|
2299
|
+
};
|
|
2300
|
+
var computeSessionLevels = (data, session, rthStart, rthEnd, orMinutes) => {
|
|
2301
|
+
const lastIndex = data.length - 1;
|
|
2302
|
+
if (lastIndex < 0) return [];
|
|
2303
|
+
const currentDay = session.dayAt(lastIndex);
|
|
2304
|
+
if (!Number.isFinite(currentDay)) return [];
|
|
2305
|
+
let currentFrom = lastIndex;
|
|
2306
|
+
while (currentFrom > 0 && session.dayAt(currentFrom - 1) === currentDay) currentFrom -= 1;
|
|
2307
|
+
const priorTo = currentFrom - 1;
|
|
2308
|
+
const result = [];
|
|
2309
|
+
if (priorTo >= 0) {
|
|
2310
|
+
const priorDay = session.dayAt(priorTo);
|
|
2311
|
+
let priorFrom = priorTo;
|
|
2312
|
+
while (priorFrom > 0 && session.dayAt(priorFrom - 1) === priorDay) priorFrom -= 1;
|
|
2313
|
+
let high = Number.NEGATIVE_INFINITY;
|
|
2314
|
+
let low = Number.POSITIVE_INFINITY;
|
|
2315
|
+
for (let i = priorFrom; i <= priorTo; i += 1) {
|
|
2316
|
+
const bar = data[i];
|
|
2317
|
+
if (!bar) continue;
|
|
2318
|
+
if (bar.h > high) high = bar.h;
|
|
2319
|
+
if (bar.l < low) low = bar.l;
|
|
2320
|
+
}
|
|
2321
|
+
const close = data[priorTo]?.c;
|
|
2322
|
+
if (Number.isFinite(high)) result.push({ price: high, label: "PDH", color: "prior", fromIndex: currentFrom });
|
|
2323
|
+
if (Number.isFinite(low)) result.push({ price: low, label: "PDL", color: "prior", fromIndex: currentFrom });
|
|
2324
|
+
if (close !== void 0) result.push({ price: close, label: "PDC", color: "prior", fromIndex: currentFrom });
|
|
2325
|
+
}
|
|
2326
|
+
let onHigh = Number.NEGATIVE_INFINITY;
|
|
2327
|
+
let onLow = Number.POSITIVE_INFINITY;
|
|
2328
|
+
let orHigh = Number.NEGATIVE_INFINITY;
|
|
2329
|
+
let orLow = Number.POSITIVE_INFINITY;
|
|
2330
|
+
let ibHigh = Number.NEGATIVE_INFINITY;
|
|
2331
|
+
let ibLow = Number.POSITIVE_INFINITY;
|
|
2332
|
+
let orFromIndex = lastIndex;
|
|
2333
|
+
let ibFromIndex = lastIndex;
|
|
2334
|
+
for (let i = currentFrom; i <= lastIndex; i += 1) {
|
|
2335
|
+
const bar = data[i];
|
|
2336
|
+
if (!bar) continue;
|
|
2337
|
+
const minutes = session.minutesOfDayAt(i);
|
|
2338
|
+
if (!Number.isFinite(minutes)) continue;
|
|
2339
|
+
if (minutes < rthStart || minutes >= rthEnd) {
|
|
2340
|
+
if (bar.h > onHigh) onHigh = bar.h;
|
|
2341
|
+
if (bar.l < onLow) onLow = bar.l;
|
|
2342
|
+
continue;
|
|
2343
|
+
}
|
|
2344
|
+
if (minutes < rthStart + orMinutes) {
|
|
2345
|
+
if (bar.h > orHigh) orHigh = bar.h;
|
|
2346
|
+
if (bar.l < orLow) orLow = bar.l;
|
|
2347
|
+
orFromIndex = Math.min(orFromIndex, i);
|
|
2348
|
+
}
|
|
2349
|
+
if (minutes < rthStart + 60) {
|
|
2350
|
+
if (bar.h > ibHigh) ibHigh = bar.h;
|
|
2351
|
+
if (bar.l < ibLow) ibLow = bar.l;
|
|
2352
|
+
ibFromIndex = Math.min(ibFromIndex, i);
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2355
|
+
if (Number.isFinite(onHigh)) result.push({ price: onHigh, label: "ONH", color: "overnight", fromIndex: currentFrom });
|
|
2356
|
+
if (Number.isFinite(onLow)) result.push({ price: onLow, label: "ONL", color: "overnight", fromIndex: currentFrom });
|
|
2357
|
+
if (Number.isFinite(orHigh)) result.push({ price: orHigh, label: "ORH", color: "openingRange", fromIndex: orFromIndex });
|
|
2358
|
+
if (Number.isFinite(orLow)) result.push({ price: orLow, label: "ORL", color: "openingRange", fromIndex: orFromIndex });
|
|
2359
|
+
if (Number.isFinite(ibHigh)) result.push({ price: ibHigh, label: "IBH", color: "initialBalance", fromIndex: ibFromIndex });
|
|
2360
|
+
if (Number.isFinite(ibLow)) result.push({ price: ibLow, label: "IBL", color: "initialBalance", fromIndex: ibFromIndex });
|
|
2361
|
+
return result;
|
|
2362
|
+
};
|
|
2363
|
+
var BUILTIN_SESSION_LEVELS_INDICATOR = {
|
|
2364
|
+
id: "session-levels",
|
|
2365
|
+
name: "Session levels",
|
|
2366
|
+
pane: "overlay",
|
|
2367
|
+
defaultInputs: {
|
|
2368
|
+
showPriorDay: true,
|
|
2369
|
+
showOvernight: true,
|
|
2370
|
+
showOpeningRange: true,
|
|
2371
|
+
showInitialBalance: false,
|
|
2372
|
+
// Cash-session bounds in the session's own zone; everything before the
|
|
2373
|
+
// open on the same trading day counts as overnight.
|
|
2374
|
+
rthStart: "09:30",
|
|
2375
|
+
rthEnd: "16:00",
|
|
2376
|
+
openingRangeMinutes: 30,
|
|
2377
|
+
priorDayColor: "#8b93a7",
|
|
2378
|
+
overnightColor: "#7c5cff",
|
|
2379
|
+
openingRangeColor: "#26a69a",
|
|
2380
|
+
initialBalanceColor: "#f7a600",
|
|
2381
|
+
lineWidth: 1,
|
|
2382
|
+
showLabels: true
|
|
2383
|
+
},
|
|
2384
|
+
draw: (ctx, renderContext, inputs) => {
|
|
2385
|
+
const { yFromPrice, chartLeft, chartRight, chartTop, chartBottom, data, session } = renderContext;
|
|
2386
|
+
if (!yFromPrice || !session || data.length === 0) return;
|
|
2387
|
+
const rthStart = parseMinutes(inputs.rthStart, 570);
|
|
2388
|
+
const rthEnd = parseMinutes(inputs.rthEnd, 960);
|
|
2389
|
+
const orMinutes = Math.max(1, Math.floor(Number(inputs.openingRangeMinutes) || 30));
|
|
2390
|
+
const levels = withCachedComputation(
|
|
2391
|
+
`session-levels|${rthStart}|${rthEnd}|${orMinutes}|${data.length}`,
|
|
2392
|
+
data,
|
|
2393
|
+
() => computeSessionLevels(data, session, rthStart, rthEnd, orMinutes)
|
|
2394
|
+
);
|
|
2395
|
+
const groupEnabled = {
|
|
2396
|
+
prior: inputs.showPriorDay !== false,
|
|
2397
|
+
overnight: inputs.showOvernight !== false,
|
|
2398
|
+
openingRange: inputs.showOpeningRange !== false,
|
|
2399
|
+
initialBalance: inputs.showInitialBalance === true
|
|
2400
|
+
};
|
|
2401
|
+
const groupColor = {
|
|
2402
|
+
prior: inputs.priorDayColor ?? "#8b93a7",
|
|
2403
|
+
overnight: inputs.overnightColor ?? "#7c5cff",
|
|
2404
|
+
openingRange: inputs.openingRangeColor ?? "#26a69a",
|
|
2405
|
+
initialBalance: inputs.initialBalanceColor ?? "#f7a600"
|
|
2406
|
+
};
|
|
2407
|
+
ctx.save();
|
|
2408
|
+
ctx.lineWidth = Math.max(1, Number(inputs.lineWidth) || 1);
|
|
2409
|
+
ctx.font = "10px system-ui, -apple-system, Segoe UI, Roboto, sans-serif";
|
|
2410
|
+
ctx.textAlign = "right";
|
|
2411
|
+
ctx.textBaseline = "bottom";
|
|
2412
|
+
for (const level of levels) {
|
|
2413
|
+
if (!groupEnabled[level.color]) continue;
|
|
2414
|
+
const y = yFromPrice(level.price);
|
|
2415
|
+
if (!Number.isFinite(y) || y < chartTop || y > chartBottom) continue;
|
|
2416
|
+
const startX = Math.max(chartLeft, renderContext.xFromIndex(level.fromIndex));
|
|
2417
|
+
const color = groupColor[level.color] ?? "#8b93a7";
|
|
2418
|
+
ctx.strokeStyle = color;
|
|
2419
|
+
ctx.setLineDash(level.color === "prior" ? [] : [5, 4]);
|
|
2420
|
+
ctx.beginPath();
|
|
2421
|
+
ctx.moveTo(startX, Math.round(y) + 0.5);
|
|
2422
|
+
ctx.lineTo(chartRight, Math.round(y) + 0.5);
|
|
2423
|
+
ctx.stroke();
|
|
2424
|
+
if (inputs.showLabels !== false) {
|
|
2425
|
+
ctx.fillStyle = color;
|
|
2426
|
+
ctx.fillText(level.label, chartRight - 4, y - 2);
|
|
2427
|
+
}
|
|
2428
|
+
}
|
|
2429
|
+
ctx.restore();
|
|
2430
|
+
}
|
|
2431
|
+
};
|
|
2432
|
+
var BUILTIN_ANCHORED_VWAP_INDICATOR = {
|
|
2433
|
+
id: "anchored-vwap",
|
|
2434
|
+
name: "Anchored VWAP",
|
|
2435
|
+
pane: "overlay",
|
|
2436
|
+
defaultInputs: {
|
|
2437
|
+
anchorTime: "",
|
|
2438
|
+
color: "#22d3ee",
|
|
2439
|
+
width: 2,
|
|
2440
|
+
showBands: false,
|
|
2441
|
+
bandMultiplier: 1,
|
|
2442
|
+
bandColor: "#94a3b8",
|
|
2443
|
+
bandFillOpacity: 0.06,
|
|
2444
|
+
showAnchorMarker: true
|
|
2445
|
+
},
|
|
2446
|
+
draw: (ctx, renderContext, inputs) => {
|
|
2447
|
+
const { data, yFromPrice } = renderContext;
|
|
2448
|
+
if (!yFromPrice || data.length === 0) return;
|
|
2449
|
+
const anchorIndex = resolveAnchorIndex(renderContext, inputs.anchorTime);
|
|
2450
|
+
if (anchorIndex === null) return;
|
|
2451
|
+
const series = withCachedComputation(
|
|
2452
|
+
`anchored-vwap|${anchorIndex}|${inputs.showBands ? "b" : "n"}`,
|
|
2453
|
+
data,
|
|
2454
|
+
() => computeAnchoredVwap(data, anchorIndex)
|
|
2455
|
+
);
|
|
2456
|
+
if (inputs.showBands) {
|
|
2457
|
+
const multiplier = Math.max(0.1, Number(inputs.bandMultiplier) || 1);
|
|
2458
|
+
const upper = series.vwap.map((value, idx) => {
|
|
2459
|
+
const sigma = series.sigma[idx];
|
|
2460
|
+
return value == null || sigma == null ? null : value + multiplier * sigma;
|
|
2461
|
+
});
|
|
2462
|
+
const lower = series.vwap.map((value, idx) => {
|
|
2463
|
+
const sigma = series.sigma[idx];
|
|
2464
|
+
return value == null || sigma == null ? null : value - multiplier * sigma;
|
|
2465
|
+
});
|
|
2466
|
+
const bandColor = inputs.bandColor ?? "#94a3b8";
|
|
2467
|
+
fillBetweenSeries(ctx, renderContext, upper, lower, bandColor, Number(inputs.bandFillOpacity) || 0.06);
|
|
2468
|
+
drawOverlaySeries(ctx, renderContext, upper, bandColor, 1);
|
|
2469
|
+
drawOverlaySeries(ctx, renderContext, lower, bandColor, 1);
|
|
2470
|
+
}
|
|
2471
|
+
drawOverlaySeries(ctx, renderContext, series.vwap, inputs.color ?? "#22d3ee", Number(inputs.width) || 2);
|
|
2472
|
+
if (inputs.showAnchorMarker !== false) {
|
|
2473
|
+
const anchorBar = data[anchorIndex];
|
|
2474
|
+
const anchorValue = series.vwap[anchorIndex];
|
|
2475
|
+
if (anchorBar && anchorValue != null) {
|
|
2476
|
+
const x = renderContext.xFromIndex(anchorIndex);
|
|
2477
|
+
const y = yFromPrice(anchorValue);
|
|
2478
|
+
if (x >= renderContext.chartLeft - 8 && x <= renderContext.chartRight + 8) {
|
|
2479
|
+
ctx.save();
|
|
2480
|
+
ctx.fillStyle = inputs.color ?? "#22d3ee";
|
|
2481
|
+
ctx.beginPath();
|
|
2482
|
+
ctx.arc(x, y, 3.5, 0, Math.PI * 2);
|
|
2483
|
+
ctx.fill();
|
|
2484
|
+
ctx.restore();
|
|
2485
|
+
}
|
|
2486
|
+
}
|
|
2487
|
+
}
|
|
2488
|
+
},
|
|
2489
|
+
getAutoscaleRange: (data, startIndex, endIndex, inputs, skipIndex) => {
|
|
2490
|
+
const anchorIndex = resolveAnchorIndexFromData(data, inputs.anchorTime);
|
|
2491
|
+
if (anchorIndex === null) return null;
|
|
2492
|
+
const series = withCachedComputation(
|
|
2493
|
+
`anchored-vwap|${anchorIndex}|${inputs.showBands ? "b" : "n"}`,
|
|
2494
|
+
data,
|
|
2495
|
+
() => computeAnchoredVwap(data, anchorIndex)
|
|
2496
|
+
);
|
|
2497
|
+
if (!inputs.showBands) return rangeOfSeries([series.vwap], startIndex, endIndex, skipIndex);
|
|
2498
|
+
const multiplier = Math.max(0.1, Number(inputs.bandMultiplier) || 1);
|
|
2499
|
+
let min = Number.POSITIVE_INFINITY;
|
|
2500
|
+
let max = Number.NEGATIVE_INFINITY;
|
|
2501
|
+
for (let idx = startIndex; idx <= endIndex; idx += 1) {
|
|
2502
|
+
if (idx === skipIndex) continue;
|
|
2503
|
+
const value = series.vwap[idx];
|
|
2504
|
+
const sigma = series.sigma[idx];
|
|
2505
|
+
if (value == null || sigma == null) continue;
|
|
2506
|
+
const lo = value - multiplier * sigma;
|
|
2507
|
+
const hi = value + multiplier * sigma;
|
|
2508
|
+
if (lo < min) min = lo;
|
|
2509
|
+
if (hi > max) max = hi;
|
|
2510
|
+
}
|
|
2511
|
+
return min <= max ? { min, max } : null;
|
|
2512
|
+
}
|
|
2513
|
+
};
|
|
2514
|
+
var resolveAnchorIndexFromData = (data, anchorTime) => {
|
|
2515
|
+
if (data.length === 0) return null;
|
|
2516
|
+
const raw = typeof anchorTime === "number" ? anchorTime : Date.parse(String(anchorTime ?? ""));
|
|
2517
|
+
if (!Number.isFinite(raw)) return null;
|
|
2518
|
+
let lo = 0;
|
|
2519
|
+
let hi = data.length - 1;
|
|
2520
|
+
if (raw <= data[0].time.getTime()) return 0;
|
|
2521
|
+
if (raw >= data[hi].time.getTime()) return hi;
|
|
2522
|
+
while (lo < hi) {
|
|
2523
|
+
const mid = lo + hi + 1 >> 1;
|
|
2524
|
+
if (data[mid].time.getTime() <= raw) lo = mid;
|
|
2525
|
+
else hi = mid - 1;
|
|
2526
|
+
}
|
|
2527
|
+
return lo;
|
|
2528
|
+
};
|
|
2529
|
+
var resolveAnchorIndex = (renderContext, anchorTime) => {
|
|
2530
|
+
const explicit = resolveAnchorIndexFromData(renderContext.data, anchorTime);
|
|
2531
|
+
if (explicit !== null) return explicit;
|
|
2532
|
+
const session = renderContext.session;
|
|
2533
|
+
const lastIndex = renderContext.data.length - 1;
|
|
2534
|
+
if (!session || lastIndex < 0) return lastIndex >= 0 ? 0 : null;
|
|
2535
|
+
const day = session.dayAt(lastIndex);
|
|
2536
|
+
let index = lastIndex;
|
|
2537
|
+
while (index > 0 && session.dayAt(index - 1) === day) index -= 1;
|
|
2538
|
+
return index;
|
|
2539
|
+
};
|
|
2540
|
+
var computeAnchoredVwap = (data, anchorIndex) => {
|
|
2541
|
+
const vwap = new Array(data.length).fill(null);
|
|
2542
|
+
const sigma = new Array(data.length).fill(null);
|
|
2543
|
+
let cumPv = 0;
|
|
2544
|
+
let cumPv2 = 0;
|
|
2545
|
+
let cumV = 0;
|
|
2546
|
+
for (let i = Math.max(0, anchorIndex); i < data.length; i += 1) {
|
|
2547
|
+
const bar = data[i];
|
|
2548
|
+
if (!bar) continue;
|
|
2549
|
+
const typical = (bar.h + bar.l + bar.c) / 3;
|
|
2550
|
+
const volume = Math.max(0, bar.v ?? 0) || 1;
|
|
2551
|
+
cumPv += typical * volume;
|
|
2552
|
+
cumPv2 += typical * typical * volume;
|
|
2553
|
+
cumV += volume;
|
|
2554
|
+
if (cumV <= 0) continue;
|
|
2555
|
+
const value = cumPv / cumV;
|
|
2556
|
+
vwap[i] = value;
|
|
2557
|
+
sigma[i] = Math.sqrt(Math.max(0, cumPv2 / cumV - value * value));
|
|
2558
|
+
}
|
|
2559
|
+
return { vwap, sigma };
|
|
2560
|
+
};
|
|
2074
2561
|
var BUILTIN_INDICATORS = [
|
|
2075
2562
|
BUILTIN_VOLUME_INDICATOR,
|
|
2076
2563
|
BUILTIN_SMA_INDICATOR,
|
|
@@ -2098,7 +2585,10 @@ var BUILTIN_INDICATORS = [
|
|
|
2098
2585
|
BUILTIN_SUPERTREND_INDICATOR,
|
|
2099
2586
|
BUILTIN_ICHIMOKU_INDICATOR,
|
|
2100
2587
|
BUILTIN_KELTNER_INDICATOR,
|
|
2101
|
-
BUILTIN_DONCHIAN_INDICATOR
|
|
2588
|
+
BUILTIN_DONCHIAN_INDICATOR,
|
|
2589
|
+
BUILTIN_VOLUME_PROFILE_INDICATOR,
|
|
2590
|
+
BUILTIN_SESSION_LEVELS_INDICATOR,
|
|
2591
|
+
BUILTIN_ANCHORED_VWAP_INDICATOR
|
|
2102
2592
|
];
|
|
2103
2593
|
|
|
2104
2594
|
// src/scripts.ts
|
|
@@ -3494,6 +3984,44 @@ function createChart(element, options = {}) {
|
|
|
3494
3984
|
let compiledSession = compileSession(sessionOptions.spec);
|
|
3495
3985
|
const isHour12 = () => timeFormat === "12h";
|
|
3496
3986
|
const formatClock = (time) => zoneClock.formatTime(time.getTime(), isHour12());
|
|
3987
|
+
let sessionInfoCache = [];
|
|
3988
|
+
let sessionInfoCacheKey = "";
|
|
3989
|
+
const sessionInfoForIndex = (index) => {
|
|
3990
|
+
if (!compiledSession) return null;
|
|
3991
|
+
const point = data[index];
|
|
3992
|
+
if (!point) return null;
|
|
3993
|
+
const key = `${data.length}|${data[0]?.time.getTime() ?? 0}|${data[data.length - 1]?.time.getTime() ?? 0}|${sessionOptions.spec === null ? "none" : JSON.stringify(sessionOptions.spec)}`;
|
|
3994
|
+
if (key !== sessionInfoCacheKey) {
|
|
3995
|
+
sessionInfoCacheKey = key;
|
|
3996
|
+
sessionInfoCache = new Array(data.length);
|
|
3997
|
+
}
|
|
3998
|
+
const cached = sessionInfoCache[index];
|
|
3999
|
+
if (cached) return cached;
|
|
4000
|
+
const ms = point.time.getTime();
|
|
4001
|
+
const info = sessionInfoAt(compiledSession, ms);
|
|
4002
|
+
const resolved = {
|
|
4003
|
+
day: info.sessionDay,
|
|
4004
|
+
inSession: info.inSession,
|
|
4005
|
+
minutes: compiledSession.clock.minutesOfDay(ms)
|
|
4006
|
+
};
|
|
4007
|
+
sessionInfoCache[index] = resolved;
|
|
4008
|
+
return resolved;
|
|
4009
|
+
};
|
|
4010
|
+
const buildIndicatorSessionContext = () => {
|
|
4011
|
+
if (!compiledSession) return void 0;
|
|
4012
|
+
const dayAt = (index) => sessionInfoForIndex(index)?.day ?? Number.NaN;
|
|
4013
|
+
return {
|
|
4014
|
+
dayAt,
|
|
4015
|
+
inSessionAt: (index) => sessionInfoForIndex(index)?.inSession ?? false,
|
|
4016
|
+
isSessionStart: (index) => {
|
|
4017
|
+
if (index <= 0) return index === 0;
|
|
4018
|
+
const current = dayAt(index);
|
|
4019
|
+
const previous = dayAt(index - 1);
|
|
4020
|
+
return Number.isFinite(current) && Number.isFinite(previous) && current !== previous;
|
|
4021
|
+
},
|
|
4022
|
+
minutesOfDayAt: (index) => sessionInfoForIndex(index)?.minutes ?? Number.NaN
|
|
4023
|
+
};
|
|
4024
|
+
};
|
|
3497
4025
|
let barMarks = [];
|
|
3498
4026
|
let timescaleMarks = [];
|
|
3499
4027
|
let markRegions = [];
|
|
@@ -6900,6 +7428,7 @@ function createChart(element, options = {}) {
|
|
|
6900
7428
|
).sort((a, b) => a.indicator.zIndex - b.indicator.zIndex);
|
|
6901
7429
|
if (activeOverlayIndicators.length > 0) {
|
|
6902
7430
|
const xFromIndex = (index) => chartLeft + (index + 0.5 - xStart) / xSpan * chartWidth;
|
|
7431
|
+
const indicatorSession = buildIndicatorSessionContext();
|
|
6903
7432
|
activeOverlayIndicators.forEach(({ indicator, plugin }) => {
|
|
6904
7433
|
plugin.draw(
|
|
6905
7434
|
ctx,
|
|
@@ -6921,7 +7450,8 @@ function createChart(element, options = {}) {
|
|
|
6921
7450
|
getVolumeByIndex,
|
|
6922
7451
|
candleSpacing,
|
|
6923
7452
|
upColor: mergedOptions.upColor,
|
|
6924
|
-
downColor: mergedOptions.downColor
|
|
7453
|
+
downColor: mergedOptions.downColor,
|
|
7454
|
+
...indicatorSession ? { session: indicatorSession } : {}
|
|
6925
7455
|
},
|
|
6926
7456
|
indicator.inputs
|
|
6927
7457
|
);
|
|
@@ -7721,8 +8251,17 @@ function createChart(element, options = {}) {
|
|
|
7721
8251
|
return typeof value === "string" && !/^#(?:[0-9a-fA-F]{3}){1,2}$/.test(value.trim());
|
|
7722
8252
|
};
|
|
7723
8253
|
const LEGEND_EXCLUDED_INPUT_KEYS = /* @__PURE__ */ new Set(["width", "bandMultiplier", "bandFillOpacity", "fillOpacity"]);
|
|
8254
|
+
const formatLegendValue = (value) => {
|
|
8255
|
+
if (typeof value === "string" && /^\d{4}-\d{2}-\d{2}T/.test(value)) {
|
|
8256
|
+
const ms = Date.parse(value);
|
|
8257
|
+
if (Number.isFinite(ms)) {
|
|
8258
|
+
return getTimeStepMs() < 864e5 ? `${zoneClock.formatDayMonth(ms)} ${zoneClock.formatTime(ms, isHour12())}` : zoneClock.formatDayMonth(ms);
|
|
8259
|
+
}
|
|
8260
|
+
}
|
|
8261
|
+
return String(value);
|
|
8262
|
+
};
|
|
7724
8263
|
const labelEntries = activeOverlayIndicators.map(({ indicator, plugin }) => {
|
|
7725
|
-
const inputValues = Object.entries(indicator.inputs).filter(([key, value]) => !LEGEND_EXCLUDED_INPUT_KEYS.has(key) && typeof value !== "boolean" && isLegendInputValue(value)).slice(0, 2).map(([, value]) =>
|
|
8264
|
+
const inputValues = Object.entries(indicator.inputs).filter(([key, value]) => !LEGEND_EXCLUDED_INPUT_KEYS.has(key) && typeof value !== "boolean" && isLegendInputValue(value)).slice(0, 2).map(([, value]) => formatLegendValue(value));
|
|
7726
8265
|
if (labels.showIndicatorNames && labels.showIndicatorValues && inputValues.length > 0) {
|
|
7727
8266
|
return `${plugin.name} ${inputValues.join(" ")}`;
|
|
7728
8267
|
}
|
|
@@ -10548,6 +11087,22 @@ function createChart(element, options = {}) {
|
|
|
10548
11087
|
if (event.altKey || event.ctrlKey || event.metaKey) {
|
|
10549
11088
|
return;
|
|
10550
11089
|
}
|
|
11090
|
+
if (replay && keyboard.replay !== false) {
|
|
11091
|
+
if (event.key === "ArrowRight" || event.key === "ArrowLeft") {
|
|
11092
|
+
const direction = event.key === "ArrowRight" ? 1 : -1;
|
|
11093
|
+
replayStep(direction * (event.shiftKey ? 10 : 1));
|
|
11094
|
+
emit(direction > 0 ? "replay-step-forward" : "replay-step-back");
|
|
11095
|
+
event.preventDefault();
|
|
11096
|
+
return;
|
|
11097
|
+
}
|
|
11098
|
+
if (event.key === " " || event.key === "Spacebar") {
|
|
11099
|
+
if (replay.playing) replayPause();
|
|
11100
|
+
else replayPlay();
|
|
11101
|
+
emit("replay-toggle-play");
|
|
11102
|
+
event.preventDefault();
|
|
11103
|
+
return;
|
|
11104
|
+
}
|
|
11105
|
+
}
|
|
10551
11106
|
switch (event.key) {
|
|
10552
11107
|
case "Delete":
|
|
10553
11108
|
case "Backspace": {
|
|
@@ -10959,7 +11514,11 @@ function createChart(element, options = {}) {
|
|
|
10959
11514
|
const replayStep = (bars = 1) => {
|
|
10960
11515
|
if (!replay) return;
|
|
10961
11516
|
const step = Math.trunc(bars) || 1;
|
|
10962
|
-
|
|
11517
|
+
replaySeek(replay.cursor + step);
|
|
11518
|
+
};
|
|
11519
|
+
const replaySeek = (index) => {
|
|
11520
|
+
if (!replay || !Number.isFinite(index)) return;
|
|
11521
|
+
const next = clamp(Math.round(index), 0, replay.fullRaw.length - 1);
|
|
10963
11522
|
if (next === replay.cursor) return;
|
|
10964
11523
|
replay.cursor = next;
|
|
10965
11524
|
applyReplaySlice();
|
|
@@ -11592,6 +12151,7 @@ function createChart(element, options = {}) {
|
|
|
11592
12151
|
startReplay,
|
|
11593
12152
|
stopReplay,
|
|
11594
12153
|
replayStep,
|
|
12154
|
+
replaySeek,
|
|
11595
12155
|
replayPlay,
|
|
11596
12156
|
replayPause,
|
|
11597
12157
|
setReplaySpeed,
|
|
@@ -264,6 +264,22 @@ interface IndicatorRenderContext {
|
|
|
264
264
|
opacity: number;
|
|
265
265
|
horizontalLines: boolean;
|
|
266
266
|
};
|
|
267
|
+
/**
|
|
268
|
+
* The chart's trading-session view, so indicators can be session-aware
|
|
269
|
+
* without re-deriving one. Present only when a session spec is configured
|
|
270
|
+
* (`setSession`); session-based indicators should fall back to calendar
|
|
271
|
+
* days — or render nothing — when it is absent.
|
|
272
|
+
*/
|
|
273
|
+
session?: {
|
|
274
|
+
/** Trading day a bar belongs to; equal for bars in the same session. */
|
|
275
|
+
dayAt: (index: number) => number;
|
|
276
|
+
/** False for bars outside trading hours (overnight, maintenance break). */
|
|
277
|
+
inSessionAt: (index: number) => boolean;
|
|
278
|
+
/** True when the bar opens a new trading day. */
|
|
279
|
+
isSessionStart: (index: number) => boolean;
|
|
280
|
+
/** Minutes past midnight in the session's own zone. */
|
|
281
|
+
minutesOfDayAt: (index: number) => number;
|
|
282
|
+
};
|
|
267
283
|
}
|
|
268
284
|
/** Emitted when a pane-legend control (hover buttons) is clicked. */
|
|
269
285
|
interface IndicatorPaneActionEvent {
|
|
@@ -561,7 +577,7 @@ interface ChartContextMenuEvent {
|
|
|
561
577
|
};
|
|
562
578
|
}
|
|
563
579
|
/** Built-in keyboard actions, reported after the chart applies them. */
|
|
564
|
-
type ChartKeyboardAction = "delete-drawing" | "cancel" | "undo" | "redo" | "pan-left" | "pan-right" | "pan-up" | "pan-down" | "zoom-in" | "zoom-out" | "reset-viewport" | "scroll-to-realtime";
|
|
580
|
+
type ChartKeyboardAction = "delete-drawing" | "cancel" | "undo" | "redo" | "pan-left" | "pan-right" | "pan-up" | "pan-down" | "zoom-in" | "zoom-out" | "reset-viewport" | "scroll-to-realtime" | "replay-step-forward" | "replay-step-back" | "replay-toggle-play";
|
|
565
581
|
interface ChartKeyboardShortcutEvent {
|
|
566
582
|
action: ChartKeyboardAction;
|
|
567
583
|
/** The originating `KeyboardEvent.key`. */
|
|
@@ -583,6 +599,12 @@ interface KeyboardOptions {
|
|
|
583
599
|
deleteSelectedDrawing?: boolean;
|
|
584
600
|
/** Ctrl/Cmd+Z and Ctrl/Cmd+Shift+Z (or Ctrl+Y). Default true. */
|
|
585
601
|
undoRedo?: boolean;
|
|
602
|
+
/**
|
|
603
|
+
* While replay is running, arrows step the tape (Shift steps 10) and Space
|
|
604
|
+
* toggles playback, instead of panning. Only applies during replay.
|
|
605
|
+
* Default true.
|
|
606
|
+
*/
|
|
607
|
+
replay?: boolean;
|
|
586
608
|
}
|
|
587
609
|
/**
|
|
588
610
|
* Deep zoom-out rendering. Once bars are narrower than `thresholdPx`, several
|
|
@@ -1035,6 +1057,11 @@ interface ChartInstance {
|
|
|
1035
1057
|
stopReplay: () => void;
|
|
1036
1058
|
/** Reveal `bars` more bars (negative rewinds). Default 1. */
|
|
1037
1059
|
replayStep: (bars?: number) => void;
|
|
1060
|
+
/**
|
|
1061
|
+
* Jump straight to a bar index, for a scrubber or a "replay from here"
|
|
1062
|
+
* action. Clamped to the series; playback continues from where it lands.
|
|
1063
|
+
*/
|
|
1064
|
+
replaySeek: (index: number) => void;
|
|
1038
1065
|
replayPlay: (speed?: number) => void;
|
|
1039
1066
|
replayPause: () => void;
|
|
1040
1067
|
/** Bars revealed per second. */
|