hyperprop-charting-library 0.1.23 → 0.1.25
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 +44 -0
- package/dist/hyperprop-charting-library.cjs +291 -36
- package/dist/hyperprop-charting-library.d.ts +18 -1
- package/dist/hyperprop-charting-library.js +291 -36
- package/dist/index.cjs +291 -36
- package/dist/index.d.cts +18 -1
- package/dist/index.d.ts +18 -1
- package/dist/index.js +291 -36
- package/docs/API.md +18 -1
- package/docs/RECIPES.md +59 -0
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ interface ChartOptions {
|
|
|
4
4
|
backgroundColor?: string;
|
|
5
5
|
axisColor?: string;
|
|
6
6
|
axis?: AxisOptions;
|
|
7
|
+
xAxis?: AxisOptions;
|
|
8
|
+
yAxis?: AxisOptions;
|
|
7
9
|
priceDecimals?: number;
|
|
8
10
|
stabilizePriceLabels?: boolean;
|
|
9
11
|
priceLabelMinIntegerDigits?: number;
|
|
@@ -22,6 +24,9 @@ interface ChartOptions {
|
|
|
22
24
|
candleBodyWidthRatio?: number;
|
|
23
25
|
candleMinWidth?: number;
|
|
24
26
|
candleWickWidth?: number;
|
|
27
|
+
tickSize?: number;
|
|
28
|
+
candleColorMode?: "openClose" | "prevClose";
|
|
29
|
+
candleColorEpsilon?: number;
|
|
25
30
|
autoScaleSmoothing?: number;
|
|
26
31
|
autoScaleIgnoreLatestCandle?: boolean;
|
|
27
32
|
doubleClickEnabled?: boolean;
|
|
@@ -42,6 +47,9 @@ interface IndicatorInstanceOptions<TInputs extends Record<string, unknown> = Rec
|
|
|
42
47
|
visible?: boolean;
|
|
43
48
|
pane?: IndicatorPane;
|
|
44
49
|
paneHeightRatio?: number;
|
|
50
|
+
zIndex?: number;
|
|
51
|
+
excludeFromAutoscale?: boolean;
|
|
52
|
+
overlayScaleWeight?: number;
|
|
45
53
|
inputs?: Partial<TInputs>;
|
|
46
54
|
}
|
|
47
55
|
interface IndicatorRenderContext {
|
|
@@ -65,6 +73,7 @@ interface IndicatorRenderContext {
|
|
|
65
73
|
chartHeight: number;
|
|
66
74
|
xFromIndex: (index: number) => number;
|
|
67
75
|
yFromPrice: ((price: number) => number) | null;
|
|
76
|
+
getCandleDirectionByIndex: (index: number) => "up" | "down";
|
|
68
77
|
candleSpacing: number;
|
|
69
78
|
upColor: string;
|
|
70
79
|
downColor: string;
|
|
@@ -77,6 +86,12 @@ interface IndicatorPlugin<TInputs extends Record<string, unknown> = Record<strin
|
|
|
77
86
|
defaultInputs?: TInputs;
|
|
78
87
|
draw: (ctx: CanvasRenderingContext2D, renderContext: IndicatorRenderContext, inputs: TInputs) => void;
|
|
79
88
|
}
|
|
89
|
+
interface BuiltInIndicatorInfo {
|
|
90
|
+
id: string;
|
|
91
|
+
name: string;
|
|
92
|
+
pane: IndicatorPane;
|
|
93
|
+
paneHeightRatio?: number;
|
|
94
|
+
}
|
|
80
95
|
interface DashPatternOptions {
|
|
81
96
|
dotted: [number, number];
|
|
82
97
|
dashed: [number, number];
|
|
@@ -268,6 +283,8 @@ interface ChartInstance {
|
|
|
268
283
|
setDoubleClickAction: (action: "reset" | "placeLimitOrder") => void;
|
|
269
284
|
registerIndicator: (plugin: IndicatorPlugin<any>) => void;
|
|
270
285
|
unregisterIndicator: (type: string) => void;
|
|
286
|
+
listBuiltInIndicators: () => BuiltInIndicatorInfo[];
|
|
287
|
+
getIndicators: () => IndicatorInstanceOptions[];
|
|
271
288
|
addIndicator: (type: string, inputs?: Record<string, unknown>, options?: Partial<IndicatorInstanceOptions>) => string;
|
|
272
289
|
updateIndicator: (id: string, patch: Partial<IndicatorInstanceOptions>) => void;
|
|
273
290
|
removeIndicator: (id: string) => void;
|
|
@@ -285,4 +302,4 @@ interface OhlcDataPoint {
|
|
|
285
302
|
}
|
|
286
303
|
declare function createChart(element: HTMLElement, options?: ChartOptions): ChartInstance;
|
|
287
304
|
|
|
288
|
-
export { type AxisOptions, type ChartClickEvent, type ChartInstance, type ChartOptions, type CrosshairMoveEvent, type CrosshairOptions, type CrosshairPriceActionEvent, type DashPatternOptions, type GridOptions, type IndicatorInstanceOptions, type IndicatorPane, type IndicatorPlugin, type IndicatorRenderContext, type OhlcDataPoint, type OrderActionButton, type OrderActionEvent, type OrderLineOptions, type PriceLineOptions, type TickerLineOptions, type WatermarkOptions, createChart };
|
|
305
|
+
export { type AxisOptions, type BuiltInIndicatorInfo, type ChartClickEvent, type ChartInstance, type ChartOptions, type CrosshairMoveEvent, type CrosshairOptions, type CrosshairPriceActionEvent, type DashPatternOptions, type GridOptions, type IndicatorInstanceOptions, type IndicatorPane, type IndicatorPlugin, type IndicatorRenderContext, type OhlcDataPoint, type OrderActionButton, type OrderActionEvent, type OrderLineOptions, type PriceLineOptions, type TickerLineOptions, type WatermarkOptions, createChart };
|
package/dist/index.js
CHANGED
|
@@ -109,6 +109,8 @@ var DEFAULT_OPTIONS = {
|
|
|
109
109
|
backgroundColor: "#101114",
|
|
110
110
|
axisColor: "#7f8289",
|
|
111
111
|
axis: DEFAULT_AXIS_OPTIONS,
|
|
112
|
+
xAxis: DEFAULT_AXIS_OPTIONS,
|
|
113
|
+
yAxis: DEFAULT_AXIS_OPTIONS,
|
|
112
114
|
priceDecimals: 2,
|
|
113
115
|
stabilizePriceLabels: true,
|
|
114
116
|
priceLabelMinIntegerDigits: 3,
|
|
@@ -127,6 +129,9 @@ var DEFAULT_OPTIONS = {
|
|
|
127
129
|
candleBodyWidthRatio: 0.7,
|
|
128
130
|
candleMinWidth: 0.5,
|
|
129
131
|
candleWickWidth: 1,
|
|
132
|
+
tickSize: 0,
|
|
133
|
+
candleColorMode: "openClose",
|
|
134
|
+
candleColorEpsilon: -1,
|
|
130
135
|
autoScaleSmoothing: 0.16,
|
|
131
136
|
autoScaleIgnoreLatestCandle: true,
|
|
132
137
|
doubleClickEnabled: true,
|
|
@@ -362,6 +367,35 @@ var computeAtrSeries = (data, length) => {
|
|
|
362
367
|
}
|
|
363
368
|
return result;
|
|
364
369
|
};
|
|
370
|
+
var builtInSeriesCache = /* @__PURE__ */ new Map();
|
|
371
|
+
var getSeriesFingerprint = (data) => {
|
|
372
|
+
const length = data.length;
|
|
373
|
+
const last = length > 0 ? data[length - 1] : void 0;
|
|
374
|
+
const prev = length > 1 ? data[length - 2] : void 0;
|
|
375
|
+
if (!last) return "empty";
|
|
376
|
+
return [
|
|
377
|
+
length,
|
|
378
|
+
last.time.getTime(),
|
|
379
|
+
last.o,
|
|
380
|
+
last.h,
|
|
381
|
+
last.l,
|
|
382
|
+
last.c,
|
|
383
|
+
last.v ?? "",
|
|
384
|
+
prev?.time.getTime() ?? "",
|
|
385
|
+
prev?.c ?? "",
|
|
386
|
+
prev?.v ?? ""
|
|
387
|
+
].join("|");
|
|
388
|
+
};
|
|
389
|
+
var withCachedSeries = (key, data, compute) => {
|
|
390
|
+
const fingerprint = getSeriesFingerprint(data);
|
|
391
|
+
const existing = builtInSeriesCache.get(key);
|
|
392
|
+
if (existing && existing.fingerprint === fingerprint) {
|
|
393
|
+
return existing.values;
|
|
394
|
+
}
|
|
395
|
+
const values = compute();
|
|
396
|
+
builtInSeriesCache.set(key, { fingerprint, values });
|
|
397
|
+
return values;
|
|
398
|
+
};
|
|
365
399
|
var drawOverlaySeries = (ctx, renderContext, values, color, width) => {
|
|
366
400
|
if (!renderContext.yFromPrice) return;
|
|
367
401
|
const yFromPrice = renderContext.yFromPrice;
|
|
@@ -459,6 +493,8 @@ var BUILTIN_VOLUME_INDICATOR = {
|
|
|
459
493
|
defaultInputs: {
|
|
460
494
|
upOpacity: 0.7,
|
|
461
495
|
downOpacity: 0.7,
|
|
496
|
+
upColor: "",
|
|
497
|
+
downColor: "",
|
|
462
498
|
minBarWidth: 1,
|
|
463
499
|
overlayHeightRatio: 0.22
|
|
464
500
|
},
|
|
@@ -487,11 +523,13 @@ var BUILTIN_VOLUME_INDICATOR = {
|
|
|
487
523
|
const xCenter = xFromIndex(index);
|
|
488
524
|
const barX = Math.round(xCenter - barWidth / 2);
|
|
489
525
|
const barY = Math.round(paneBottom - volumeHeight);
|
|
490
|
-
const
|
|
491
|
-
const opacity =
|
|
526
|
+
const direction = renderContext.getCandleDirectionByIndex(index);
|
|
527
|
+
const opacity = direction === "up" ? upOpacity : downOpacity;
|
|
528
|
+
const upBarColor = inputs.upColor && inputs.upColor.trim().length > 0 ? inputs.upColor : upColor;
|
|
529
|
+
const downBarColor = inputs.downColor && inputs.downColor.trim().length > 0 ? inputs.downColor : downColor;
|
|
492
530
|
ctx.save();
|
|
493
531
|
ctx.globalAlpha = opacity;
|
|
494
|
-
ctx.fillStyle =
|
|
532
|
+
ctx.fillStyle = direction === "up" ? upBarColor : downBarColor;
|
|
495
533
|
ctx.fillRect(barX, barY, Math.max(1, Math.round(barWidth)), volumeHeight);
|
|
496
534
|
ctx.restore();
|
|
497
535
|
}
|
|
@@ -504,7 +542,11 @@ var BUILTIN_SMA_INDICATOR = {
|
|
|
504
542
|
defaultInputs: { length: 20, source: "close", color: "#60a5fa", width: 2 },
|
|
505
543
|
draw: (ctx, renderContext, inputs) => {
|
|
506
544
|
const length = clampIndicatorLength(inputs.length, 20);
|
|
507
|
-
const values =
|
|
545
|
+
const values = withCachedSeries(
|
|
546
|
+
`sma|${length}|${inputs.source ?? "close"}`,
|
|
547
|
+
renderContext.data,
|
|
548
|
+
() => computeSmaSeries(renderContext.data, length, inputs.source ?? "close")
|
|
549
|
+
);
|
|
508
550
|
drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#60a5fa", Number(inputs.width) || 2);
|
|
509
551
|
}
|
|
510
552
|
};
|
|
@@ -515,7 +557,11 @@ var BUILTIN_EMA_INDICATOR = {
|
|
|
515
557
|
defaultInputs: { length: 20, source: "close", color: "#f59e0b", width: 2 },
|
|
516
558
|
draw: (ctx, renderContext, inputs) => {
|
|
517
559
|
const length = clampIndicatorLength(inputs.length, 20);
|
|
518
|
-
const values =
|
|
560
|
+
const values = withCachedSeries(
|
|
561
|
+
`ema|${length}|${inputs.source ?? "close"}`,
|
|
562
|
+
renderContext.data,
|
|
563
|
+
() => computeEmaSeries(renderContext.data, length, inputs.source ?? "close")
|
|
564
|
+
);
|
|
519
565
|
drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#f59e0b", Number(inputs.width) || 2);
|
|
520
566
|
}
|
|
521
567
|
};
|
|
@@ -526,7 +572,11 @@ var BUILTIN_WMA_INDICATOR = {
|
|
|
526
572
|
defaultInputs: { length: 20, source: "close", color: "#a78bfa", width: 2 },
|
|
527
573
|
draw: (ctx, renderContext, inputs) => {
|
|
528
574
|
const length = clampIndicatorLength(inputs.length, 20);
|
|
529
|
-
const values =
|
|
575
|
+
const values = withCachedSeries(
|
|
576
|
+
`wma|${length}|${inputs.source ?? "close"}`,
|
|
577
|
+
renderContext.data,
|
|
578
|
+
() => computeWmaSeries(renderContext.data, length, inputs.source ?? "close")
|
|
579
|
+
);
|
|
530
580
|
drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#a78bfa", Number(inputs.width) || 2);
|
|
531
581
|
}
|
|
532
582
|
};
|
|
@@ -537,7 +587,11 @@ var BUILTIN_VWMA_INDICATOR = {
|
|
|
537
587
|
defaultInputs: { length: 20, source: "close", color: "#ef4444", width: 2 },
|
|
538
588
|
draw: (ctx, renderContext, inputs) => {
|
|
539
589
|
const length = clampIndicatorLength(inputs.length, 20);
|
|
540
|
-
const values =
|
|
590
|
+
const values = withCachedSeries(
|
|
591
|
+
`vwma|${length}|${inputs.source ?? "close"}`,
|
|
592
|
+
renderContext.data,
|
|
593
|
+
() => computeVwmaSeries(renderContext.data, length, inputs.source ?? "close")
|
|
594
|
+
);
|
|
541
595
|
drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#ef4444", Number(inputs.width) || 2);
|
|
542
596
|
}
|
|
543
597
|
};
|
|
@@ -548,7 +602,11 @@ var BUILTIN_RMA_INDICATOR = {
|
|
|
548
602
|
defaultInputs: { length: 14, source: "close", color: "#22c55e", width: 2 },
|
|
549
603
|
draw: (ctx, renderContext, inputs) => {
|
|
550
604
|
const length = clampIndicatorLength(inputs.length, 14);
|
|
551
|
-
const values =
|
|
605
|
+
const values = withCachedSeries(
|
|
606
|
+
`rma|${length}|${inputs.source ?? "close"}`,
|
|
607
|
+
renderContext.data,
|
|
608
|
+
() => computeRmaSeries(renderContext.data, length, inputs.source ?? "close")
|
|
609
|
+
);
|
|
552
610
|
drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#22c55e", Number(inputs.width) || 2);
|
|
553
611
|
}
|
|
554
612
|
};
|
|
@@ -559,7 +617,11 @@ var BUILTIN_HMA_INDICATOR = {
|
|
|
559
617
|
defaultInputs: { length: 21, source: "close", color: "#14b8a6", width: 2 },
|
|
560
618
|
draw: (ctx, renderContext, inputs) => {
|
|
561
619
|
const length = clampIndicatorLength(inputs.length, 21);
|
|
562
|
-
const values =
|
|
620
|
+
const values = withCachedSeries(
|
|
621
|
+
`hma|${length}|${inputs.source ?? "close"}`,
|
|
622
|
+
renderContext.data,
|
|
623
|
+
() => computeHmaSeries(renderContext.data, length, inputs.source ?? "close")
|
|
624
|
+
);
|
|
563
625
|
drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#14b8a6", Number(inputs.width) || 2);
|
|
564
626
|
}
|
|
565
627
|
};
|
|
@@ -571,7 +633,11 @@ var BUILTIN_STDDEV_INDICATOR = {
|
|
|
571
633
|
defaultInputs: { length: 20, source: "close", color: "#f97316", width: 2 },
|
|
572
634
|
draw: (ctx, renderContext, inputs) => {
|
|
573
635
|
const length = clampIndicatorLength(inputs.length, 20);
|
|
574
|
-
const values =
|
|
636
|
+
const values = withCachedSeries(
|
|
637
|
+
`stddev|${length}|${inputs.source ?? "close"}`,
|
|
638
|
+
renderContext.data,
|
|
639
|
+
() => computeStdDevSeries(renderContext.data, length, inputs.source ?? "close")
|
|
640
|
+
);
|
|
575
641
|
drawSeparateSeries(ctx, renderContext, values, inputs.color ?? "#f97316", Number(inputs.width) || 2);
|
|
576
642
|
}
|
|
577
643
|
};
|
|
@@ -583,7 +649,7 @@ var BUILTIN_ATR_INDICATOR = {
|
|
|
583
649
|
defaultInputs: { length: 14, color: "#eab308", width: 2 },
|
|
584
650
|
draw: (ctx, renderContext, inputs) => {
|
|
585
651
|
const length = clampIndicatorLength(inputs.length, 14);
|
|
586
|
-
const values = computeAtrSeries(renderContext.data, length);
|
|
652
|
+
const values = withCachedSeries(`atr|${length}`, renderContext.data, () => computeAtrSeries(renderContext.data, length));
|
|
587
653
|
drawSeparateSeries(ctx, renderContext, values, inputs.color ?? "#eab308", Number(inputs.width) || 2);
|
|
588
654
|
}
|
|
589
655
|
};
|
|
@@ -595,7 +661,7 @@ var BUILTIN_RSI_INDICATOR = {
|
|
|
595
661
|
defaultInputs: { length: 14, color: "#3b82f6", width: 2 },
|
|
596
662
|
draw: (ctx, renderContext, inputs) => {
|
|
597
663
|
const length = clampIndicatorLength(inputs.length, 14);
|
|
598
|
-
const values = computeRsiSeries(renderContext.data, length);
|
|
664
|
+
const values = withCachedSeries(`rsi|${length}`, renderContext.data, () => computeRsiSeries(renderContext.data, length));
|
|
599
665
|
drawSeparateSeries(
|
|
600
666
|
ctx,
|
|
601
667
|
renderContext,
|
|
@@ -629,6 +695,18 @@ function createChart(element, options = {}) {
|
|
|
629
695
|
...options.axis ?? {},
|
|
630
696
|
...options.axisColor ? { lineColor: options.axisColor, textColor: options.axisColor } : {}
|
|
631
697
|
},
|
|
698
|
+
xAxis: {
|
|
699
|
+
...DEFAULT_AXIS_OPTIONS,
|
|
700
|
+
...options.axis ?? {},
|
|
701
|
+
...options.axisColor ? { lineColor: options.axisColor, textColor: options.axisColor } : {},
|
|
702
|
+
...options.xAxis ?? {}
|
|
703
|
+
},
|
|
704
|
+
yAxis: {
|
|
705
|
+
...DEFAULT_AXIS_OPTIONS,
|
|
706
|
+
...options.axis ?? {},
|
|
707
|
+
...options.axisColor ? { lineColor: options.axisColor, textColor: options.axisColor } : {},
|
|
708
|
+
...options.yAxis ?? {}
|
|
709
|
+
},
|
|
632
710
|
crosshair: {
|
|
633
711
|
...DEFAULT_CROSSHAIR_OPTIONS,
|
|
634
712
|
...options.crosshair ?? {}
|
|
@@ -684,6 +762,9 @@ function createChart(element, options = {}) {
|
|
|
684
762
|
visible: indicator.visible ?? true,
|
|
685
763
|
pane: indicator.pane ?? plugin?.pane ?? "overlay",
|
|
686
764
|
...indicator.paneHeightRatio === void 0 ? {} : { paneHeightRatio: indicator.paneHeightRatio },
|
|
765
|
+
zIndex: Math.round(Number(indicator.zIndex) || 0),
|
|
766
|
+
excludeFromAutoscale: indicator.excludeFromAutoscale ?? true,
|
|
767
|
+
overlayScaleWeight: Math.min(1, Math.max(0, Number(indicator.overlayScaleWeight) || 0.25)),
|
|
687
768
|
inputs: {
|
|
688
769
|
...defaults,
|
|
689
770
|
...indicator.inputs ?? {}
|
|
@@ -839,9 +920,63 @@ function createChart(element, options = {}) {
|
|
|
839
920
|
}
|
|
840
921
|
return { min: nextMin, max: nextMax };
|
|
841
922
|
};
|
|
923
|
+
const getConfiguredPriceDecimals = () => clamp(Math.round(mergedOptions.priceDecimals), 0, 8);
|
|
924
|
+
const getConfiguredTickSize = () => {
|
|
925
|
+
const step = Number(mergedOptions.tickSize);
|
|
926
|
+
return Number.isFinite(step) && step > 0 ? step : 0;
|
|
927
|
+
};
|
|
928
|
+
const getTickSizeDecimals = () => {
|
|
929
|
+
const tickSize = getConfiguredTickSize();
|
|
930
|
+
if (tickSize <= 0) {
|
|
931
|
+
return 0;
|
|
932
|
+
}
|
|
933
|
+
let scaled = tickSize;
|
|
934
|
+
let decimals = 0;
|
|
935
|
+
while (decimals < 8 && Math.abs(Math.round(scaled) - scaled) > 1e-10) {
|
|
936
|
+
scaled *= 10;
|
|
937
|
+
decimals += 1;
|
|
938
|
+
}
|
|
939
|
+
return decimals;
|
|
940
|
+
};
|
|
941
|
+
const getDisplayPriceDecimals = () => {
|
|
942
|
+
const configured = getConfiguredPriceDecimals();
|
|
943
|
+
const tickDecimals = getTickSizeDecimals();
|
|
944
|
+
return Math.max(configured, tickDecimals);
|
|
945
|
+
};
|
|
946
|
+
const quantizeToTickSize = (price) => {
|
|
947
|
+
const tickSize = getConfiguredTickSize();
|
|
948
|
+
if (tickSize <= 0 || !Number.isFinite(price)) {
|
|
949
|
+
return price;
|
|
950
|
+
}
|
|
951
|
+
return Math.round(price / tickSize) * tickSize;
|
|
952
|
+
};
|
|
842
953
|
const formatPrice = (price) => {
|
|
843
|
-
const
|
|
844
|
-
|
|
954
|
+
const rounded = quantizeToTickSize(price);
|
|
955
|
+
const decimals = getDisplayPriceDecimals();
|
|
956
|
+
return rounded.toFixed(decimals);
|
|
957
|
+
};
|
|
958
|
+
const roundToPricePrecision = (price) => {
|
|
959
|
+
const rounded = quantizeToTickSize(price);
|
|
960
|
+
const decimals = getDisplayPriceDecimals();
|
|
961
|
+
return Number(rounded.toFixed(decimals));
|
|
962
|
+
};
|
|
963
|
+
const getResolvedCandleColorEpsilon = () => {
|
|
964
|
+
const configured = mergedOptions.candleColorEpsilon;
|
|
965
|
+
if (configured >= 0) {
|
|
966
|
+
return configured;
|
|
967
|
+
}
|
|
968
|
+
const tickSize = getConfiguredTickSize();
|
|
969
|
+
if (tickSize > 0) {
|
|
970
|
+
return tickSize / 2;
|
|
971
|
+
}
|
|
972
|
+
const decimals = getDisplayPriceDecimals();
|
|
973
|
+
return decimals > 0 ? 0.5 / 10 ** decimals : 0;
|
|
974
|
+
};
|
|
975
|
+
const getDirectionFromDelta = (delta) => {
|
|
976
|
+
const epsilon = Math.max(0, getResolvedCandleColorEpsilon());
|
|
977
|
+
if (delta > epsilon) return 1;
|
|
978
|
+
if (delta < -epsilon) return -1;
|
|
979
|
+
return 0;
|
|
845
980
|
};
|
|
846
981
|
const getStabilizedPriceTemplate = () => {
|
|
847
982
|
const explicitTemplate = mergedOptions.priceLabelWidthTemplate.trim();
|
|
@@ -869,14 +1004,33 @@ function createChart(element, options = {}) {
|
|
|
869
1004
|
return Math.max(measured, templateWidth);
|
|
870
1005
|
};
|
|
871
1006
|
const parseData = (nextData) => {
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
1007
|
+
const dedupedByTime = /* @__PURE__ */ new Map();
|
|
1008
|
+
for (const point of nextData) {
|
|
1009
|
+
const time = new Date(point.t);
|
|
1010
|
+
const timeMs = time.getTime();
|
|
1011
|
+
if (!Number.isFinite(timeMs)) {
|
|
1012
|
+
continue;
|
|
1013
|
+
}
|
|
1014
|
+
const open = Number(point.o);
|
|
1015
|
+
const close = Number(point.c);
|
|
1016
|
+
const highInput = Number(point.h);
|
|
1017
|
+
const lowInput = Number(point.l);
|
|
1018
|
+
if (!Number.isFinite(open) || !Number.isFinite(close) || !Number.isFinite(highInput) || !Number.isFinite(lowInput)) {
|
|
1019
|
+
continue;
|
|
1020
|
+
}
|
|
1021
|
+
const normalizedHigh = Math.max(highInput, open, close);
|
|
1022
|
+
const normalizedLow = Math.min(lowInput, open, close);
|
|
1023
|
+
const volumeValue = point.v === void 0 ? void 0 : Number(point.v);
|
|
1024
|
+
dedupedByTime.set(timeMs, {
|
|
1025
|
+
time,
|
|
1026
|
+
o: open,
|
|
1027
|
+
h: normalizedHigh,
|
|
1028
|
+
l: normalizedLow,
|
|
1029
|
+
c: close,
|
|
1030
|
+
...volumeValue === void 0 || !Number.isFinite(volumeValue) ? {} : { v: volumeValue }
|
|
1031
|
+
});
|
|
1032
|
+
}
|
|
1033
|
+
return Array.from(dedupedByTime.values()).sort((a, b) => a.time.getTime() - b.time.getTime());
|
|
880
1034
|
};
|
|
881
1035
|
const getTimeStepMs = () => {
|
|
882
1036
|
if (data.length < 2) {
|
|
@@ -953,6 +1107,26 @@ function createChart(element, options = {}) {
|
|
|
953
1107
|
const upperDelta = Math.abs(upperPoint.time.getTime() - timeMs);
|
|
954
1108
|
return lowerDelta <= upperDelta ? lower : upper;
|
|
955
1109
|
};
|
|
1110
|
+
const getCandleDirectionByIndex = (index) => {
|
|
1111
|
+
const point = data[index];
|
|
1112
|
+
if (!point) {
|
|
1113
|
+
return "up";
|
|
1114
|
+
}
|
|
1115
|
+
const prevPoint = index > 0 ? data[index - 1] : void 0;
|
|
1116
|
+
const mode = mergedOptions.candleColorMode;
|
|
1117
|
+
const baseForMode = mode === "prevClose" && prevPoint ? prevPoint.c : point.o;
|
|
1118
|
+
let direction = getDirectionFromDelta(point.c - baseForMode);
|
|
1119
|
+
if (direction === 0 && mode === "prevClose") {
|
|
1120
|
+
direction = getDirectionFromDelta(point.c - point.o);
|
|
1121
|
+
}
|
|
1122
|
+
if (direction === 0 && prevPoint) {
|
|
1123
|
+
direction = getDirectionFromDelta(point.c - prevPoint.c);
|
|
1124
|
+
}
|
|
1125
|
+
if (direction === 0) {
|
|
1126
|
+
return point.c >= point.o ? "up" : "down";
|
|
1127
|
+
}
|
|
1128
|
+
return direction > 0 ? "up" : "down";
|
|
1129
|
+
};
|
|
956
1130
|
const formatHoverTimeLabel = (time, mode) => {
|
|
957
1131
|
if (mode === "time") {
|
|
958
1132
|
return time.toLocaleTimeString(void 0, {
|
|
@@ -1327,6 +1501,10 @@ function createChart(element, options = {}) {
|
|
|
1327
1501
|
canvas.height = Math.floor(height * pixelRatio);
|
|
1328
1502
|
ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
|
|
1329
1503
|
const axis = { ...DEFAULT_AXIS_OPTIONS, ...mergedOptions.axis ?? {} };
|
|
1504
|
+
const xAxis = { ...DEFAULT_AXIS_OPTIONS, ...mergedOptions.xAxis ?? {} };
|
|
1505
|
+
const yAxis = { ...DEFAULT_AXIS_OPTIONS, ...mergedOptions.yAxis ?? {} };
|
|
1506
|
+
const xAxisFontSize = Math.max(8, xAxis.fontSize);
|
|
1507
|
+
const yAxisFontSize = Math.max(8, yAxis.fontSize);
|
|
1330
1508
|
ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
|
|
1331
1509
|
ctx.fillStyle = mergedOptions.backgroundColor;
|
|
1332
1510
|
ctx.fillRect(0, 0, width, height);
|
|
@@ -1341,6 +1519,9 @@ function createChart(element, options = {}) {
|
|
|
1341
1519
|
const separatePaneSpacing = 6;
|
|
1342
1520
|
const activeSeparateIndicators = indicators.filter((indicator) => indicator.visible).map((indicator) => ({ indicator, plugin: indicatorRegistry.get(indicator.type) })).filter(
|
|
1343
1521
|
(value) => value.plugin !== void 0 && (value.indicator.pane ?? value.plugin.pane ?? "overlay") === "separate"
|
|
1522
|
+
).sort((a, b) => a.indicator.zIndex - b.indicator.zIndex);
|
|
1523
|
+
const overlayIndicatorsForScale = indicators.filter((indicator) => indicator.visible).map((indicator) => ({ indicator, plugin: indicatorRegistry.get(indicator.type) })).filter(
|
|
1524
|
+
(value) => value.plugin !== void 0 && (value.indicator.pane ?? value.plugin.pane ?? "overlay") === "overlay"
|
|
1344
1525
|
);
|
|
1345
1526
|
const separatePaneHeightDefaults = activeSeparateIndicators.map(({ indicator, plugin }) => {
|
|
1346
1527
|
const ratio = Math.min(0.45, Math.max(0.08, indicator.paneHeightRatio ?? plugin.paneHeightRatio ?? 0.22));
|
|
@@ -1392,8 +1573,47 @@ function createChart(element, options = {}) {
|
|
|
1392
1573
|
}
|
|
1393
1574
|
}
|
|
1394
1575
|
}
|
|
1395
|
-
|
|
1396
|
-
|
|
1576
|
+
let minPrice = Math.min(...priceSource.map((point) => point.l));
|
|
1577
|
+
let maxPrice = Math.max(...priceSource.map((point) => point.h));
|
|
1578
|
+
if (overlayIndicatorsForScale.length > 0) {
|
|
1579
|
+
for (const { indicator } of overlayIndicatorsForScale) {
|
|
1580
|
+
if (indicator.excludeFromAutoscale) {
|
|
1581
|
+
continue;
|
|
1582
|
+
}
|
|
1583
|
+
const type = indicator.type;
|
|
1584
|
+
const inputs = indicator.inputs;
|
|
1585
|
+
const source = inputs.source ?? "close";
|
|
1586
|
+
const length = clampIndicatorLength(inputs.length ?? 14, 14);
|
|
1587
|
+
let series = null;
|
|
1588
|
+
if (type === "sma") series = withCachedSeries(`sma|${length}|${source}`, data, () => computeSmaSeries(data, length, source));
|
|
1589
|
+
if (type === "ema") series = withCachedSeries(`ema|${length}|${source}`, data, () => computeEmaSeries(data, length, source));
|
|
1590
|
+
if (type === "wma") series = withCachedSeries(`wma|${length}|${source}`, data, () => computeWmaSeries(data, length, source));
|
|
1591
|
+
if (type === "vwma") series = withCachedSeries(`vwma|${length}|${source}`, data, () => computeVwmaSeries(data, length, source));
|
|
1592
|
+
if (type === "rma") series = withCachedSeries(`rma|${length}|${source}`, data, () => computeRmaSeries(data, length, source));
|
|
1593
|
+
if (type === "hma") series = withCachedSeries(`hma|${length}|${source}`, data, () => computeHmaSeries(data, length, source));
|
|
1594
|
+
if (!series) {
|
|
1595
|
+
continue;
|
|
1596
|
+
}
|
|
1597
|
+
const visibleValues = [];
|
|
1598
|
+
for (let idx = startIndex; idx <= endIndex; idx += 1) {
|
|
1599
|
+
const value = series[idx];
|
|
1600
|
+
if (Number.isFinite(value ?? Number.NaN)) {
|
|
1601
|
+
visibleValues.push(value);
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
if (visibleValues.length === 0) {
|
|
1605
|
+
continue;
|
|
1606
|
+
}
|
|
1607
|
+
const seriesMin = Math.min(...visibleValues);
|
|
1608
|
+
const seriesMax = Math.max(...visibleValues);
|
|
1609
|
+
const weight = Math.min(1, Math.max(0, indicator.overlayScaleWeight));
|
|
1610
|
+
const currentMid = (minPrice + maxPrice) / 2;
|
|
1611
|
+
const weightedMin = currentMid + (seriesMin - currentMid) * weight;
|
|
1612
|
+
const weightedMax = currentMid + (seriesMax - currentMid) * weight;
|
|
1613
|
+
minPrice = Math.min(minPrice, weightedMin);
|
|
1614
|
+
maxPrice = Math.max(maxPrice, weightedMax);
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1397
1617
|
const priceRange = maxPrice - minPrice || 1;
|
|
1398
1618
|
const autoMin = minPrice - priceRange * 0.08;
|
|
1399
1619
|
const autoMax = maxPrice + priceRange * 0.08;
|
|
@@ -1538,8 +1758,8 @@ function createChart(element, options = {}) {
|
|
|
1538
1758
|
const closeY = yFromPrice(point.c);
|
|
1539
1759
|
const highY = yFromPrice(point.h);
|
|
1540
1760
|
const lowY = yFromPrice(point.l);
|
|
1541
|
-
const
|
|
1542
|
-
const candleColor =
|
|
1761
|
+
const direction = getCandleDirectionByIndex(index);
|
|
1762
|
+
const candleColor = direction === "up" ? mergedOptions.upColor : mergedOptions.downColor;
|
|
1543
1763
|
ctx.strokeStyle = candleColor;
|
|
1544
1764
|
ctx.lineWidth = candleWickWidth;
|
|
1545
1765
|
ctx.beginPath();
|
|
@@ -1553,7 +1773,7 @@ function createChart(element, options = {}) {
|
|
|
1553
1773
|
}
|
|
1554
1774
|
const activeOverlayIndicators = indicators.filter((indicator) => indicator.visible).map((indicator) => ({ indicator, plugin: indicatorRegistry.get(indicator.type) })).filter(
|
|
1555
1775
|
(value) => value.plugin !== void 0 && (value.indicator.pane ?? value.plugin.pane ?? "overlay") === "overlay"
|
|
1556
|
-
);
|
|
1776
|
+
).sort((a, b) => a.indicator.zIndex - b.indicator.zIndex);
|
|
1557
1777
|
if (activeOverlayIndicators.length > 0) {
|
|
1558
1778
|
const xFromIndex = (index) => chartLeft + (index + 0.5 - xStart) / xSpan * chartWidth;
|
|
1559
1779
|
activeOverlayIndicators.forEach(({ indicator, plugin }) => {
|
|
@@ -1573,6 +1793,7 @@ function createChart(element, options = {}) {
|
|
|
1573
1793
|
chartHeight,
|
|
1574
1794
|
xFromIndex,
|
|
1575
1795
|
yFromPrice,
|
|
1796
|
+
getCandleDirectionByIndex,
|
|
1576
1797
|
candleSpacing,
|
|
1577
1798
|
upColor: mergedOptions.upColor,
|
|
1578
1799
|
downColor: mergedOptions.downColor
|
|
@@ -1632,6 +1853,7 @@ function createChart(element, options = {}) {
|
|
|
1632
1853
|
chartHeight: paneHeight,
|
|
1633
1854
|
xFromIndex,
|
|
1634
1855
|
yFromPrice: null,
|
|
1856
|
+
getCandleDirectionByIndex,
|
|
1635
1857
|
candleSpacing,
|
|
1636
1858
|
upColor: mergedOptions.upColor,
|
|
1637
1859
|
downColor: mergedOptions.downColor
|
|
@@ -1661,7 +1883,10 @@ function createChart(element, options = {}) {
|
|
|
1661
1883
|
const ratio = tick / yTicks;
|
|
1662
1884
|
const price = yMin + yRange * ratio;
|
|
1663
1885
|
const y = yFromPrice(price);
|
|
1664
|
-
|
|
1886
|
+
const prevFont = ctx.font;
|
|
1887
|
+
ctx.font = `${yAxisFontSize}px ${mergedOptions.fontFamily}`;
|
|
1888
|
+
drawText(formatPrice(price), chartRight + 6, y, "left", "middle", yAxis.textColor);
|
|
1889
|
+
ctx.font = prevFont;
|
|
1665
1890
|
}
|
|
1666
1891
|
const ticker = mergedOptions.tickerLine ?? DEFAULT_OPTIONS.tickerLine;
|
|
1667
1892
|
const lastPoint = data[data.length - 1];
|
|
@@ -1669,7 +1894,8 @@ function createChart(element, options = {}) {
|
|
|
1669
1894
|
const tickerPrice = lastPoint.c;
|
|
1670
1895
|
const tickerY = yFromPrice(tickerPrice);
|
|
1671
1896
|
const lineY = clamp(tickerY, chartTop + 1, chartBottom - 1);
|
|
1672
|
-
const
|
|
1897
|
+
const lastDirection = getCandleDirectionByIndex(data.length - 1);
|
|
1898
|
+
const tickerColor = ticker.color ?? (lastDirection === "up" ? mergedOptions.upColor : mergedOptions.downColor);
|
|
1673
1899
|
const tickerThickness = Math.max(1, ticker.thickness ?? 1);
|
|
1674
1900
|
const tickerStyle = ticker.style ?? "solid";
|
|
1675
1901
|
ctx.save();
|
|
@@ -1717,7 +1943,10 @@ function createChart(element, options = {}) {
|
|
|
1717
1943
|
month: "short",
|
|
1718
1944
|
day: "numeric"
|
|
1719
1945
|
});
|
|
1720
|
-
|
|
1946
|
+
const prevFont = ctx.font;
|
|
1947
|
+
ctx.font = `${xAxisFontSize}px ${mergedOptions.fontFamily}`;
|
|
1948
|
+
drawText(timeLabel, x, fullChartBottom + 8, "center", "top", xAxis.textColor);
|
|
1949
|
+
ctx.font = prevFont;
|
|
1721
1950
|
}
|
|
1722
1951
|
if (crosshair.visible && crosshairPoint) {
|
|
1723
1952
|
const cx = clamp(crosshairPoint.x, chartLeft, chartRight);
|
|
@@ -2037,7 +2266,7 @@ function createChart(element, options = {}) {
|
|
|
2037
2266
|
x,
|
|
2038
2267
|
y,
|
|
2039
2268
|
region,
|
|
2040
|
-
...region === "plot" ? { price:
|
|
2269
|
+
...region === "plot" ? { price: roundToPricePrecision(priceFromCanvasY(y)) } : {},
|
|
2041
2270
|
...index === null ? {} : { index },
|
|
2042
2271
|
...hoverTime ? { time: hoverTime.toISOString() } : {},
|
|
2043
2272
|
...point ? { point } : {}
|
|
@@ -2083,7 +2312,7 @@ function createChart(element, options = {}) {
|
|
|
2083
2312
|
if (orderRegion) {
|
|
2084
2313
|
if (orderRegion.draggable) {
|
|
2085
2314
|
activePointerId = event.pointerId;
|
|
2086
|
-
const startPrice =
|
|
2315
|
+
const startPrice = roundToPricePrecision(orderRegion.line.price);
|
|
2087
2316
|
actionDragState = {
|
|
2088
2317
|
orderId: orderRegion.orderId,
|
|
2089
2318
|
action: orderRegion.action,
|
|
@@ -2142,7 +2371,7 @@ function createChart(element, options = {}) {
|
|
|
2142
2371
|
if (activePointerId !== null && event.pointerId !== activePointerId) {
|
|
2143
2372
|
return;
|
|
2144
2373
|
}
|
|
2145
|
-
const nextPrice =
|
|
2374
|
+
const nextPrice = roundToPricePrecision(priceFromCanvasY(point.y));
|
|
2146
2375
|
if (nextPrice !== orderDragState.lastPrice) {
|
|
2147
2376
|
orderDragState.lastPrice = nextPrice;
|
|
2148
2377
|
orderLines = orderLines.map((line) => {
|
|
@@ -2172,7 +2401,7 @@ function createChart(element, options = {}) {
|
|
|
2172
2401
|
if (activePointerId !== null && event.pointerId !== activePointerId) {
|
|
2173
2402
|
return;
|
|
2174
2403
|
}
|
|
2175
|
-
const nextPrice =
|
|
2404
|
+
const nextPrice = roundToPricePrecision(priceFromCanvasY(point.y));
|
|
2176
2405
|
if (nextPrice !== actionDragState.lastPrice) {
|
|
2177
2406
|
actionDragState.lastPrice = nextPrice;
|
|
2178
2407
|
actionDragState.moved = true;
|
|
@@ -2289,7 +2518,7 @@ function createChart(element, options = {}) {
|
|
|
2289
2518
|
canvas.style.cursor = "default";
|
|
2290
2519
|
if (event && pointerDownInfo && event.pointerId === pointerDownInfo.pointerId) {
|
|
2291
2520
|
if (!pointerDownInfo.moved) {
|
|
2292
|
-
const clickPrice = pointerDownInfo.region === "plot" ?
|
|
2521
|
+
const clickPrice = pointerDownInfo.region === "plot" ? roundToPricePrecision(priceFromCanvasY(pointerDownInfo.y)) : void 0;
|
|
2293
2522
|
chartClickHandler?.({
|
|
2294
2523
|
x: pointerDownInfo.x,
|
|
2295
2524
|
y: pointerDownInfo.y,
|
|
@@ -2349,7 +2578,7 @@ function createChart(element, options = {}) {
|
|
|
2349
2578
|
}
|
|
2350
2579
|
orderActionHandler?.({
|
|
2351
2580
|
action: "createLimit",
|
|
2352
|
-
price:
|
|
2581
|
+
price: roundToPricePrecision(priceFromCanvasY(point.y))
|
|
2353
2582
|
});
|
|
2354
2583
|
return;
|
|
2355
2584
|
}
|
|
@@ -2488,6 +2717,27 @@ function createChart(element, options = {}) {
|
|
|
2488
2717
|
indicators = indicators.filter((indicator) => indicator.type !== type);
|
|
2489
2718
|
draw();
|
|
2490
2719
|
};
|
|
2720
|
+
const listBuiltInIndicators = () => {
|
|
2721
|
+
return BUILTIN_INDICATORS.map((indicator) => ({
|
|
2722
|
+
id: indicator.id,
|
|
2723
|
+
name: indicator.name,
|
|
2724
|
+
pane: indicator.pane ?? "overlay",
|
|
2725
|
+
...indicator.paneHeightRatio === void 0 ? {} : { paneHeightRatio: indicator.paneHeightRatio }
|
|
2726
|
+
}));
|
|
2727
|
+
};
|
|
2728
|
+
const getIndicators = () => {
|
|
2729
|
+
return indicators.map((indicator) => ({
|
|
2730
|
+
id: indicator.id,
|
|
2731
|
+
type: indicator.type,
|
|
2732
|
+
visible: indicator.visible,
|
|
2733
|
+
pane: indicator.pane,
|
|
2734
|
+
...indicator.paneHeightRatio === void 0 ? {} : { paneHeightRatio: indicator.paneHeightRatio },
|
|
2735
|
+
zIndex: indicator.zIndex,
|
|
2736
|
+
excludeFromAutoscale: indicator.excludeFromAutoscale,
|
|
2737
|
+
overlayScaleWeight: indicator.overlayScaleWeight,
|
|
2738
|
+
inputs: { ...indicator.inputs }
|
|
2739
|
+
}));
|
|
2740
|
+
};
|
|
2491
2741
|
const setIndicators = (nextIndicators) => {
|
|
2492
2742
|
indicators = nextIndicators.map((indicator) => normalizeIndicatorState(indicator));
|
|
2493
2743
|
draw();
|
|
@@ -2517,6 +2767,9 @@ function createChart(element, options = {}) {
|
|
|
2517
2767
|
visible: patch.visible ?? indicator.visible,
|
|
2518
2768
|
pane: patch.pane ?? indicator.pane ?? plugin?.pane ?? "overlay",
|
|
2519
2769
|
...patch.paneHeightRatio !== void 0 || indicator.paneHeightRatio !== void 0 ? { paneHeightRatio: patch.paneHeightRatio ?? indicator.paneHeightRatio } : {},
|
|
2770
|
+
zIndex: patch.zIndex === void 0 ? indicator.zIndex : Math.round(Number(patch.zIndex) || 0),
|
|
2771
|
+
excludeFromAutoscale: patch.excludeFromAutoscale ?? indicator.excludeFromAutoscale,
|
|
2772
|
+
overlayScaleWeight: patch.overlayScaleWeight === void 0 ? indicator.overlayScaleWeight : Math.min(1, Math.max(0, Number(patch.overlayScaleWeight) || 0)),
|
|
2520
2773
|
type: patch.type ?? indicator.type,
|
|
2521
2774
|
inputs: {
|
|
2522
2775
|
...indicator.inputs,
|
|
@@ -2566,6 +2819,8 @@ function createChart(element, options = {}) {
|
|
|
2566
2819
|
setDoubleClickAction,
|
|
2567
2820
|
registerIndicator,
|
|
2568
2821
|
unregisterIndicator,
|
|
2822
|
+
listBuiltInIndicators,
|
|
2823
|
+
getIndicators,
|
|
2569
2824
|
addIndicator,
|
|
2570
2825
|
updateIndicator,
|
|
2571
2826
|
removeIndicator,
|