hyperprop-charting-library 0.1.130 → 0.1.132
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 +225 -6
- package/dist/hyperprop-charting-library.d.ts +57 -1
- package/dist/hyperprop-charting-library.js +224 -6
- package/dist/index.cjs +225 -6
- package/dist/index.d.cts +57 -1
- package/dist/index.d.ts +57 -1
- package/dist/index.js +224 -6
- package/docs/API.md +64 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1468,6 +1468,195 @@ var drawSeparateMultiSeries = (ctx, renderContext, seriesList, options = {}) =>
|
|
|
1468
1468
|
}
|
|
1469
1469
|
return paneInfo;
|
|
1470
1470
|
};
|
|
1471
|
+
var scriptRollingExtreme = (values, length, mode) => {
|
|
1472
|
+
const fallback = mode === "max" ? Number.NEGATIVE_INFINITY : Number.POSITIVE_INFINITY;
|
|
1473
|
+
const filled = new Array(values.length);
|
|
1474
|
+
for (let i = 0; i < values.length; i += 1) {
|
|
1475
|
+
const value = values[i];
|
|
1476
|
+
filled[i] = value != null && Number.isFinite(value) ? value : fallback;
|
|
1477
|
+
}
|
|
1478
|
+
const raw = rollingExtremeSeries(filled, Math.max(1, Math.floor(length)), mode);
|
|
1479
|
+
return raw.map((value) => value != null && Number.isFinite(value) ? value : null);
|
|
1480
|
+
};
|
|
1481
|
+
var scriptStdevSeries = (values, length) => {
|
|
1482
|
+
const window2 = Math.max(1, Math.floor(length));
|
|
1483
|
+
const result = new Array(values.length).fill(null);
|
|
1484
|
+
let sum = 0;
|
|
1485
|
+
let sumSq = 0;
|
|
1486
|
+
let valid = 0;
|
|
1487
|
+
for (let i = 0; i < values.length; i += 1) {
|
|
1488
|
+
const value = values[i];
|
|
1489
|
+
if (value == null || !Number.isFinite(value)) {
|
|
1490
|
+
sum = 0;
|
|
1491
|
+
sumSq = 0;
|
|
1492
|
+
valid = 0;
|
|
1493
|
+
continue;
|
|
1494
|
+
}
|
|
1495
|
+
sum += value;
|
|
1496
|
+
sumSq += value * value;
|
|
1497
|
+
valid += 1;
|
|
1498
|
+
if (valid > window2) {
|
|
1499
|
+
const old = values[i - window2];
|
|
1500
|
+
sum -= old;
|
|
1501
|
+
sumSq -= old * old;
|
|
1502
|
+
valid -= 1;
|
|
1503
|
+
}
|
|
1504
|
+
if (valid === window2) {
|
|
1505
|
+
const mean = sum / window2;
|
|
1506
|
+
result[i] = Math.sqrt(Math.max(0, sumSq / window2 - mean * mean));
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
return result;
|
|
1510
|
+
};
|
|
1511
|
+
var scriptAtrSeries = (bars, length) => {
|
|
1512
|
+
const tr = new Array(bars.length).fill(null);
|
|
1513
|
+
for (let i = 0; i < bars.length; i += 1) {
|
|
1514
|
+
const bar = bars[i];
|
|
1515
|
+
if (i === 0) {
|
|
1516
|
+
tr[i] = bar.h - bar.l;
|
|
1517
|
+
continue;
|
|
1518
|
+
}
|
|
1519
|
+
const prevClose = bars[i - 1].c;
|
|
1520
|
+
tr[i] = Math.max(bar.h - bar.l, Math.abs(bar.h - prevClose), Math.abs(bar.l - prevClose));
|
|
1521
|
+
}
|
|
1522
|
+
return rmaFromValues(tr, Math.max(1, Math.floor(length)));
|
|
1523
|
+
};
|
|
1524
|
+
var SCRIPT_HELPERS = Object.freeze({
|
|
1525
|
+
sma: (values, length) => smaFromValues(values, Math.max(1, Math.floor(length))),
|
|
1526
|
+
ema: (values, length) => emaFromValues(values, Math.max(1, Math.floor(length))),
|
|
1527
|
+
rma: (values, length) => rmaFromValues(values, Math.max(1, Math.floor(length))),
|
|
1528
|
+
highest: (values, length) => scriptRollingExtreme(values, length, "max"),
|
|
1529
|
+
lowest: (values, length) => scriptRollingExtreme(values, length, "min"),
|
|
1530
|
+
change: (values, length = 1) => values.map((value, index) => {
|
|
1531
|
+
const prev = values[index - Math.max(1, Math.floor(length))];
|
|
1532
|
+
return value != null && prev != null ? value - prev : null;
|
|
1533
|
+
}),
|
|
1534
|
+
stdev: scriptStdevSeries,
|
|
1535
|
+
atr: scriptAtrSeries
|
|
1536
|
+
});
|
|
1537
|
+
var SCRIPT_PLOT_PALETTE = ["#2962ff", "#ff6d00", "#26a69a", "#ab47bc", "#ef5350", "#fdd835"];
|
|
1538
|
+
var hashScriptSource = (source) => {
|
|
1539
|
+
let hash = 5381;
|
|
1540
|
+
for (let i = 0; i < source.length; i += 1) {
|
|
1541
|
+
hash = (hash << 5) + hash + source.charCodeAt(i) | 0;
|
|
1542
|
+
}
|
|
1543
|
+
return (hash >>> 0).toString(36);
|
|
1544
|
+
};
|
|
1545
|
+
var compileScriptCompute = (source) => {
|
|
1546
|
+
const factory = new Function(
|
|
1547
|
+
`"use strict";
|
|
1548
|
+
${source}
|
|
1549
|
+
;if (typeof compute !== "function") { throw new Error("Script must define function compute(bars, inputs, hp)"); }
|
|
1550
|
+
return compute;`
|
|
1551
|
+
);
|
|
1552
|
+
return factory();
|
|
1553
|
+
};
|
|
1554
|
+
var drawScriptError = (ctx, renderContext, name, message) => {
|
|
1555
|
+
ctx.save();
|
|
1556
|
+
ctx.font = "11px -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif";
|
|
1557
|
+
ctx.textAlign = "left";
|
|
1558
|
+
ctx.textBaseline = "top";
|
|
1559
|
+
ctx.fillStyle = "#ef5350";
|
|
1560
|
+
const text = `${name}: ${message}`.slice(0, 160);
|
|
1561
|
+
ctx.fillText(text, renderContext.chartLeft + 10, renderContext.chartTop + 24);
|
|
1562
|
+
ctx.restore();
|
|
1563
|
+
};
|
|
1564
|
+
var compileScriptIndicator = (definition) => {
|
|
1565
|
+
const pane = definition.pane ?? "separate";
|
|
1566
|
+
const defaultInputs = { showValueLine: true };
|
|
1567
|
+
for (const input of definition.inputs ?? []) {
|
|
1568
|
+
defaultInputs[input.key] = input.default;
|
|
1569
|
+
}
|
|
1570
|
+
let compiled = null;
|
|
1571
|
+
let compileError = null;
|
|
1572
|
+
try {
|
|
1573
|
+
compiled = compileScriptCompute(definition.source);
|
|
1574
|
+
} catch (error) {
|
|
1575
|
+
compileError = error instanceof Error ? error.message : String(error);
|
|
1576
|
+
}
|
|
1577
|
+
const sourceKey = hashScriptSource(definition.source);
|
|
1578
|
+
const runCompute = (data, inputs) => {
|
|
1579
|
+
if (!compiled) {
|
|
1580
|
+
return { error: compileError ?? "Script failed to compile" };
|
|
1581
|
+
}
|
|
1582
|
+
try {
|
|
1583
|
+
const result = compiled(data, inputs, SCRIPT_HELPERS);
|
|
1584
|
+
if (!result || !Array.isArray(result.plots)) {
|
|
1585
|
+
return { error: "compute() must return { plots: [...] }" };
|
|
1586
|
+
}
|
|
1587
|
+
for (const plot of result.plots) {
|
|
1588
|
+
if (!plot || !Array.isArray(plot.values)) {
|
|
1589
|
+
return { error: "Every plot needs a `values` array" };
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
return { result };
|
|
1593
|
+
} catch (error) {
|
|
1594
|
+
return { error: error instanceof Error ? error.message : String(error) };
|
|
1595
|
+
}
|
|
1596
|
+
};
|
|
1597
|
+
const cachedCompute = (data, inputs) => withCachedComputation(
|
|
1598
|
+
`script|${definition.id}|${sourceKey}|${JSON.stringify(inputs)}`,
|
|
1599
|
+
data,
|
|
1600
|
+
() => runCompute(data, inputs)
|
|
1601
|
+
);
|
|
1602
|
+
const plugin = {
|
|
1603
|
+
id: definition.id,
|
|
1604
|
+
name: definition.name,
|
|
1605
|
+
pane,
|
|
1606
|
+
defaultInputs,
|
|
1607
|
+
draw: (ctx, renderContext, inputs) => {
|
|
1608
|
+
const outcome = cachedCompute(renderContext.data, inputs);
|
|
1609
|
+
if (!outcome.result) {
|
|
1610
|
+
drawScriptError(ctx, renderContext, definition.name, outcome.error ?? "Unknown script error");
|
|
1611
|
+
return pane === "separate" ? { title: `${definition.name} (script error)` } : void 0;
|
|
1612
|
+
}
|
|
1613
|
+
const { plots, range, guides, decimals } = outcome.result;
|
|
1614
|
+
if (pane === "separate") {
|
|
1615
|
+
const seriesList = plots.map((plot, index) => ({
|
|
1616
|
+
values: plot.values,
|
|
1617
|
+
color: plot.color ?? SCRIPT_PLOT_PALETTE[index % SCRIPT_PLOT_PALETTE.length],
|
|
1618
|
+
...plot.width !== void 0 ? { width: plot.width } : {},
|
|
1619
|
+
...plot.title ? { label: plot.title } : {},
|
|
1620
|
+
...plot.style === "histogram" ? { histogram: true } : {},
|
|
1621
|
+
...plot.negativeColor ? { negativeColor: plot.negativeColor } : {}
|
|
1622
|
+
}));
|
|
1623
|
+
return drawSeparateMultiSeries(ctx, renderContext, seriesList, {
|
|
1624
|
+
title: definition.name,
|
|
1625
|
+
...range?.min !== void 0 ? { minOverride: range.min } : {},
|
|
1626
|
+
...range?.max !== void 0 ? { maxOverride: range.max } : {},
|
|
1627
|
+
...guides && guides.length > 0 ? { guideLines: guides } : {},
|
|
1628
|
+
...decimals !== void 0 ? { decimals } : {},
|
|
1629
|
+
valueLines: inputs.showValueLine !== false
|
|
1630
|
+
});
|
|
1631
|
+
}
|
|
1632
|
+
for (let index = 0; index < plots.length; index += 1) {
|
|
1633
|
+
const plot = plots[index];
|
|
1634
|
+
if (plot.style === "histogram") continue;
|
|
1635
|
+
drawOverlaySeries(
|
|
1636
|
+
ctx,
|
|
1637
|
+
renderContext,
|
|
1638
|
+
plot.values,
|
|
1639
|
+
plot.color ?? SCRIPT_PLOT_PALETTE[index % SCRIPT_PLOT_PALETTE.length],
|
|
1640
|
+
plot.width ?? 2
|
|
1641
|
+
);
|
|
1642
|
+
}
|
|
1643
|
+
return void 0;
|
|
1644
|
+
},
|
|
1645
|
+
...pane === "overlay" ? {
|
|
1646
|
+
getAutoscaleRange: (data, startIndex, endIndex, inputs, skipIndex) => {
|
|
1647
|
+
const outcome = cachedCompute(data, inputs);
|
|
1648
|
+
if (!outcome.result) return null;
|
|
1649
|
+
const lineSeries = outcome.result.plots.filter((plot) => plot.style !== "histogram").map((plot) => plot.values);
|
|
1650
|
+
if (lineSeries.length === 0) return null;
|
|
1651
|
+
return rangeOfSeries(lineSeries, startIndex, endIndex, skipIndex);
|
|
1652
|
+
}
|
|
1653
|
+
} : {}
|
|
1654
|
+
};
|
|
1655
|
+
if (definition.paneHeightRatio !== void 0) {
|
|
1656
|
+
plugin.paneHeightRatio = definition.paneHeightRatio;
|
|
1657
|
+
}
|
|
1658
|
+
return plugin;
|
|
1659
|
+
};
|
|
1471
1660
|
var computeMacd = (data, fast, slow, signalLength) => {
|
|
1472
1661
|
const fastEma = computeEmaSeries(data, fast, "close");
|
|
1473
1662
|
const slowEma = computeEmaSeries(data, slow, "close");
|
|
@@ -2519,7 +2708,7 @@ function createChart(element, options = {}) {
|
|
|
2519
2708
|
return;
|
|
2520
2709
|
}
|
|
2521
2710
|
const tickerOpts = mergedOptions.tickerLine ?? DEFAULT_OPTIONS.tickerLine;
|
|
2522
|
-
const speed = clamp(tickerOpts.smoothingSpeed ??
|
|
2711
|
+
const speed = clamp(tickerOpts.smoothingSpeed ?? 16, 1, 60);
|
|
2523
2712
|
const dt = 1 / 60;
|
|
2524
2713
|
const lerp = 1 - Math.exp(-speed * dt);
|
|
2525
2714
|
smoothedTickerPrice += priceDiff * lerp;
|
|
@@ -2549,6 +2738,29 @@ function createChart(element, options = {}) {
|
|
|
2549
2738
|
smoothingRafId = requestAnimationFrame(tickerSmoothingLoop);
|
|
2550
2739
|
}
|
|
2551
2740
|
};
|
|
2741
|
+
let countdownTimerId = null;
|
|
2742
|
+
const countdownRepaintNeeded = () => {
|
|
2743
|
+
if (data.length === 0) {
|
|
2744
|
+
return false;
|
|
2745
|
+
}
|
|
2746
|
+
const ticker = mergedOptions.tickerLine ?? DEFAULT_OPTIONS.tickerLine;
|
|
2747
|
+
const labels = mergedOptions.labels ?? DEFAULT_OPTIONS.labels;
|
|
2748
|
+
return Boolean(ticker.showCountdownInLabel) || Boolean(labels.visible && labels.showCountdownToBarClose);
|
|
2749
|
+
};
|
|
2750
|
+
const armCountdownHeartbeat = () => {
|
|
2751
|
+
if (countdownTimerId !== null || !countdownRepaintNeeded()) {
|
|
2752
|
+
return;
|
|
2753
|
+
}
|
|
2754
|
+
const delay = 1e3 - Date.now() % 1e3 + 15;
|
|
2755
|
+
countdownTimerId = setTimeout(() => {
|
|
2756
|
+
countdownTimerId = null;
|
|
2757
|
+
if (!countdownRepaintNeeded()) {
|
|
2758
|
+
return;
|
|
2759
|
+
}
|
|
2760
|
+
scheduleDraw({ updateAutoScale: false });
|
|
2761
|
+
armCountdownHeartbeat();
|
|
2762
|
+
}, delay);
|
|
2763
|
+
};
|
|
2552
2764
|
const canvas = document.createElement("canvas");
|
|
2553
2765
|
const ctx = canvas.getContext("2d");
|
|
2554
2766
|
if (!ctx) {
|
|
@@ -3474,6 +3686,7 @@ function createChart(element, options = {}) {
|
|
|
3474
3686
|
let pendingDrawUpdateAutoScale = false;
|
|
3475
3687
|
let pendingDrawOverlayOnly = true;
|
|
3476
3688
|
const scheduleDraw = (options2 = {}) => {
|
|
3689
|
+
armCountdownHeartbeat();
|
|
3477
3690
|
const overlayOnly = options2.overlayOnly ?? false;
|
|
3478
3691
|
pendingDrawOverlayOnly = pendingDrawOverlayOnly && overlayOnly;
|
|
3479
3692
|
pendingDrawUpdateAutoScale = pendingDrawUpdateAutoScale || !overlayOnly && (options2.updateAutoScale ?? true);
|
|
@@ -5058,18 +5271,17 @@ function createChart(element, options = {}) {
|
|
|
5058
5271
|
legendTitle,
|
|
5059
5272
|
...legendValues.map((value) => value.text ?? (value.value === void 0 ? "" : formatPaneValue(value.value))).filter(Boolean)
|
|
5060
5273
|
].filter(Boolean);
|
|
5061
|
-
const legendText = legendParts.join(" ");
|
|
5062
|
-
if (legendParts.length > 0) {
|
|
5063
|
-
drawText(legendText, chartLeft + 10, paneTop + 8, "left", "top", labels.indicatorTextColor);
|
|
5064
|
-
}
|
|
5065
5274
|
if (hoveredPaneId === indicator.id) {
|
|
5275
|
+
drawText(legendTitle, chartLeft + 10, paneTop + 8, "left", "top", labels.indicatorTextColor);
|
|
5066
5276
|
drawPaneButtons(
|
|
5067
5277
|
indicator.id,
|
|
5068
5278
|
indicator.type,
|
|
5069
5279
|
true,
|
|
5070
|
-
chartLeft + 10 + Math.ceil(measureTextWidth(
|
|
5280
|
+
chartLeft + 10 + Math.ceil(measureTextWidth(legendTitle)) + 10,
|
|
5071
5281
|
paneTop + 8 + legendFontSize / 2
|
|
5072
5282
|
);
|
|
5283
|
+
} else if (legendParts.length > 0) {
|
|
5284
|
+
drawText(legendParts.join(" "), chartLeft + 10, paneTop + 8, "left", "top", labels.indicatorTextColor);
|
|
5073
5285
|
}
|
|
5074
5286
|
for (const label of paneInfo?.valueLabels ?? []) {
|
|
5075
5287
|
if (!labels.showIndicatorValueLabels) {
|
|
@@ -7732,6 +7944,10 @@ function createChart(element, options = {}) {
|
|
|
7732
7944
|
cancelAnimationFrame(smoothingRafId);
|
|
7733
7945
|
smoothingRafId = null;
|
|
7734
7946
|
}
|
|
7947
|
+
if (countdownTimerId !== null) {
|
|
7948
|
+
clearTimeout(countdownTimerId);
|
|
7949
|
+
countdownTimerId = null;
|
|
7950
|
+
}
|
|
7735
7951
|
if (drawRafId !== null) {
|
|
7736
7952
|
cancelAnimationFrame(drawRafId);
|
|
7737
7953
|
drawRafId = null;
|
|
@@ -7753,6 +7969,7 @@ function createChart(element, options = {}) {
|
|
|
7753
7969
|
element.innerHTML = "";
|
|
7754
7970
|
};
|
|
7755
7971
|
draw();
|
|
7972
|
+
armCountdownHeartbeat();
|
|
7756
7973
|
return {
|
|
7757
7974
|
updateOptions,
|
|
7758
7975
|
setChartType,
|
|
@@ -7822,5 +8039,6 @@ function createChart(element, options = {}) {
|
|
|
7822
8039
|
export {
|
|
7823
8040
|
FIB_DEFAULT_PALETTE,
|
|
7824
8041
|
POSITION_DEFAULT_COLORS,
|
|
8042
|
+
compileScriptIndicator,
|
|
7825
8043
|
createChart
|
|
7826
8044
|
};
|
package/docs/API.md
CHANGED
|
@@ -543,3 +543,67 @@ Use `getDrawings()` / `setDrawings()` for persistence.
|
|
|
543
543
|
- `setIndicators(indicators: IndicatorInstanceOptions[]): void`
|
|
544
544
|
- `resize(width?: number, height?: number): void`
|
|
545
545
|
- `destroy(): void`
|
|
546
|
+
|
|
547
|
+
---
|
|
548
|
+
|
|
549
|
+
## Script Indicators (user-authored)
|
|
550
|
+
|
|
551
|
+
`compileScriptIndicator(definition: ScriptIndicatorDefinition): IndicatorPlugin` — compiles a
|
|
552
|
+
user-authored "Hyperprop Script" into a regular indicator plugin. Register the result with
|
|
553
|
+
`chart.registerIndicator(...)` and add it with `chart.addIndicator(definition.id)`. Separate-pane
|
|
554
|
+
scripts automatically get the shared grid, legend, live values, value lines, drag-resize and the
|
|
555
|
+
hover controls (eye / gear / source / delete). Overlay scripts participate in price autoscale.
|
|
556
|
+
|
|
557
|
+
```ts
|
|
558
|
+
import { createChart, compileScriptIndicator } from "hyperprop-charting-library";
|
|
559
|
+
|
|
560
|
+
const plugin = compileScriptIndicator({
|
|
561
|
+
id: "script:my-rsi",
|
|
562
|
+
name: "My RSI",
|
|
563
|
+
pane: "separate", // or "overlay"
|
|
564
|
+
inputs: [
|
|
565
|
+
{ key: "length", label: "Length", type: "number", default: 14 },
|
|
566
|
+
{ key: "color", label: "Color", type: "color", default: "#ab47bc" }
|
|
567
|
+
],
|
|
568
|
+
source: `
|
|
569
|
+
function compute(bars, inputs, hp) {
|
|
570
|
+
const closes = bars.map(b => b.c);
|
|
571
|
+
const gains = closes.map((c, i) => i === 0 ? null : Math.max(0, c - closes[i - 1]));
|
|
572
|
+
const losses = closes.map((c, i) => i === 0 ? null : Math.max(0, closes[i - 1] - c));
|
|
573
|
+
const avgGain = hp.rma(gains, inputs.length);
|
|
574
|
+
const avgLoss = hp.rma(losses, inputs.length);
|
|
575
|
+
const rsi = avgGain.map((g, i) => {
|
|
576
|
+
const l = avgLoss[i];
|
|
577
|
+
if (g == null || l == null) return null;
|
|
578
|
+
return l === 0 ? 100 : 100 - 100 / (1 + g / l);
|
|
579
|
+
});
|
|
580
|
+
return {
|
|
581
|
+
plots: [{ title: "RSI", values: rsi, color: inputs.color }],
|
|
582
|
+
range: { min: 0, max: 100 },
|
|
583
|
+
guides: [30, 70]
|
|
584
|
+
};
|
|
585
|
+
}`
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
chart.registerIndicator(plugin);
|
|
589
|
+
chart.addIndicator("script:my-rsi");
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
Script contract:
|
|
593
|
+
|
|
594
|
+
- The source must define `function compute(bars, inputs, hp)`.
|
|
595
|
+
- `bars`: `Array<{ time: Date, o, h, l, c, v? }>` — the full series.
|
|
596
|
+
- `inputs`: resolved input values (defaults merged with per-instance overrides). `showValueLine`
|
|
597
|
+
is injected automatically and honored by separate panes.
|
|
598
|
+
- `hp`: TA helpers, all `(values: Array<number|null>, length) => Array<number|null>` unless noted:
|
|
599
|
+
`sma`, `ema`, `rma`, `highest`, `lowest`, `stdev`, `change(values, length = 1)`,
|
|
600
|
+
`atr(bars, length)`.
|
|
601
|
+
- Return `{ plots, range?, guides?, decimals? }`:
|
|
602
|
+
- `plots`: `Array<{ title?, values, color?, width?, style?: "line" | "histogram", negativeColor? }>`
|
|
603
|
+
- `range`: `{ min?, max? }` fixes the pane scale (e.g. `0..100` for oscillators).
|
|
604
|
+
- `guides`: dashed horizontal guide levels (e.g. `[30, 70]`).
|
|
605
|
+
- `decimals`: axis/legend decimal places.
|
|
606
|
+
|
|
607
|
+
Errors never break the chart: compile or runtime errors render as an inline red message in the
|
|
608
|
+
pane. Scripts are plain JavaScript executed in the page context — treat them like any other code
|
|
609
|
+
you'd paste into your app (only run scripts you trust).
|