hyperprop-charting-library 0.1.119 → 0.1.121
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 +809 -351
- package/dist/hyperprop-charting-library.d.ts +25 -0
- package/dist/hyperprop-charting-library.js +809 -351
- package/dist/index.cjs +809 -351
- package/dist/index.d.cts +25 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +809 -351
- package/package.json +1 -1
|
@@ -189,8 +189,12 @@ var DEFAULT_OPTIONS = {
|
|
|
189
189
|
tickSize: 0,
|
|
190
190
|
candleColorMode: "openClose",
|
|
191
191
|
candleColorEpsilon: -1,
|
|
192
|
-
|
|
192
|
+
// 0 = deterministic scale (lightweight-charts behavior): the y-range is a
|
|
193
|
+
// pure function of the visible data, so it can never drift or "breathe".
|
|
194
|
+
// Values > 0 re-enable eased contraction for hosts that prefer it.
|
|
195
|
+
autoScaleSmoothing: 0,
|
|
193
196
|
autoScaleIgnoreLatestCandle: true,
|
|
197
|
+
kineticScroll: { touch: true, mouse: false },
|
|
194
198
|
pinOutOfRangeLines: false,
|
|
195
199
|
doubleClickEnabled: true,
|
|
196
200
|
doubleClickAction: "reset",
|
|
@@ -409,6 +413,60 @@ var computeStdDevSeries = (data, length, source) => {
|
|
|
409
413
|
}
|
|
410
414
|
return result;
|
|
411
415
|
};
|
|
416
|
+
var VWAP_SESSION_ANCHOR_MS = 22 * 36e5;
|
|
417
|
+
var vwapSessionDayOf = (ms) => Math.floor((ms - VWAP_SESSION_ANCHOR_MS) / 864e5);
|
|
418
|
+
var computeVwapSeries = (data, band) => {
|
|
419
|
+
const result = new Array(data.length).fill(null);
|
|
420
|
+
let session = Number.NaN;
|
|
421
|
+
let cumPv = 0;
|
|
422
|
+
let cumPv2 = 0;
|
|
423
|
+
let cumV = 0;
|
|
424
|
+
for (let i = 0; i < data.length; i += 1) {
|
|
425
|
+
const point = data[i];
|
|
426
|
+
if (!point) continue;
|
|
427
|
+
const day = vwapSessionDayOf(point.time.getTime());
|
|
428
|
+
if (day !== session) {
|
|
429
|
+
session = day;
|
|
430
|
+
cumPv = 0;
|
|
431
|
+
cumPv2 = 0;
|
|
432
|
+
cumV = 0;
|
|
433
|
+
}
|
|
434
|
+
const typical = (point.h + point.l + point.c) / 3;
|
|
435
|
+
const volume = Math.max(0, point.v ?? 0);
|
|
436
|
+
cumPv += typical * volume;
|
|
437
|
+
cumPv2 += typical * typical * volume;
|
|
438
|
+
cumV += volume;
|
|
439
|
+
if (cumV <= 0) continue;
|
|
440
|
+
const vwap = cumPv / cumV;
|
|
441
|
+
if (band === "vwap") {
|
|
442
|
+
result[i] = vwap;
|
|
443
|
+
} else {
|
|
444
|
+
const variance = Math.max(0, cumPv2 / cumV - vwap * vwap);
|
|
445
|
+
result[i] = Math.sqrt(variance);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
for (let i = 1; i < data.length; i += 1) {
|
|
449
|
+
const point = data[i];
|
|
450
|
+
const prev = data[i - 1];
|
|
451
|
+
const next = data[i + 1];
|
|
452
|
+
if (!point || !prev || !next) continue;
|
|
453
|
+
const day = vwapSessionDayOf(point.time.getTime());
|
|
454
|
+
if (day !== vwapSessionDayOf(prev.time.getTime()) && day === vwapSessionDayOf(next.time.getTime())) {
|
|
455
|
+
result[i] = null;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
return result;
|
|
459
|
+
};
|
|
460
|
+
var computeBollingerSeries = (data, length, multiplier, source, band) => {
|
|
461
|
+
const basis = computeSmaSeries(data, length, source);
|
|
462
|
+
if (band === "basis") return basis;
|
|
463
|
+
const dev = computeStdDevSeries(data, length, source);
|
|
464
|
+
return basis.map((value, idx) => {
|
|
465
|
+
const sigma = dev[idx];
|
|
466
|
+
if (value == null || sigma == null) return null;
|
|
467
|
+
return band === "upper" ? value + multiplier * sigma : value - multiplier * sigma;
|
|
468
|
+
});
|
|
469
|
+
};
|
|
412
470
|
var computeRsiSeries = (data, length) => {
|
|
413
471
|
const result = new Array(data.length).fill(null);
|
|
414
472
|
if (data.length < 2) return result;
|
|
@@ -535,6 +593,59 @@ var drawOverlaySeries = (ctx, renderContext, values, color, width) => {
|
|
|
535
593
|
}
|
|
536
594
|
ctx.restore();
|
|
537
595
|
};
|
|
596
|
+
var rangeOfSeries = (seriesList, startIndex, endIndex, skipIndex) => {
|
|
597
|
+
let min = Number.POSITIVE_INFINITY;
|
|
598
|
+
let max = Number.NEGATIVE_INFINITY;
|
|
599
|
+
for (const series of seriesList) {
|
|
600
|
+
for (let idx = startIndex; idx <= endIndex; idx += 1) {
|
|
601
|
+
if (idx === skipIndex) continue;
|
|
602
|
+
const value = series[idx];
|
|
603
|
+
if (value == null || !Number.isFinite(value)) continue;
|
|
604
|
+
if (value < min) min = value;
|
|
605
|
+
if (value > max) max = value;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
return min <= max ? { min, max } : null;
|
|
609
|
+
};
|
|
610
|
+
var maAutoscaleRange = (cacheKeyPrefix, compute, defaultLength) => (data, startIndex, endIndex, inputs, skipIndex) => {
|
|
611
|
+
const length = clampIndicatorLength(inputs.length, defaultLength);
|
|
612
|
+
const source = inputs.source ?? "close";
|
|
613
|
+
const series = withCachedSeries(`${cacheKeyPrefix}|${length}|${source}`, data, () => compute(data, length, source));
|
|
614
|
+
return rangeOfSeries([series], startIndex, endIndex, skipIndex);
|
|
615
|
+
};
|
|
616
|
+
var fillBetweenSeries = (ctx, renderContext, upper, lower, color, opacity) => {
|
|
617
|
+
if (!renderContext.yFromPrice || opacity <= 0) return;
|
|
618
|
+
const yFromPrice = renderContext.yFromPrice;
|
|
619
|
+
ctx.save();
|
|
620
|
+
ctx.globalAlpha = Math.min(1, Math.max(0, opacity));
|
|
621
|
+
ctx.fillStyle = color;
|
|
622
|
+
let runStart = -1;
|
|
623
|
+
const flushRun = (endIndexExclusive) => {
|
|
624
|
+
if (runStart < 0) return;
|
|
625
|
+
ctx.beginPath();
|
|
626
|
+
for (let i = runStart; i < endIndexExclusive; i += 1) {
|
|
627
|
+
const x = renderContext.xFromIndex(i);
|
|
628
|
+
const y = yFromPrice(upper[i]);
|
|
629
|
+
if (i === runStart) ctx.moveTo(x, y);
|
|
630
|
+
else ctx.lineTo(x, y);
|
|
631
|
+
}
|
|
632
|
+
for (let i = endIndexExclusive - 1; i >= runStart; i -= 1) {
|
|
633
|
+
ctx.lineTo(renderContext.xFromIndex(i), yFromPrice(lower[i]));
|
|
634
|
+
}
|
|
635
|
+
ctx.closePath();
|
|
636
|
+
ctx.fill();
|
|
637
|
+
runStart = -1;
|
|
638
|
+
};
|
|
639
|
+
for (let index = renderContext.startIndex; index <= renderContext.endIndex; index += 1) {
|
|
640
|
+
const up = upper[index];
|
|
641
|
+
const lo = lower[index];
|
|
642
|
+
const valid = Number.isFinite(up ?? Number.NaN) && Number.isFinite(lo ?? Number.NaN);
|
|
643
|
+
if (valid && runStart < 0) runStart = index;
|
|
644
|
+
if (!valid) flushRun(index);
|
|
645
|
+
}
|
|
646
|
+
flushRun(renderContext.endIndex + 1);
|
|
647
|
+
ctx.restore();
|
|
648
|
+
};
|
|
538
649
|
var drawSeparateSeries = (ctx, renderContext, values, color, width, minOverride, maxOverride, guideLines, options = {}) => {
|
|
539
650
|
const visible = [];
|
|
540
651
|
for (let index = renderContext.startIndex; index <= renderContext.endIndex; index += 1) {
|
|
@@ -719,7 +830,8 @@ var BUILTIN_SMA_INDICATOR = {
|
|
|
719
830
|
() => computeSmaSeries(renderContext.data, length, inputs.source ?? "close")
|
|
720
831
|
);
|
|
721
832
|
drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#60a5fa", Number(inputs.width) || 2);
|
|
722
|
-
}
|
|
833
|
+
},
|
|
834
|
+
getAutoscaleRange: maAutoscaleRange("sma", computeSmaSeries, 20)
|
|
723
835
|
};
|
|
724
836
|
var BUILTIN_EMA_INDICATOR = {
|
|
725
837
|
id: "ema",
|
|
@@ -734,7 +846,8 @@ var BUILTIN_EMA_INDICATOR = {
|
|
|
734
846
|
() => computeEmaSeries(renderContext.data, length, inputs.source ?? "close")
|
|
735
847
|
);
|
|
736
848
|
drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#f59e0b", Number(inputs.width) || 2);
|
|
737
|
-
}
|
|
849
|
+
},
|
|
850
|
+
getAutoscaleRange: maAutoscaleRange("ema", computeEmaSeries, 20)
|
|
738
851
|
};
|
|
739
852
|
var BUILTIN_WMA_INDICATOR = {
|
|
740
853
|
id: "wma",
|
|
@@ -749,7 +862,8 @@ var BUILTIN_WMA_INDICATOR = {
|
|
|
749
862
|
() => computeWmaSeries(renderContext.data, length, inputs.source ?? "close")
|
|
750
863
|
);
|
|
751
864
|
drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#a78bfa", Number(inputs.width) || 2);
|
|
752
|
-
}
|
|
865
|
+
},
|
|
866
|
+
getAutoscaleRange: maAutoscaleRange("wma", computeWmaSeries, 20)
|
|
753
867
|
};
|
|
754
868
|
var BUILTIN_VWMA_INDICATOR = {
|
|
755
869
|
id: "vwma",
|
|
@@ -764,7 +878,8 @@ var BUILTIN_VWMA_INDICATOR = {
|
|
|
764
878
|
() => computeVwmaSeries(renderContext.data, length, inputs.source ?? "close")
|
|
765
879
|
);
|
|
766
880
|
drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#ef4444", Number(inputs.width) || 2);
|
|
767
|
-
}
|
|
881
|
+
},
|
|
882
|
+
getAutoscaleRange: maAutoscaleRange("vwma", computeVwmaSeries, 20)
|
|
768
883
|
};
|
|
769
884
|
var BUILTIN_RMA_INDICATOR = {
|
|
770
885
|
id: "rma",
|
|
@@ -779,7 +894,8 @@ var BUILTIN_RMA_INDICATOR = {
|
|
|
779
894
|
() => computeRmaSeries(renderContext.data, length, inputs.source ?? "close")
|
|
780
895
|
);
|
|
781
896
|
drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#22c55e", Number(inputs.width) || 2);
|
|
782
|
-
}
|
|
897
|
+
},
|
|
898
|
+
getAutoscaleRange: maAutoscaleRange("rma", computeRmaSeries, 14)
|
|
783
899
|
};
|
|
784
900
|
var BUILTIN_HMA_INDICATOR = {
|
|
785
901
|
id: "hma",
|
|
@@ -794,6 +910,127 @@ var BUILTIN_HMA_INDICATOR = {
|
|
|
794
910
|
() => computeHmaSeries(renderContext.data, length, inputs.source ?? "close")
|
|
795
911
|
);
|
|
796
912
|
drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#14b8a6", Number(inputs.width) || 2);
|
|
913
|
+
},
|
|
914
|
+
getAutoscaleRange: maAutoscaleRange("hma", computeHmaSeries, 21)
|
|
915
|
+
};
|
|
916
|
+
var BUILTIN_VWAP_INDICATOR = {
|
|
917
|
+
id: "vwap",
|
|
918
|
+
name: "VWAP",
|
|
919
|
+
pane: "overlay",
|
|
920
|
+
defaultInputs: {
|
|
921
|
+
color: "#f59e0b",
|
|
922
|
+
width: 2,
|
|
923
|
+
showBands: false,
|
|
924
|
+
bandMultiplier: 1,
|
|
925
|
+
bandColor: "#94a3b8",
|
|
926
|
+
bandFillOpacity: 0.06
|
|
927
|
+
},
|
|
928
|
+
draw: (ctx, renderContext, inputs) => {
|
|
929
|
+
const vwap = withCachedSeries(
|
|
930
|
+
"vwap|line",
|
|
931
|
+
renderContext.data,
|
|
932
|
+
() => computeVwapSeries(renderContext.data, "vwap")
|
|
933
|
+
);
|
|
934
|
+
if (inputs.showBands) {
|
|
935
|
+
const sigma = withCachedSeries(
|
|
936
|
+
"vwap|sigma",
|
|
937
|
+
renderContext.data,
|
|
938
|
+
() => computeVwapSeries(renderContext.data, "sigma")
|
|
939
|
+
);
|
|
940
|
+
const multiplier = Math.max(0.1, Number(inputs.bandMultiplier) || 1);
|
|
941
|
+
const upper = vwap.map((value, idx) => {
|
|
942
|
+
const s = sigma[idx];
|
|
943
|
+
return value == null || s == null ? null : value + multiplier * s;
|
|
944
|
+
});
|
|
945
|
+
const lower = vwap.map((value, idx) => {
|
|
946
|
+
const s = sigma[idx];
|
|
947
|
+
return value == null || s == null ? null : value - multiplier * s;
|
|
948
|
+
});
|
|
949
|
+
const bandColor = inputs.bandColor ?? "#94a3b8";
|
|
950
|
+
fillBetweenSeries(ctx, renderContext, upper, lower, bandColor, Number(inputs.bandFillOpacity) || 0.06);
|
|
951
|
+
drawOverlaySeries(ctx, renderContext, upper, bandColor, 1);
|
|
952
|
+
drawOverlaySeries(ctx, renderContext, lower, bandColor, 1);
|
|
953
|
+
}
|
|
954
|
+
drawOverlaySeries(ctx, renderContext, vwap, inputs.color ?? "#f59e0b", Number(inputs.width) || 2);
|
|
955
|
+
},
|
|
956
|
+
getAutoscaleRange: (data, startIndex, endIndex, inputs, skipIndex) => {
|
|
957
|
+
const vwap = withCachedSeries("vwap|line", data, () => computeVwapSeries(data, "vwap"));
|
|
958
|
+
if (!inputs.showBands) {
|
|
959
|
+
return rangeOfSeries([vwap], startIndex, endIndex, skipIndex);
|
|
960
|
+
}
|
|
961
|
+
const sigma = withCachedSeries("vwap|sigma", data, () => computeVwapSeries(data, "sigma"));
|
|
962
|
+
const multiplier = Math.max(0.1, Number(inputs.bandMultiplier) || 1);
|
|
963
|
+
let min = Number.POSITIVE_INFINITY;
|
|
964
|
+
let max = Number.NEGATIVE_INFINITY;
|
|
965
|
+
for (let idx = startIndex; idx <= endIndex; idx += 1) {
|
|
966
|
+
if (idx === skipIndex) continue;
|
|
967
|
+
const value = vwap[idx];
|
|
968
|
+
const s = sigma[idx];
|
|
969
|
+
if (value == null || s == null) continue;
|
|
970
|
+
const lo = value - multiplier * s;
|
|
971
|
+
const hi = value + multiplier * s;
|
|
972
|
+
if (lo < min) min = lo;
|
|
973
|
+
if (hi > max) max = hi;
|
|
974
|
+
}
|
|
975
|
+
return min <= max ? { min, max } : null;
|
|
976
|
+
}
|
|
977
|
+
};
|
|
978
|
+
var BUILTIN_BOLLINGER_INDICATOR = {
|
|
979
|
+
id: "bollinger",
|
|
980
|
+
name: "BB",
|
|
981
|
+
pane: "overlay",
|
|
982
|
+
// Basis is pink so it stays distinguishable from the (orange) VWAP line
|
|
983
|
+
// when both overlays are active.
|
|
984
|
+
defaultInputs: {
|
|
985
|
+
length: 20,
|
|
986
|
+
multiplier: 2,
|
|
987
|
+
source: "close",
|
|
988
|
+
basisColor: "#ec4899",
|
|
989
|
+
bandColor: "#3b82f6",
|
|
990
|
+
width: 1.5,
|
|
991
|
+
fillOpacity: 0.05
|
|
992
|
+
},
|
|
993
|
+
draw: (ctx, renderContext, inputs) => {
|
|
994
|
+
const length = clampIndicatorLength(inputs.length, 20);
|
|
995
|
+
const multiplier = Math.max(0.1, Number(inputs.multiplier) || 2);
|
|
996
|
+
const source = inputs.source ?? "close";
|
|
997
|
+
const basis = withCachedSeries(
|
|
998
|
+
`bb|basis|${length}|${source}`,
|
|
999
|
+
renderContext.data,
|
|
1000
|
+
() => computeBollingerSeries(renderContext.data, length, multiplier, source, "basis")
|
|
1001
|
+
);
|
|
1002
|
+
const upper = withCachedSeries(
|
|
1003
|
+
`bb|upper|${length}|${multiplier}|${source}`,
|
|
1004
|
+
renderContext.data,
|
|
1005
|
+
() => computeBollingerSeries(renderContext.data, length, multiplier, source, "upper")
|
|
1006
|
+
);
|
|
1007
|
+
const lower = withCachedSeries(
|
|
1008
|
+
`bb|lower|${length}|${multiplier}|${source}`,
|
|
1009
|
+
renderContext.data,
|
|
1010
|
+
() => computeBollingerSeries(renderContext.data, length, multiplier, source, "lower")
|
|
1011
|
+
);
|
|
1012
|
+
const bandColor = inputs.bandColor ?? "#3b82f6";
|
|
1013
|
+
const width = Number(inputs.width) || 1.5;
|
|
1014
|
+
fillBetweenSeries(ctx, renderContext, upper, lower, bandColor, Number(inputs.fillOpacity) || 0.05);
|
|
1015
|
+
drawOverlaySeries(ctx, renderContext, upper, bandColor, width);
|
|
1016
|
+
drawOverlaySeries(ctx, renderContext, lower, bandColor, width);
|
|
1017
|
+
drawOverlaySeries(ctx, renderContext, basis, inputs.basisColor ?? "#ec4899", width);
|
|
1018
|
+
},
|
|
1019
|
+
getAutoscaleRange: (data, startIndex, endIndex, inputs, skipIndex) => {
|
|
1020
|
+
const length = clampIndicatorLength(inputs.length, 20);
|
|
1021
|
+
const multiplier = Math.max(0.1, Number(inputs.multiplier) || 2);
|
|
1022
|
+
const source = inputs.source ?? "close";
|
|
1023
|
+
const upper = withCachedSeries(
|
|
1024
|
+
`bb|upper|${length}|${multiplier}|${source}`,
|
|
1025
|
+
data,
|
|
1026
|
+
() => computeBollingerSeries(data, length, multiplier, source, "upper")
|
|
1027
|
+
);
|
|
1028
|
+
const lower = withCachedSeries(
|
|
1029
|
+
`bb|lower|${length}|${multiplier}|${source}`,
|
|
1030
|
+
data,
|
|
1031
|
+
() => computeBollingerSeries(data, length, multiplier, source, "lower")
|
|
1032
|
+
);
|
|
1033
|
+
return rangeOfSeries([upper, lower], startIndex, endIndex, skipIndex);
|
|
797
1034
|
}
|
|
798
1035
|
};
|
|
799
1036
|
var BUILTIN_STDDEV_INDICATOR = {
|
|
@@ -879,11 +1116,27 @@ var BUILTIN_INDICATORS = [
|
|
|
879
1116
|
BUILTIN_VWMA_INDICATOR,
|
|
880
1117
|
BUILTIN_RMA_INDICATOR,
|
|
881
1118
|
BUILTIN_HMA_INDICATOR,
|
|
1119
|
+
BUILTIN_VWAP_INDICATOR,
|
|
1120
|
+
BUILTIN_BOLLINGER_INDICATOR,
|
|
882
1121
|
BUILTIN_STDDEV_INDICATOR,
|
|
883
1122
|
BUILTIN_ATR_INDICATOR
|
|
884
1123
|
];
|
|
885
1124
|
function createChart(element, options = {}) {
|
|
886
1125
|
let mergedOptions = mergeChartOptions(DEFAULT_OPTIONS, options);
|
|
1126
|
+
let resolvedAxis = { ...DEFAULT_AXIS_OPTIONS, ...mergedOptions.axis ?? {} };
|
|
1127
|
+
let resolvedXAxis = { ...DEFAULT_AXIS_OPTIONS, ...mergedOptions.xAxis ?? {} };
|
|
1128
|
+
let resolvedYAxis = { ...DEFAULT_AXIS_OPTIONS, ...mergedOptions.yAxis ?? {} };
|
|
1129
|
+
let resolvedLabels = { ...DEFAULT_LABELS_OPTIONS, ...mergedOptions.labels ?? {} };
|
|
1130
|
+
let resolvedGrid = { ...DEFAULT_GRID_OPTIONS, ...mergedOptions.grid ?? {} };
|
|
1131
|
+
let resolvedCrosshair = { ...DEFAULT_CROSSHAIR_OPTIONS, ...mergedOptions.crosshair ?? {} };
|
|
1132
|
+
const refreshResolvedOptions = () => {
|
|
1133
|
+
resolvedAxis = { ...DEFAULT_AXIS_OPTIONS, ...mergedOptions.axis ?? {} };
|
|
1134
|
+
resolvedXAxis = { ...DEFAULT_AXIS_OPTIONS, ...mergedOptions.xAxis ?? {} };
|
|
1135
|
+
resolvedYAxis = { ...DEFAULT_AXIS_OPTIONS, ...mergedOptions.yAxis ?? {} };
|
|
1136
|
+
resolvedLabels = { ...DEFAULT_LABELS_OPTIONS, ...mergedOptions.labels ?? {} };
|
|
1137
|
+
resolvedGrid = { ...DEFAULT_GRID_OPTIONS, ...mergedOptions.grid ?? {} };
|
|
1138
|
+
resolvedCrosshair = { ...DEFAULT_CROSSHAIR_OPTIONS, ...mergedOptions.crosshair ?? {} };
|
|
1139
|
+
};
|
|
887
1140
|
let width = mergedOptions.width;
|
|
888
1141
|
let height = mergedOptions.height;
|
|
889
1142
|
let data = [];
|
|
@@ -920,8 +1173,11 @@ function createChart(element, options = {}) {
|
|
|
920
1173
|
pane: indicator.pane ?? plugin?.pane ?? "overlay",
|
|
921
1174
|
...indicator.paneHeightRatio === void 0 ? {} : { paneHeightRatio: indicator.paneHeightRatio },
|
|
922
1175
|
zIndex: Math.round(Number(indicator.zIndex) || 0),
|
|
923
|
-
|
|
924
|
-
|
|
1176
|
+
// Overlay series participate in autoscale by default (lightweight-charts
|
|
1177
|
+
// semantics): bands and MAs expand the price range instead of clipping
|
|
1178
|
+
// at the chart edge. Hosts can still opt out per indicator.
|
|
1179
|
+
excludeFromAutoscale: indicator.excludeFromAutoscale ?? false,
|
|
1180
|
+
overlayScaleWeight: indicator.overlayScaleWeight === void 0 ? 1 : Math.min(1, Math.max(0, Number(indicator.overlayScaleWeight) || 0)),
|
|
925
1181
|
inputs: {
|
|
926
1182
|
...defaults,
|
|
927
1183
|
...indicator.inputs ?? {}
|
|
@@ -1123,6 +1379,9 @@ function createChart(element, options = {}) {
|
|
|
1123
1379
|
canvas.setAttribute("draggable", "false");
|
|
1124
1380
|
element.innerHTML = "";
|
|
1125
1381
|
element.appendChild(canvas);
|
|
1382
|
+
const baseCanvas = document.createElement("canvas");
|
|
1383
|
+
const baseCtx = baseCanvas.getContext("2d");
|
|
1384
|
+
let overlayLayout = null;
|
|
1126
1385
|
const margin = { top: 16, right: 72, bottom: 26, left: 12 };
|
|
1127
1386
|
let cachedRightMargin = margin.right;
|
|
1128
1387
|
const minVisibleBars = Math.max(1, Math.floor(mergedOptions.minVisibleBars));
|
|
@@ -1162,6 +1421,20 @@ function createChart(element, options = {}) {
|
|
|
1162
1421
|
const clamp = (value, min, max) => {
|
|
1163
1422
|
return Math.min(max, Math.max(min, value));
|
|
1164
1423
|
};
|
|
1424
|
+
const textWidthCache = /* @__PURE__ */ new Map();
|
|
1425
|
+
const measureTextWidth = (text) => {
|
|
1426
|
+
const key = `${ctx.font}\0${text}`;
|
|
1427
|
+
const cached = textWidthCache.get(key);
|
|
1428
|
+
if (cached !== void 0) {
|
|
1429
|
+
return cached;
|
|
1430
|
+
}
|
|
1431
|
+
const width2 = ctx.measureText(text).width;
|
|
1432
|
+
if (textWidthCache.size >= 4096) {
|
|
1433
|
+
textWidthCache.clear();
|
|
1434
|
+
}
|
|
1435
|
+
textWidthCache.set(key, width2);
|
|
1436
|
+
return width2;
|
|
1437
|
+
};
|
|
1165
1438
|
const resetLabelSlots = (enabled) => {
|
|
1166
1439
|
noOverlappingLineLabels = enabled;
|
|
1167
1440
|
rightAxisLabelSlots = [];
|
|
@@ -1364,7 +1637,7 @@ function createChart(element, options = {}) {
|
|
|
1364
1637
|
return `${integerPart}${decimalPart}`;
|
|
1365
1638
|
};
|
|
1366
1639
|
const getMeasuredLabelWidth = (text, paddingX) => {
|
|
1367
|
-
return Math.ceil(
|
|
1640
|
+
return Math.ceil(measureTextWidth(text)) + paddingX * 2;
|
|
1368
1641
|
};
|
|
1369
1642
|
const getStabilizedNumericLabelWidth = (text, paddingX) => {
|
|
1370
1643
|
const measured = getMeasuredLabelWidth(text, paddingX);
|
|
@@ -1605,11 +1878,11 @@ function createChart(element, options = {}) {
|
|
|
1605
1878
|
return;
|
|
1606
1879
|
}
|
|
1607
1880
|
const labelText = mergedLine.label ?? formatPrice(mergedLine.price);
|
|
1608
|
-
const axis =
|
|
1881
|
+
const axis = resolvedAxis;
|
|
1609
1882
|
ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
|
|
1610
1883
|
const labelPaddingX = 8;
|
|
1611
1884
|
const labelHeight = 20;
|
|
1612
|
-
const measuredLabelWidth = mergedLine.label === void 0 ? getPriceLabelWidth(labelText, labelPaddingX) : Math.ceil(
|
|
1885
|
+
const measuredLabelWidth = mergedLine.label === void 0 ? getPriceLabelWidth(labelText, labelPaddingX) : Math.ceil(measureTextWidth(labelText)) + labelPaddingX * 2;
|
|
1613
1886
|
const labelWidth = getRightAxisLabelWidth(chartRight, measuredLabelWidth);
|
|
1614
1887
|
const labelX = getRightAxisLabelX(chartRight);
|
|
1615
1888
|
const labelY = placeRightAxisLabel(lineY - labelHeight / 2, labelHeight, chartTop, chartBottom - labelHeight);
|
|
@@ -1704,7 +1977,7 @@ function createChart(element, options = {}) {
|
|
|
1704
1977
|
const closeAction = mergedLine.type === "market" ? "close" : "cancel";
|
|
1705
1978
|
const showCloseButton = mergedLine.showCloseButton;
|
|
1706
1979
|
const actionButtonText = mergedLine.actionButtonText;
|
|
1707
|
-
const axis =
|
|
1980
|
+
const axis = resolvedAxis;
|
|
1708
1981
|
ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
|
|
1709
1982
|
const legacyButton = actionButtonText ? [
|
|
1710
1983
|
{
|
|
@@ -1726,7 +1999,7 @@ function createChart(element, options = {}) {
|
|
|
1726
1999
|
const actionButtonMetrics = actionButtons.map((button) => {
|
|
1727
2000
|
const paddingX = Math.max(2, button.paddingX ?? mergedLine.actionButtonPaddingX);
|
|
1728
2001
|
const minWidth = Math.max(16, button.minWidth ?? mergedLine.actionButtonMinWidth);
|
|
1729
|
-
const width2 = Math.max(minWidth, Math.ceil(
|
|
2002
|
+
const width2 = Math.max(minWidth, Math.ceil(measureTextWidth(button.text)) + paddingX * 2);
|
|
1730
2003
|
return { button, width: width2 };
|
|
1731
2004
|
});
|
|
1732
2005
|
const actionButtonInnerGap = actionButtonMetrics.length > 1 ? Math.max(0, mergedLine.actionButtonsInnerGap) : 0;
|
|
@@ -1734,8 +2007,8 @@ function createChart(element, options = {}) {
|
|
|
1734
2007
|
const actionButtonsGap = actionButtonMetrics.length > 0 ? Math.max(0, mergedLine.actionButtonsGroupGap) : 0;
|
|
1735
2008
|
const segmentPaddingX = 8;
|
|
1736
2009
|
const labelHeight = 22;
|
|
1737
|
-
const qtyWidth = qtyText ? Math.ceil(
|
|
1738
|
-
const centerMeasuredWidth = Math.ceil(
|
|
2010
|
+
const qtyWidth = qtyText ? Math.ceil(measureTextWidth(qtyText)) + segmentPaddingX * 2 : 0;
|
|
2011
|
+
const centerMeasuredWidth = Math.ceil(measureTextWidth(centerText)) + segmentPaddingX * 2;
|
|
1739
2012
|
const centerWidth = mergedLine.id === void 0 ? centerMeasuredWidth : Math.max(centerMeasuredWidth, orderWidgetWidthById.get(mergedLine.id) ?? 0);
|
|
1740
2013
|
if (mergedLine.id) {
|
|
1741
2014
|
orderWidgetWidthById.set(mergedLine.id, centerWidth);
|
|
@@ -1743,7 +2016,7 @@ function createChart(element, options = {}) {
|
|
|
1743
2016
|
const closeWidth = showCloseButton ? 24 : 0;
|
|
1744
2017
|
const mainWidgetWidth = qtyWidth + centerWidth + closeWidth;
|
|
1745
2018
|
const totalWidth = mainWidgetWidth + actionButtonsGap + actionButtonsTotalWidth;
|
|
1746
|
-
const crosshair =
|
|
2019
|
+
const crosshair = resolvedCrosshair;
|
|
1747
2020
|
const crosshairActionInset = crosshair.visible && crosshair.showPriceLabel && crosshair.showPriceActionButton ? Math.max(
|
|
1748
2021
|
0,
|
|
1749
2022
|
Math.round(crosshair.priceActionButtonGap) + (clamp(Math.round(crosshair.priceActionButtonSize), 12, Math.max(12, labelHeight - 4)) + 10) - 4
|
|
@@ -1924,20 +2197,29 @@ function createChart(element, options = {}) {
|
|
|
1924
2197
|
}
|
|
1925
2198
|
}
|
|
1926
2199
|
crosshairPoint = point;
|
|
1927
|
-
scheduleDraw();
|
|
2200
|
+
scheduleDraw({ overlayOnly: true });
|
|
1928
2201
|
};
|
|
1929
2202
|
let drawRafId = null;
|
|
1930
2203
|
let pendingDrawUpdateAutoScale = false;
|
|
2204
|
+
let pendingDrawOverlayOnly = true;
|
|
1931
2205
|
const scheduleDraw = (options2 = {}) => {
|
|
1932
|
-
|
|
2206
|
+
const overlayOnly = options2.overlayOnly ?? false;
|
|
2207
|
+
pendingDrawOverlayOnly = pendingDrawOverlayOnly && overlayOnly;
|
|
2208
|
+
pendingDrawUpdateAutoScale = pendingDrawUpdateAutoScale || !overlayOnly && (options2.updateAutoScale ?? true);
|
|
1933
2209
|
if (drawRafId !== null) {
|
|
1934
2210
|
return;
|
|
1935
2211
|
}
|
|
1936
2212
|
drawRafId = requestAnimationFrame(() => {
|
|
1937
2213
|
drawRafId = null;
|
|
1938
2214
|
const updateAutoScale = pendingDrawUpdateAutoScale;
|
|
2215
|
+
const flushOverlayOnly = pendingDrawOverlayOnly;
|
|
1939
2216
|
pendingDrawUpdateAutoScale = false;
|
|
1940
|
-
|
|
2217
|
+
pendingDrawOverlayOnly = true;
|
|
2218
|
+
if (flushOverlayOnly) {
|
|
2219
|
+
drawOverlayOnly();
|
|
2220
|
+
} else {
|
|
2221
|
+
draw({ updateAutoScale });
|
|
2222
|
+
}
|
|
1941
2223
|
});
|
|
1942
2224
|
};
|
|
1943
2225
|
const draw = (options2 = {}) => {
|
|
@@ -1951,15 +2233,15 @@ function createChart(element, options = {}) {
|
|
|
1951
2233
|
canvas.width = Math.floor(width * pixelRatio);
|
|
1952
2234
|
canvas.height = Math.floor(height * pixelRatio);
|
|
1953
2235
|
ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
|
|
1954
|
-
const axis =
|
|
1955
|
-
const xAxis =
|
|
1956
|
-
const yAxis =
|
|
2236
|
+
const axis = resolvedAxis;
|
|
2237
|
+
const xAxis = resolvedXAxis;
|
|
2238
|
+
const yAxis = resolvedYAxis;
|
|
1957
2239
|
const xAxisFontSize = Math.max(8, xAxis.fontSize);
|
|
1958
2240
|
const yAxisFontSize = Math.max(8, yAxis.fontSize);
|
|
1959
2241
|
ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
|
|
1960
2242
|
ctx.fillStyle = mergedOptions.backgroundColor;
|
|
1961
2243
|
ctx.fillRect(0, 0, width, height);
|
|
1962
|
-
const labels =
|
|
2244
|
+
const labels = resolvedLabels;
|
|
1963
2245
|
const estimateRightMargin = () => {
|
|
1964
2246
|
const paddingX = Math.max(4, labels.labelPaddingX);
|
|
1965
2247
|
let required = margin.right - 1;
|
|
@@ -2072,6 +2354,7 @@ function createChart(element, options = {}) {
|
|
|
2072
2354
|
yMax: 0
|
|
2073
2355
|
};
|
|
2074
2356
|
drawText("Load OHLC data to render chart", chartLeft + 10, chartTop + 20, "left", "middle", "#64748b");
|
|
2357
|
+
overlayLayout = null;
|
|
2075
2358
|
return;
|
|
2076
2359
|
}
|
|
2077
2360
|
clampXViewport();
|
|
@@ -2079,62 +2362,39 @@ function createChart(element, options = {}) {
|
|
|
2079
2362
|
const xEnd = xStart + xSpan;
|
|
2080
2363
|
const startIndex = Math.max(0, Math.floor(xStart));
|
|
2081
2364
|
const endIndex = Math.min(data.length - 1, Math.ceil(xEnd) - 1);
|
|
2082
|
-
const
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
const fallback = data.slice(fallbackStart, latestIndex);
|
|
2093
|
-
if (fallback.length > 0) {
|
|
2094
|
-
priceSource = fallback;
|
|
2095
|
-
}
|
|
2365
|
+
const skipLatestIndex = mergedOptions.autoScaleIgnoreLatestCandle && data.length > 1 ? data.length - 1 : -1;
|
|
2366
|
+
const scanCandleRange = (from, to) => {
|
|
2367
|
+
let min = Number.POSITIVE_INFINITY;
|
|
2368
|
+
let max = Number.NEGATIVE_INFINITY;
|
|
2369
|
+
for (let idx = from; idx <= to; idx += 1) {
|
|
2370
|
+
if (idx === skipLatestIndex) continue;
|
|
2371
|
+
const point = data[idx];
|
|
2372
|
+
if (!point) continue;
|
|
2373
|
+
if (point.l < min) min = point.l;
|
|
2374
|
+
if (point.h > max) max = point.h;
|
|
2096
2375
|
}
|
|
2376
|
+
return min <= max ? { min, max } : null;
|
|
2377
|
+
};
|
|
2378
|
+
let candleRange = scanCandleRange(startIndex, endIndex);
|
|
2379
|
+
if (!candleRange) {
|
|
2380
|
+
const latestIndex = data.length - 1;
|
|
2381
|
+
candleRange = scanCandleRange(Math.max(0, latestIndex - 120), latestIndex - 1) ?? scanCandleRange(0, latestIndex) ?? { min: data[latestIndex].l, max: data[latestIndex].h };
|
|
2097
2382
|
}
|
|
2098
|
-
let minPrice =
|
|
2099
|
-
let maxPrice =
|
|
2383
|
+
let minPrice = candleRange.min;
|
|
2384
|
+
let maxPrice = candleRange.max;
|
|
2100
2385
|
if (overlayIndicatorsForScale.length > 0) {
|
|
2101
|
-
for (const { indicator } of overlayIndicatorsForScale) {
|
|
2386
|
+
for (const { indicator, plugin } of overlayIndicatorsForScale) {
|
|
2102
2387
|
if (indicator.excludeFromAutoscale) {
|
|
2103
2388
|
continue;
|
|
2104
2389
|
}
|
|
2105
|
-
const
|
|
2106
|
-
|
|
2107
|
-
const source = inputs.source ?? "close";
|
|
2108
|
-
const length = clampIndicatorLength(inputs.length ?? 14, 14);
|
|
2109
|
-
let series = null;
|
|
2110
|
-
if (type === "sma") series = withCachedSeries(`sma|${length}|${source}`, data, () => computeSmaSeries(data, length, source));
|
|
2111
|
-
if (type === "ema") series = withCachedSeries(`ema|${length}|${source}`, data, () => computeEmaSeries(data, length, source));
|
|
2112
|
-
if (type === "wma") series = withCachedSeries(`wma|${length}|${source}`, data, () => computeWmaSeries(data, length, source));
|
|
2113
|
-
if (type === "vwma") series = withCachedSeries(`vwma|${length}|${source}`, data, () => computeVwmaSeries(data, length, source));
|
|
2114
|
-
if (type === "rma") series = withCachedSeries(`rma|${length}|${source}`, data, () => computeRmaSeries(data, length, source));
|
|
2115
|
-
if (type === "hma") series = withCachedSeries(`hma|${length}|${source}`, data, () => computeHmaSeries(data, length, source));
|
|
2116
|
-
if (!series) {
|
|
2390
|
+
const range = plugin.getAutoscaleRange?.(data, startIndex, endIndex, indicator.inputs, skipLatestIndex);
|
|
2391
|
+
if (!range || !Number.isFinite(range.min) || !Number.isFinite(range.max)) {
|
|
2117
2392
|
continue;
|
|
2118
2393
|
}
|
|
2119
|
-
const visibleValues = [];
|
|
2120
|
-
for (let idx = startIndex; idx <= endIndex; idx += 1) {
|
|
2121
|
-
if (mergedOptions.autoScaleIgnoreLatestCandle && data.length > 1 && idx === data.length - 1) {
|
|
2122
|
-
continue;
|
|
2123
|
-
}
|
|
2124
|
-
const value = series[idx];
|
|
2125
|
-
if (Number.isFinite(value ?? Number.NaN)) {
|
|
2126
|
-
visibleValues.push(value);
|
|
2127
|
-
}
|
|
2128
|
-
}
|
|
2129
|
-
if (visibleValues.length === 0) {
|
|
2130
|
-
continue;
|
|
2131
|
-
}
|
|
2132
|
-
const seriesMin = Math.min(...visibleValues);
|
|
2133
|
-
const seriesMax = Math.max(...visibleValues);
|
|
2134
2394
|
const weight = Math.min(1, Math.max(0, indicator.overlayScaleWeight));
|
|
2135
2395
|
const currentMid = (minPrice + maxPrice) / 2;
|
|
2136
|
-
const weightedMin = currentMid + (
|
|
2137
|
-
const weightedMax = currentMid + (
|
|
2396
|
+
const weightedMin = currentMid + (range.min - currentMid) * weight;
|
|
2397
|
+
const weightedMax = currentMid + (range.max - currentMid) * weight;
|
|
2138
2398
|
minPrice = Math.min(minPrice, weightedMin);
|
|
2139
2399
|
maxPrice = Math.max(maxPrice, weightedMax);
|
|
2140
2400
|
}
|
|
@@ -2209,7 +2469,7 @@ function createChart(element, options = {}) {
|
|
|
2209
2469
|
ctx.save();
|
|
2210
2470
|
const prevFont = ctx.font;
|
|
2211
2471
|
ctx.font = `500 12px ${mergedOptions.fontFamily}`;
|
|
2212
|
-
const textWidth =
|
|
2472
|
+
const textWidth = measureTextWidth(labelText);
|
|
2213
2473
|
const paddingX = 6;
|
|
2214
2474
|
const bgWidth = textWidth + paddingX * 2;
|
|
2215
2475
|
const bgHeight = 18;
|
|
@@ -2375,7 +2635,7 @@ function createChart(element, options = {}) {
|
|
|
2375
2635
|
ctx.font = `500 11px ${mergedOptions.fontFamily}`;
|
|
2376
2636
|
levelLines.forEach((level, index) => {
|
|
2377
2637
|
const labelText = `${level.ratio} (${formatPrice(level.price)})`;
|
|
2378
|
-
const textWidth =
|
|
2638
|
+
const textWidth = measureTextWidth(labelText);
|
|
2379
2639
|
const padding = 4;
|
|
2380
2640
|
const bgX = lineLeft + 4;
|
|
2381
2641
|
const bgY = level.y - 9;
|
|
@@ -2448,7 +2708,7 @@ function createChart(element, options = {}) {
|
|
|
2448
2708
|
ctx.font = `500 11px ${mergedOptions.fontFamily}`;
|
|
2449
2709
|
levelLines.forEach((level, index) => {
|
|
2450
2710
|
const labelText = `${level.ratio} (${formatPrice(level.price)})`;
|
|
2451
|
-
const textWidth =
|
|
2711
|
+
const textWidth = measureTextWidth(labelText);
|
|
2452
2712
|
const padding = 4;
|
|
2453
2713
|
const bgX = chartLeft + 4;
|
|
2454
2714
|
const bgY = level.y - 9;
|
|
@@ -2591,8 +2851,8 @@ function createChart(element, options = {}) {
|
|
|
2591
2851
|
const padding = 6;
|
|
2592
2852
|
const lineH = 14;
|
|
2593
2853
|
const isDigit = (ch) => ch >= "0" && ch <= "9";
|
|
2594
|
-
const digitW =
|
|
2595
|
-
const charAdvance = (ch) => isDigit(ch) ? digitW :
|
|
2854
|
+
const digitW = measureTextWidth("0");
|
|
2855
|
+
const charAdvance = (ch) => isDigit(ch) ? digitW : measureTextWidth(ch);
|
|
2596
2856
|
const lineWidth = (line) => {
|
|
2597
2857
|
let w = 0;
|
|
2598
2858
|
for (const ch of line) w += charAdvance(ch);
|
|
@@ -2615,11 +2875,11 @@ function createChart(element, options = {}) {
|
|
|
2615
2875
|
const y = startY + lineIndex * lineH;
|
|
2616
2876
|
for (const ch of line) {
|
|
2617
2877
|
if (isDigit(ch)) {
|
|
2618
|
-
ctx.fillText(ch, x + (digitW -
|
|
2878
|
+
ctx.fillText(ch, x + (digitW - measureTextWidth(ch)) / 2, y);
|
|
2619
2879
|
x += digitW;
|
|
2620
2880
|
} else {
|
|
2621
2881
|
ctx.fillText(ch, x, y);
|
|
2622
|
-
x +=
|
|
2882
|
+
x += measureTextWidth(ch);
|
|
2623
2883
|
}
|
|
2624
2884
|
}
|
|
2625
2885
|
});
|
|
@@ -2731,15 +2991,31 @@ function createChart(element, options = {}) {
|
|
|
2731
2991
|
const pct = diff / base * 100;
|
|
2732
2992
|
const ticks = tick > 0 ? Math.round(diff / tick) : 0;
|
|
2733
2993
|
const signed = (value, text) => `${value < 0 ? "\u2212" : value > 0 ? "+" : ""}${text}`;
|
|
2734
|
-
const
|
|
2994
|
+
const labelLines = [
|
|
2995
|
+
tick > 0 ? `${signed(diff, formatPrice(Math.abs(diff)))} (${signed(pct, `${Math.abs(pct).toFixed(2)}%`)}) ${signed(ticks, String(Math.abs(ticks)))}` : `${signed(diff, formatPrice(Math.abs(diff)))} (${signed(pct, `${Math.abs(pct).toFixed(2)}%`)})`
|
|
2996
|
+
];
|
|
2997
|
+
const barSpan = Math.abs(Math.round(p1.index) - Math.round(p0.index));
|
|
2998
|
+
if (barSpan > 0) {
|
|
2999
|
+
const t0 = getTimeForIndex(Math.round(p0.index));
|
|
3000
|
+
const t1 = getTimeForIndex(Math.round(p1.index));
|
|
3001
|
+
const spanMs = t0 && t1 ? Math.abs(t1.getTime() - t0.getTime()) : 0;
|
|
3002
|
+
labelLines.push(spanMs > 0 ? `${barSpan} bars, ${formatDuration(spanMs)}` : `${barSpan} bars`);
|
|
3003
|
+
}
|
|
3004
|
+
const pointValue = Number(drawing.pointValue);
|
|
3005
|
+
if (Number.isFinite(pointValue) && pointValue > 0) {
|
|
3006
|
+
const dollars = diff * pointValue;
|
|
3007
|
+
const money = Math.abs(dollars).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
3008
|
+
labelLines.push(`${signed(dollars, `$${money}`)} / contract`);
|
|
3009
|
+
}
|
|
2735
3010
|
const prevFont = ctx.font;
|
|
2736
3011
|
ctx.font = `500 11px ${mergedOptions.fontFamily}`;
|
|
2737
3012
|
const padding = 6;
|
|
2738
|
-
const
|
|
3013
|
+
const lineHeight = 15;
|
|
3014
|
+
const textW = Math.max(...labelLines.map((line) => measureTextWidth(line)));
|
|
2739
3015
|
const pillW = textW + padding * 2;
|
|
2740
|
-
const pillH =
|
|
3016
|
+
const pillH = labelLines.length * lineHeight + padding;
|
|
2741
3017
|
const pillX = midX - pillW / 2;
|
|
2742
|
-
const pillY = botY + 6;
|
|
3018
|
+
const pillY = botY + 6 + pillH <= fullChartBottom - 2 ? botY + 6 : Math.max(chartTop + 2, topY - 6 - pillH);
|
|
2743
3019
|
ctx.fillStyle = mergedOptions.backgroundColor;
|
|
2744
3020
|
fillRoundedRect(pillX, pillY, pillW, pillH, 4);
|
|
2745
3021
|
ctx.save();
|
|
@@ -2750,7 +3026,9 @@ function createChart(element, options = {}) {
|
|
|
2750
3026
|
ctx.fillStyle = drawing.color;
|
|
2751
3027
|
ctx.textAlign = "center";
|
|
2752
3028
|
ctx.textBaseline = "middle";
|
|
2753
|
-
|
|
3029
|
+
labelLines.forEach((line, lineIndex) => {
|
|
3030
|
+
ctx.fillText(line, midX, pillY + padding / 2 + lineIndex * lineHeight + lineHeight / 2);
|
|
3031
|
+
});
|
|
2754
3032
|
ctx.font = prevFont;
|
|
2755
3033
|
if (drawing.label) {
|
|
2756
3034
|
drawDrawingLabel(drawing.label, midX, topY - 4, drawing.color);
|
|
@@ -2769,7 +3047,7 @@ function createChart(element, options = {}) {
|
|
|
2769
3047
|
ctx.textBaseline = "middle";
|
|
2770
3048
|
const lines = (drawing.label ?? "").split("\n");
|
|
2771
3049
|
const lineH = Math.round(fontSize * 1.35);
|
|
2772
|
-
const textW = Math.max(1, ...lines.map((line) =>
|
|
3050
|
+
const textW = Math.max(1, ...lines.map((line) => measureTextWidth(line)));
|
|
2773
3051
|
const padX = isNote ? 8 : 2;
|
|
2774
3052
|
const padY = isNote ? 6 : 1;
|
|
2775
3053
|
const blockW = textW + padX * 2;
|
|
@@ -2851,47 +3129,114 @@ function createChart(element, options = {}) {
|
|
|
2851
3129
|
Math.max(1, candleSpacing - 1),
|
|
2852
3130
|
Math.max(candleMinWidth, Math.floor(candleSpacing * candleBodyWidthRatio))
|
|
2853
3131
|
);
|
|
2854
|
-
const grid =
|
|
3132
|
+
const grid = resolvedGrid;
|
|
2855
3133
|
const gridOpacity = clamp(grid.opacity, 0, 1);
|
|
2856
3134
|
const yTickCountInput = grid.yTickCount ?? grid.horizontalTickCount;
|
|
2857
3135
|
const yTicks = Math.max(1, Math.floor(yTickCountInput));
|
|
2858
3136
|
const gridColor = grid.color ?? mergedOptions.gridColor;
|
|
3137
|
+
const priceTicks = [];
|
|
3138
|
+
{
|
|
3139
|
+
const targetStep = yRange / Math.max(1, yTicks);
|
|
3140
|
+
let step;
|
|
3141
|
+
if (targetStep > 0 && Number.isFinite(targetStep)) {
|
|
3142
|
+
const base = Math.pow(10, Math.floor(Math.log10(targetStep)));
|
|
3143
|
+
const frac = targetStep / base;
|
|
3144
|
+
step = (frac <= 1 ? 1 : frac <= 2 ? 2 : frac <= 2.5 ? 2.5 : frac <= 5 ? 5 : 10) * base;
|
|
3145
|
+
const instrumentTick = getConfiguredTickSize();
|
|
3146
|
+
if (instrumentTick > 0 && step < instrumentTick * 1e6) {
|
|
3147
|
+
step = Math.max(instrumentTick, Math.round(step / instrumentTick) * instrumentTick);
|
|
3148
|
+
}
|
|
3149
|
+
} else {
|
|
3150
|
+
step = 1;
|
|
3151
|
+
}
|
|
3152
|
+
const firstTick = Math.ceil(yMin / step) * step;
|
|
3153
|
+
for (let i = 0; ; i += 1) {
|
|
3154
|
+
const price = firstTick + i * step;
|
|
3155
|
+
if (price > yMax + step * 1e-6) break;
|
|
3156
|
+
priceTicks.push(price);
|
|
3157
|
+
if (priceTicks.length > 200) break;
|
|
3158
|
+
}
|
|
3159
|
+
}
|
|
2859
3160
|
if (grid.horizontalLines) {
|
|
2860
3161
|
ctx.save();
|
|
2861
3162
|
ctx.globalAlpha = gridOpacity;
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
3163
|
+
ctx.strokeStyle = gridColor;
|
|
3164
|
+
ctx.lineWidth = 1;
|
|
3165
|
+
ctx.beginPath();
|
|
3166
|
+
for (const price of priceTicks) {
|
|
2865
3167
|
const y = yFromPrice(price);
|
|
2866
|
-
ctx.strokeStyle = gridColor;
|
|
2867
|
-
ctx.lineWidth = 1;
|
|
2868
|
-
ctx.beginPath();
|
|
2869
3168
|
ctx.moveTo(crisp(chartLeft), crisp(y));
|
|
2870
3169
|
ctx.lineTo(crisp(chartRight), crisp(y));
|
|
2871
|
-
ctx.stroke();
|
|
2872
3170
|
}
|
|
3171
|
+
ctx.stroke();
|
|
2873
3172
|
ctx.restore();
|
|
2874
3173
|
}
|
|
2875
|
-
const minLabelSpacingPx = Math.max(
|
|
2876
|
-
const
|
|
2877
|
-
const xTickCountInput = grid.xTickCount ?? autoLabelCount;
|
|
2878
|
-
const xTickCount = Math.max(2, Math.floor(xTickCountInput));
|
|
2879
|
-
const rawStep = xSpan / xTickCount;
|
|
2880
|
-
const xStep = Math.max(1, Math.ceil(rawStep));
|
|
2881
|
-
const visibleTickStart = Math.floor(xStart);
|
|
3174
|
+
const minLabelSpacingPx = Math.max(60, Math.min(120, chartWidth / Math.max(2, Math.floor(grid.xTickCount ?? 8))));
|
|
3175
|
+
const visibleTickStart = Math.max(0, Math.floor(xStart));
|
|
2882
3176
|
const visibleTickEnd = Math.ceil(xEnd) - 1;
|
|
2883
|
-
const
|
|
2884
|
-
|
|
3177
|
+
const timeTicks = [];
|
|
3178
|
+
{
|
|
3179
|
+
const weightBuckets = [[], [], [], [], [], [], [], [], [], []];
|
|
3180
|
+
let prevTime = visibleTickStart > 0 ? getTimeForIndex(visibleTickStart - 1) : null;
|
|
3181
|
+
for (let index = visibleTickStart; index <= visibleTickEnd; index += 1) {
|
|
3182
|
+
const time = getTimeForIndex(index);
|
|
3183
|
+
if (!time) {
|
|
3184
|
+
prevTime = null;
|
|
3185
|
+
continue;
|
|
3186
|
+
}
|
|
3187
|
+
let weight = 0;
|
|
3188
|
+
if (!prevTime) {
|
|
3189
|
+
weight = 7;
|
|
3190
|
+
} else {
|
|
3191
|
+
if (prevTime.getFullYear() !== time.getFullYear()) weight = 9;
|
|
3192
|
+
else if (prevTime.getMonth() !== time.getMonth()) weight = 8;
|
|
3193
|
+
else if (prevTime.getDate() !== time.getDate()) weight = 7;
|
|
3194
|
+
else {
|
|
3195
|
+
const prevMinutes = prevTime.getHours() * 60 + prevTime.getMinutes();
|
|
3196
|
+
const curMinutes = time.getHours() * 60 + time.getMinutes();
|
|
3197
|
+
const crossed = (span) => Math.floor(prevMinutes / span) !== Math.floor(curMinutes / span);
|
|
3198
|
+
if (crossed(360)) weight = 6;
|
|
3199
|
+
else if (crossed(60)) weight = 5;
|
|
3200
|
+
else if (crossed(30)) weight = 4;
|
|
3201
|
+
else if (crossed(15)) weight = 3;
|
|
3202
|
+
else if (crossed(5)) weight = 2;
|
|
3203
|
+
else weight = 1;
|
|
3204
|
+
}
|
|
3205
|
+
}
|
|
3206
|
+
weightBuckets[weight].push(index);
|
|
3207
|
+
prevTime = time;
|
|
3208
|
+
}
|
|
3209
|
+
const placedXs = [];
|
|
3210
|
+
const maxMarks = Math.floor(chartWidth / minLabelSpacingPx) + 2;
|
|
3211
|
+
for (let weight = 9; weight >= 1 && timeTicks.length < maxMarks; weight -= 1) {
|
|
3212
|
+
for (const index of weightBuckets[weight]) {
|
|
3213
|
+
const x = chartLeft + (index + 0.5 - xStart) / xSpan * chartWidth;
|
|
3214
|
+
let fits = true;
|
|
3215
|
+
for (const placedX of placedXs) {
|
|
3216
|
+
if (Math.abs(placedX - x) < minLabelSpacingPx) {
|
|
3217
|
+
fits = false;
|
|
3218
|
+
break;
|
|
3219
|
+
}
|
|
3220
|
+
}
|
|
3221
|
+
if (!fits) continue;
|
|
3222
|
+
placedXs.push(x);
|
|
3223
|
+
timeTicks.push({ index, weight, x });
|
|
3224
|
+
if (timeTicks.length >= maxMarks) break;
|
|
3225
|
+
}
|
|
3226
|
+
}
|
|
3227
|
+
timeTicks.sort((a, b) => a.index - b.index);
|
|
3228
|
+
}
|
|
3229
|
+
if (grid.verticalLines && timeTicks.length > 0) {
|
|
2885
3230
|
ctx.save();
|
|
2886
3231
|
ctx.globalAlpha = gridOpacity;
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
ctx.moveTo(crisp(x), crisp(chartTop));
|
|
2892
|
-
ctx.lineTo(crisp(x), crisp(fullChartBottom));
|
|
2893
|
-
ctx.stroke();
|
|
3232
|
+
ctx.strokeStyle = gridColor;
|
|
3233
|
+
ctx.lineWidth = 1;
|
|
3234
|
+
ctx.beginPath();
|
|
3235
|
+
for (const tick of timeTicks) {
|
|
3236
|
+
ctx.moveTo(crisp(tick.x), crisp(chartTop));
|
|
3237
|
+
ctx.lineTo(crisp(tick.x), crisp(fullChartBottom));
|
|
2894
3238
|
}
|
|
3239
|
+
ctx.stroke();
|
|
2895
3240
|
ctx.restore();
|
|
2896
3241
|
}
|
|
2897
3242
|
if (grid.sessionSeparators && data.length > 1) {
|
|
@@ -2929,38 +3274,45 @@ function createChart(element, options = {}) {
|
|
|
2929
3274
|
}
|
|
2930
3275
|
return data[index]?.v;
|
|
2931
3276
|
};
|
|
2932
|
-
|
|
2933
|
-
const
|
|
2934
|
-
|
|
2935
|
-
|
|
3277
|
+
{
|
|
3278
|
+
const upWicks = new Path2D();
|
|
3279
|
+
const downWicks = new Path2D();
|
|
3280
|
+
const upBodies = new Path2D();
|
|
3281
|
+
const downBodies = new Path2D();
|
|
3282
|
+
for (let index = startIndex; index <= endIndex; index += 1) {
|
|
3283
|
+
const point = data[index];
|
|
3284
|
+
if (!point) {
|
|
3285
|
+
continue;
|
|
3286
|
+
}
|
|
3287
|
+
const isLastCandle = useSmoothedCandle && index === lastDataIndex;
|
|
3288
|
+
const actualDirection = point.c >= point.o ? "up" : "down";
|
|
3289
|
+
const displayClose = isLastCandle ? actualDirection === "up" ? Math.max(point.o, smoothedTickerPrice) : Math.min(point.o, smoothedTickerPrice) : point.c;
|
|
3290
|
+
const displayHigh = isLastCandle ? actualDirection === "up" ? Math.max(point.h, displayClose) : point.h : point.h;
|
|
3291
|
+
const displayLow = isLastCandle ? actualDirection === "up" ? point.l : Math.min(point.l, displayClose) : point.l;
|
|
3292
|
+
const centerX = chartLeft + (index + 0.5 - xStart) / xSpan * chartWidth;
|
|
3293
|
+
const direction = isLastCandle ? actualDirection : getCandleDirectionByIndex(index);
|
|
3294
|
+
const isUp = direction === "up";
|
|
3295
|
+
const roundedCenterX = Math.round(centerX);
|
|
3296
|
+
const wicks = isUp ? upWicks : downWicks;
|
|
3297
|
+
wicks.moveTo(roundedCenterX + 0.5, crisp(yFromPrice(displayHigh)));
|
|
3298
|
+
wicks.lineTo(roundedCenterX + 0.5, crisp(yFromPrice(displayLow)));
|
|
3299
|
+
const bodyLeft = roundedCenterX - Math.floor(bodyWidth / 2);
|
|
3300
|
+
const openYPx = Math.round(yFromPrice(point.o));
|
|
3301
|
+
const closeYPx = Math.round(yFromPrice(displayClose));
|
|
3302
|
+
const bodyIsUp = displayClose >= point.o;
|
|
3303
|
+
const bodyTop = bodyIsUp ? Math.min(closeYPx, openYPx - 1) : openYPx;
|
|
3304
|
+
const bodyBottom = bodyIsUp ? openYPx : Math.max(closeYPx, openYPx + 1);
|
|
3305
|
+
(isUp ? upBodies : downBodies).rect(bodyLeft, bodyTop, bodyWidth, bodyBottom - bodyTop);
|
|
2936
3306
|
}
|
|
2937
|
-
const isLastCandle = useSmoothedCandle && index === lastDataIndex;
|
|
2938
|
-
const actualDirection = point.c >= point.o ? "up" : "down";
|
|
2939
|
-
const displayClose = isLastCandle ? actualDirection === "up" ? Math.max(point.o, smoothedTickerPrice) : Math.min(point.o, smoothedTickerPrice) : point.c;
|
|
2940
|
-
const displayHigh = isLastCandle ? actualDirection === "up" ? Math.max(point.h, displayClose) : point.h : point.h;
|
|
2941
|
-
const displayLow = isLastCandle ? actualDirection === "up" ? point.l : Math.min(point.l, displayClose) : point.l;
|
|
2942
|
-
const centerX = chartLeft + (index + 0.5 - xStart) / xSpan * chartWidth;
|
|
2943
|
-
const openY = yFromPrice(point.o);
|
|
2944
|
-
const closeY = yFromPrice(displayClose);
|
|
2945
|
-
const highY = yFromPrice(displayHigh);
|
|
2946
|
-
const lowY = yFromPrice(displayLow);
|
|
2947
|
-
const direction = isLastCandle ? actualDirection : getCandleDirectionByIndex(index);
|
|
2948
|
-
const candleColor = direction === "up" ? mergedOptions.upColor : mergedOptions.downColor;
|
|
2949
|
-
const roundedCenterX = Math.round(centerX);
|
|
2950
|
-
ctx.strokeStyle = candleColor;
|
|
2951
3307
|
ctx.lineWidth = candleWickWidth;
|
|
2952
|
-
ctx.
|
|
2953
|
-
ctx.
|
|
2954
|
-
ctx.
|
|
2955
|
-
ctx.
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
const bodyTop = bodyIsUp ? Math.min(closeYPx, openYPx - 1) : openYPx;
|
|
2961
|
-
const bodyBottom = bodyIsUp ? openYPx : Math.max(closeYPx, openYPx + 1);
|
|
2962
|
-
ctx.fillStyle = candleColor;
|
|
2963
|
-
ctx.fillRect(bodyLeft, bodyTop, bodyWidth, bodyBottom - bodyTop);
|
|
3308
|
+
ctx.strokeStyle = mergedOptions.upColor;
|
|
3309
|
+
ctx.stroke(upWicks);
|
|
3310
|
+
ctx.fillStyle = mergedOptions.upColor;
|
|
3311
|
+
ctx.fill(upBodies);
|
|
3312
|
+
ctx.strokeStyle = mergedOptions.downColor;
|
|
3313
|
+
ctx.stroke(downWicks);
|
|
3314
|
+
ctx.fillStyle = mergedOptions.downColor;
|
|
3315
|
+
ctx.fill(downBodies);
|
|
2964
3316
|
}
|
|
2965
3317
|
const activeOverlayIndicators = indicators.filter((indicator) => indicator.visible).map((indicator) => ({ indicator, plugin: indicatorRegistry.get(indicator.type) })).filter(
|
|
2966
3318
|
(value) => value.plugin !== void 0 && (value.indicator.pane ?? value.plugin.pane ?? "overlay") === "overlay"
|
|
@@ -3078,56 +3430,6 @@ function createChart(element, options = {}) {
|
|
|
3078
3430
|
ctx.textAlign = prevAlign;
|
|
3079
3431
|
ctx.textBaseline = prevBaseline;
|
|
3080
3432
|
}
|
|
3081
|
-
const crosshair = { ...DEFAULT_CROSSHAIR_OPTIONS, ...mergedOptions.crosshair ?? {} };
|
|
3082
|
-
if (crosshair.visible && crosshairPoint) {
|
|
3083
|
-
const cx = clamp(crosshairPoint.x, chartLeft, chartRight);
|
|
3084
|
-
const cy = clamp(crosshairPoint.y, chartTop, chartBottom);
|
|
3085
|
-
if (crosshair.mode === "dot") {
|
|
3086
|
-
ctx.save();
|
|
3087
|
-
ctx.fillStyle = crosshair.color;
|
|
3088
|
-
ctx.beginPath();
|
|
3089
|
-
ctx.arc(cx, cy, Math.max(1, crosshair.dotRadius), 0, Math.PI * 2);
|
|
3090
|
-
ctx.fill();
|
|
3091
|
-
ctx.restore();
|
|
3092
|
-
} else {
|
|
3093
|
-
ctx.save();
|
|
3094
|
-
ctx.strokeStyle = crosshair.color;
|
|
3095
|
-
ctx.lineWidth = Math.max(1, crosshair.width);
|
|
3096
|
-
applyDashPattern(crosshair.style, dashPatterns.dotted, dashPatterns.dashed);
|
|
3097
|
-
if (crosshair.showVertical) {
|
|
3098
|
-
ctx.beginPath();
|
|
3099
|
-
ctx.moveTo(crisp(cx), crisp(chartTop));
|
|
3100
|
-
ctx.lineTo(crisp(cx), crisp(chartBottom));
|
|
3101
|
-
ctx.stroke();
|
|
3102
|
-
}
|
|
3103
|
-
if (crosshair.showHorizontal) {
|
|
3104
|
-
ctx.beginPath();
|
|
3105
|
-
ctx.moveTo(crisp(chartLeft), crisp(cy));
|
|
3106
|
-
ctx.lineTo(crisp(chartRight), crisp(cy));
|
|
3107
|
-
ctx.stroke();
|
|
3108
|
-
}
|
|
3109
|
-
ctx.restore();
|
|
3110
|
-
}
|
|
3111
|
-
if (crosshair.sideHintLeft || crosshair.sideHintRight) {
|
|
3112
|
-
ctx.save();
|
|
3113
|
-
ctx.font = `600 11px ${mergedOptions.fontFamily}`;
|
|
3114
|
-
ctx.textBaseline = "middle";
|
|
3115
|
-
ctx.setLineDash([]);
|
|
3116
|
-
const hintY = clamp(cy - 14, chartTop + 8, chartBottom - 8);
|
|
3117
|
-
const hintGap = 8;
|
|
3118
|
-
if (crosshair.sideHintLeft) {
|
|
3119
|
-
ctx.fillStyle = crosshair.sideHintLeftColor;
|
|
3120
|
-
ctx.textAlign = "right";
|
|
3121
|
-
ctx.fillText(crosshair.sideHintLeft, cx - hintGap, hintY);
|
|
3122
|
-
}
|
|
3123
|
-
if (crosshair.sideHintRight) {
|
|
3124
|
-
ctx.fillStyle = crosshair.sideHintRightColor;
|
|
3125
|
-
ctx.textAlign = "left";
|
|
3126
|
-
ctx.fillText(crosshair.sideHintRight, cx + hintGap, hintY);
|
|
3127
|
-
}
|
|
3128
|
-
ctx.restore();
|
|
3129
|
-
}
|
|
3130
|
-
}
|
|
3131
3433
|
ctx.restore();
|
|
3132
3434
|
const positionAxisGutter = Math.max(0, width - chartRight);
|
|
3133
3435
|
if (positionAxisGutter > 0) {
|
|
@@ -3240,7 +3542,7 @@ function createChart(element, options = {}) {
|
|
|
3240
3542
|
const text = label.text ?? formatPaneValue(label.value);
|
|
3241
3543
|
const labelPaddingX = 7;
|
|
3242
3544
|
const labelHeight = Math.max(16, yAxisFontSize + 8);
|
|
3243
|
-
const labelWidth = getRightAxisLabelWidth(chartRight, Math.ceil(
|
|
3545
|
+
const labelWidth = getRightAxisLabelWidth(chartRight, Math.ceil(measureTextWidth(text)) + labelPaddingX * 2);
|
|
3244
3546
|
const labelX = getRightAxisLabelX(chartRight);
|
|
3245
3547
|
const labelY = clamp(yFromPaneValue(label.value) - labelHeight / 2, paneTop + 2, paneBottom - labelHeight - 2);
|
|
3246
3548
|
ctx.fillStyle = label.backgroundColor ?? label.color ?? labels.backgroundColor;
|
|
@@ -3252,18 +3554,6 @@ function createChart(element, options = {}) {
|
|
|
3252
3554
|
}
|
|
3253
3555
|
});
|
|
3254
3556
|
}
|
|
3255
|
-
if (crosshair.visible && crosshairPoint && crosshair.showVertical) {
|
|
3256
|
-
const cx = clamp(crosshairPoint.x, chartLeft, chartRight);
|
|
3257
|
-
ctx.save();
|
|
3258
|
-
ctx.strokeStyle = crosshair.color;
|
|
3259
|
-
ctx.lineWidth = Math.max(1, crosshair.width);
|
|
3260
|
-
applyDashPattern(crosshair.style, dashPatterns.dotted, dashPatterns.dashed);
|
|
3261
|
-
ctx.beginPath();
|
|
3262
|
-
ctx.moveTo(crisp(cx), crisp(chartTop));
|
|
3263
|
-
ctx.lineTo(crisp(cx), crisp(fullChartBottom));
|
|
3264
|
-
ctx.stroke();
|
|
3265
|
-
ctx.restore();
|
|
3266
|
-
}
|
|
3267
3557
|
ctx.strokeStyle = axis.lineColor;
|
|
3268
3558
|
ctx.lineWidth = Math.max(1, axis.lineWidth);
|
|
3269
3559
|
ctx.beginPath();
|
|
@@ -3273,13 +3563,13 @@ function createChart(element, options = {}) {
|
|
|
3273
3563
|
ctx.lineTo(crisp(chartRight), crisp(fullChartBottom));
|
|
3274
3564
|
ctx.stroke();
|
|
3275
3565
|
const priceScaleTickLabelInset = yAxisFontSize / 2 + 3;
|
|
3276
|
-
|
|
3277
|
-
const ratio = tick / yTicks;
|
|
3278
|
-
const price = yMin + yRange * ratio;
|
|
3279
|
-
const y = clamp(yFromPrice(price), chartTop + priceScaleTickLabelInset, chartBottom - priceScaleTickLabelInset);
|
|
3566
|
+
{
|
|
3280
3567
|
const prevFont = ctx.font;
|
|
3281
3568
|
ctx.font = `${yAxisFontSize}px ${mergedOptions.fontFamily}`;
|
|
3282
|
-
|
|
3569
|
+
for (const price of priceTicks) {
|
|
3570
|
+
const y = clamp(yFromPrice(price), chartTop + priceScaleTickLabelInset, chartBottom - priceScaleTickLabelInset);
|
|
3571
|
+
drawText(formatPrice(price), chartRight + 6, y, "left", "middle", yAxis.textColor);
|
|
3572
|
+
}
|
|
3283
3573
|
ctx.font = prevFont;
|
|
3284
3574
|
}
|
|
3285
3575
|
resetLabelSlots(labels.noOverlapping);
|
|
@@ -3388,9 +3678,15 @@ function createChart(element, options = {}) {
|
|
|
3388
3678
|
});
|
|
3389
3679
|
}
|
|
3390
3680
|
}
|
|
3391
|
-
if (labels.showHighLow &&
|
|
3392
|
-
|
|
3393
|
-
|
|
3681
|
+
if (labels.showHighLow && endIndex >= startIndex) {
|
|
3682
|
+
let visibleHigh = Number.NEGATIVE_INFINITY;
|
|
3683
|
+
let visibleLow = Number.POSITIVE_INFINITY;
|
|
3684
|
+
for (let idx = startIndex; idx <= endIndex; idx += 1) {
|
|
3685
|
+
const point = data[idx];
|
|
3686
|
+
if (!point) continue;
|
|
3687
|
+
if (point.h > visibleHigh) visibleHigh = point.h;
|
|
3688
|
+
if (point.l < visibleLow) visibleLow = point.l;
|
|
3689
|
+
}
|
|
3394
3690
|
addPriceAxisLabel({
|
|
3395
3691
|
text: `H ${formatPrice(visibleHigh)}`,
|
|
3396
3692
|
price: visibleHigh,
|
|
@@ -3443,12 +3739,12 @@ function createChart(element, options = {}) {
|
|
|
3443
3739
|
const subtextFontSize = label.subtextFontSize !== void 0 && label.subtextFontSize > 0 ? Math.max(8, Math.round(label.subtextFontSize)) : priceLabelFontSize;
|
|
3444
3740
|
const subtextLineGap = 5;
|
|
3445
3741
|
const labelHeight = baseLabelHeight + (subtexts.length > 0 ? subtexts.length * subtextFontSize + subtexts.length * subtextLineGap : 0);
|
|
3446
|
-
const primaryWidth = label.text === formatPrice(label.price) ? getPriceLabelWidth(label.text, labelPaddingX) : Math.ceil(
|
|
3742
|
+
const primaryWidth = label.text === formatPrice(label.price) ? getPriceLabelWidth(label.text, labelPaddingX) : Math.ceil(measureTextWidth(label.text)) + labelPaddingX * 2;
|
|
3447
3743
|
let subtextWidth = 0;
|
|
3448
3744
|
if (subtexts.length > 0) {
|
|
3449
3745
|
const baseFont = ctx.font;
|
|
3450
3746
|
ctx.font = `${subtextFontSize}px ${mergedOptions.fontFamily}`;
|
|
3451
|
-
subtextWidth = Math.max(...subtexts.map((subtext) => Math.ceil(
|
|
3747
|
+
subtextWidth = Math.max(...subtexts.map((subtext) => Math.ceil(measureTextWidth(subtext)) + labelPaddingX * 2));
|
|
3452
3748
|
ctx.font = baseFont;
|
|
3453
3749
|
}
|
|
3454
3750
|
const labelTextWidth = Math.max(primaryWidth, subtextWidth);
|
|
@@ -3526,8 +3822,9 @@ function createChart(element, options = {}) {
|
|
|
3526
3822
|
}
|
|
3527
3823
|
return typeof value === "string" && !/^#(?:[0-9a-fA-F]{3}){1,2}$/.test(value.trim());
|
|
3528
3824
|
};
|
|
3825
|
+
const LEGEND_EXCLUDED_INPUT_KEYS = /* @__PURE__ */ new Set(["width", "bandMultiplier", "bandFillOpacity", "fillOpacity"]);
|
|
3529
3826
|
const labelEntries = activeOverlayIndicators.map(({ indicator, plugin }) => {
|
|
3530
|
-
const inputValues = Object.entries(indicator.inputs).filter(([, value]) => isLegendInputValue(value)).slice(0, 2).map(([, value]) => String(value));
|
|
3827
|
+
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]) => String(value));
|
|
3531
3828
|
if (labels.showIndicatorNames && labels.showIndicatorValues && inputValues.length > 0) {
|
|
3532
3829
|
return `${plugin.name} ${inputValues.join(" ")}`;
|
|
3533
3830
|
}
|
|
@@ -3551,21 +3848,15 @@ function createChart(element, options = {}) {
|
|
|
3551
3848
|
ctx.font = prevFont;
|
|
3552
3849
|
}
|
|
3553
3850
|
}
|
|
3554
|
-
|
|
3555
|
-
const axisIntraday = axisStepMs > 0 && axisStepMs < 24 * 60 * 60 * 1e3;
|
|
3556
|
-
let prevAxisTickTime = null;
|
|
3557
|
-
for (let index = tickStartIndex; index <= visibleTickEnd; index += xStep) {
|
|
3558
|
-
const tickTime = getTimeForIndex(index);
|
|
3559
|
-
if (!tickTime) {
|
|
3560
|
-
continue;
|
|
3561
|
-
}
|
|
3562
|
-
const x = chartLeft + (index + 0.5 - xStart) / xSpan * chartWidth;
|
|
3563
|
-
const isNewDay = !prevAxisTickTime || prevAxisTickTime.getDate() !== tickTime.getDate() || prevAxisTickTime.getMonth() !== tickTime.getMonth() || prevAxisTickTime.getFullYear() !== tickTime.getFullYear();
|
|
3564
|
-
const timeLabel = axisIntraday && !isNewDay ? tickTime.toLocaleTimeString(void 0, { hour: "2-digit", minute: "2-digit", hour12: false }) : tickTime.toLocaleDateString(void 0, { month: "short", day: "numeric" });
|
|
3565
|
-
prevAxisTickTime = tickTime;
|
|
3851
|
+
{
|
|
3566
3852
|
const prevFont = ctx.font;
|
|
3567
3853
|
ctx.font = `${xAxisFontSize}px ${mergedOptions.fontFamily}`;
|
|
3568
|
-
|
|
3854
|
+
for (const tick of timeTicks) {
|
|
3855
|
+
const tickTime = getTimeForIndex(tick.index);
|
|
3856
|
+
if (!tickTime) continue;
|
|
3857
|
+
const timeLabel = tick.weight >= 9 ? String(tickTime.getFullYear()) : tick.weight === 8 ? tickTime.toLocaleDateString(void 0, { month: "short" }) : tick.weight === 7 ? tickTime.toLocaleDateString(void 0, { month: "short", day: "numeric" }) : tickTime.toLocaleTimeString(void 0, { hour: "2-digit", minute: "2-digit", hour12: false });
|
|
3858
|
+
drawText(timeLabel, tick.x, (fullChartBottom + height) / 2, "center", "middle", xAxis.textColor);
|
|
3859
|
+
}
|
|
3569
3860
|
ctx.font = prevFont;
|
|
3570
3861
|
}
|
|
3571
3862
|
if (labels.visible && labels.showCountdownToBarClose && lastPoint) {
|
|
@@ -3578,138 +3869,230 @@ function createChart(element, options = {}) {
|
|
|
3578
3869
|
drawText(countdownText, chartRight + 6, (fullChartBottom + height) / 2, "left", "middle", xAxis.textColor);
|
|
3579
3870
|
ctx.font = prevFont;
|
|
3580
3871
|
}
|
|
3581
|
-
if (
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3872
|
+
if (baseCtx) {
|
|
3873
|
+
if (baseCanvas.width !== canvas.width || baseCanvas.height !== canvas.height) {
|
|
3874
|
+
baseCanvas.width = canvas.width;
|
|
3875
|
+
baseCanvas.height = canvas.height;
|
|
3876
|
+
}
|
|
3877
|
+
baseCtx.setTransform(1, 0, 0, 1, 0, 0);
|
|
3878
|
+
baseCtx.clearRect(0, 0, baseCanvas.width, baseCanvas.height);
|
|
3879
|
+
baseCtx.drawImage(canvas, 0, 0);
|
|
3880
|
+
}
|
|
3881
|
+
overlayLayout = {
|
|
3882
|
+
chartLeft,
|
|
3883
|
+
chartTop,
|
|
3884
|
+
chartRight,
|
|
3885
|
+
chartBottom,
|
|
3886
|
+
chartWidth,
|
|
3887
|
+
chartHeight,
|
|
3888
|
+
fullChartBottom,
|
|
3889
|
+
yMin,
|
|
3890
|
+
yRange,
|
|
3891
|
+
xStart,
|
|
3892
|
+
xSpan
|
|
3893
|
+
};
|
|
3894
|
+
paintCrosshairOverlay();
|
|
3895
|
+
};
|
|
3896
|
+
const paintCrosshairOverlay = () => {
|
|
3897
|
+
crosshairPriceActionRegion = null;
|
|
3898
|
+
const layout = overlayLayout;
|
|
3899
|
+
const crosshair = resolvedCrosshair;
|
|
3900
|
+
if (!layout || !crosshair.visible || !crosshairPoint) {
|
|
3901
|
+
return;
|
|
3902
|
+
}
|
|
3903
|
+
const { chartLeft, chartTop, chartRight, chartBottom, chartWidth, chartHeight, fullChartBottom, yMin, yRange, xStart, xSpan: xSpan2 } = layout;
|
|
3904
|
+
const cx = clamp(crosshairPoint.x, chartLeft, chartRight);
|
|
3905
|
+
const cy = clamp(crosshairPoint.y, chartTop, chartBottom);
|
|
3906
|
+
ctx.save();
|
|
3907
|
+
ctx.beginPath();
|
|
3908
|
+
ctx.rect(chartLeft, chartTop, chartWidth, Math.max(0, chartBottom - chartTop));
|
|
3909
|
+
ctx.clip();
|
|
3910
|
+
if (crosshair.mode === "dot") {
|
|
3911
|
+
ctx.fillStyle = crosshair.color;
|
|
3912
|
+
ctx.beginPath();
|
|
3913
|
+
ctx.arc(cx, cy, Math.max(1, crosshair.dotRadius), 0, Math.PI * 2);
|
|
3914
|
+
ctx.fill();
|
|
3915
|
+
} else {
|
|
3916
|
+
ctx.strokeStyle = crosshair.color;
|
|
3917
|
+
ctx.lineWidth = Math.max(1, crosshair.width);
|
|
3918
|
+
applyDashPattern(crosshair.style, dashPatterns.dotted, dashPatterns.dashed);
|
|
3919
|
+
if (crosshair.showHorizontal) {
|
|
3920
|
+
ctx.beginPath();
|
|
3921
|
+
ctx.moveTo(crisp(chartLeft), crisp(cy));
|
|
3922
|
+
ctx.lineTo(crisp(chartRight), crisp(cy));
|
|
3923
|
+
ctx.stroke();
|
|
3924
|
+
}
|
|
3925
|
+
}
|
|
3926
|
+
if (crosshair.sideHintLeft || crosshair.sideHintRight) {
|
|
3927
|
+
ctx.save();
|
|
3928
|
+
ctx.font = `600 11px ${mergedOptions.fontFamily}`;
|
|
3929
|
+
ctx.textBaseline = "middle";
|
|
3930
|
+
ctx.setLineDash([]);
|
|
3931
|
+
const hintY = clamp(cy - 14, chartTop + 8, chartBottom - 8);
|
|
3932
|
+
const hintGap = 8;
|
|
3933
|
+
if (crosshair.sideHintLeft) {
|
|
3934
|
+
ctx.fillStyle = crosshair.sideHintLeftColor;
|
|
3935
|
+
ctx.textAlign = "right";
|
|
3936
|
+
ctx.fillText(crosshair.sideHintLeft, cx - hintGap, hintY);
|
|
3937
|
+
}
|
|
3938
|
+
if (crosshair.sideHintRight) {
|
|
3939
|
+
ctx.fillStyle = crosshair.sideHintRightColor;
|
|
3940
|
+
ctx.textAlign = "left";
|
|
3941
|
+
ctx.fillText(crosshair.sideHintRight, cx + hintGap, hintY);
|
|
3942
|
+
}
|
|
3943
|
+
ctx.restore();
|
|
3944
|
+
}
|
|
3945
|
+
ctx.restore();
|
|
3946
|
+
if (crosshair.showVertical) {
|
|
3947
|
+
ctx.save();
|
|
3948
|
+
ctx.strokeStyle = crosshair.color;
|
|
3949
|
+
ctx.lineWidth = Math.max(1, crosshair.width);
|
|
3950
|
+
applyDashPattern(crosshair.style, dashPatterns.dotted, dashPatterns.dashed);
|
|
3951
|
+
ctx.beginPath();
|
|
3952
|
+
ctx.moveTo(crisp(cx), crisp(chartTop));
|
|
3953
|
+
ctx.lineTo(crisp(cx), crisp(fullChartBottom));
|
|
3954
|
+
ctx.stroke();
|
|
3955
|
+
ctx.restore();
|
|
3956
|
+
}
|
|
3957
|
+
const labelPaddingX = 8;
|
|
3958
|
+
const labelHeight = 20;
|
|
3959
|
+
const labelRadius = Math.max(0, crosshair.labelBorderRadius);
|
|
3960
|
+
const labelBackground = crosshair.labelBackgroundColor;
|
|
3961
|
+
const labelTextColor = crosshair.labelTextColor;
|
|
3962
|
+
const labelBorderColor = crosshair.labelBorderColor;
|
|
3963
|
+
const labelBorderWidth = Math.max(0, crosshair.labelBorderWidth);
|
|
3964
|
+
const labelBorderStyle = crosshair.labelBorderStyle;
|
|
3965
|
+
const strokeCrosshairLabel = (x, y, widthValue, heightValue = labelHeight) => {
|
|
3966
|
+
if (labelBorderWidth <= 0) {
|
|
3967
|
+
return;
|
|
3968
|
+
}
|
|
3969
|
+
ctx.save();
|
|
3970
|
+
ctx.strokeStyle = labelBorderColor;
|
|
3971
|
+
ctx.lineWidth = labelBorderWidth;
|
|
3972
|
+
applyDashPattern(
|
|
3973
|
+
labelBorderStyle,
|
|
3974
|
+
dashPatterns.borderDotted,
|
|
3975
|
+
dashPatterns.borderDashed
|
|
3976
|
+
);
|
|
3977
|
+
strokeRoundedRect(Math.round(x), Math.round(y), widthValue, heightValue, labelRadius);
|
|
3978
|
+
ctx.restore();
|
|
3979
|
+
};
|
|
3980
|
+
if (crosshair.showPriceLabel) {
|
|
3981
|
+
const hoverPrice = yMin + (chartBottom - cy) / chartHeight * yRange;
|
|
3982
|
+
const priceText = formatPrice(hoverPrice);
|
|
3983
|
+
const priceWidth = getRightAxisLabelWidth(chartRight, getPriceLabelWidth(priceText, labelPaddingX));
|
|
3984
|
+
const priceX = getRightAxisLabelX(chartRight);
|
|
3985
|
+
const priceY = clamp(cy - labelHeight / 2, chartTop, chartBottom - labelHeight);
|
|
3986
|
+
ctx.fillStyle = labelBackground;
|
|
3987
|
+
fillRoundedRect(Math.round(priceX), Math.round(priceY), priceWidth, labelHeight, labelRadius);
|
|
3988
|
+
strokeCrosshairLabel(priceX, priceY, priceWidth);
|
|
3989
|
+
drawText(priceText, priceX + labelPaddingX, priceY + labelHeight / 2, "left", "middle", labelTextColor);
|
|
3990
|
+
if (crosshair.showPriceActionButton) {
|
|
3991
|
+
const buttonSize = clamp(Math.round(crosshair.priceActionButtonSize), 12, Math.max(12, labelHeight - 4));
|
|
3992
|
+
const buttonGap = Math.max(0, Math.round(crosshair.priceActionButtonGap));
|
|
3993
|
+
const containerPaddingX = 5;
|
|
3994
|
+
const containerWidth = buttonSize + containerPaddingX * 2;
|
|
3995
|
+
const containerX = priceX - buttonGap - containerWidth;
|
|
3996
|
+
const containerY = priceY;
|
|
3997
|
+
const buttonX = containerX + containerPaddingX;
|
|
3998
|
+
const buttonY = priceY + (labelHeight - buttonSize) / 2;
|
|
3999
|
+
const buttonRadius = crosshair.priceActionButtonRounded ? clamp(Math.round(crosshair.priceActionButtonBorderRadius), 0, buttonSize / 2) : 0;
|
|
4000
|
+
const buttonBorderWidth = Math.max(1, Math.round(labelBorderWidth || 1));
|
|
4001
|
+
const buttonCenterX = buttonX + buttonSize / 2;
|
|
4002
|
+
const buttonCenterY = buttonY + buttonSize / 2;
|
|
4003
|
+
const containerRadius = labelRadius;
|
|
3613
4004
|
ctx.fillStyle = labelBackground;
|
|
3614
|
-
fillRoundedRect(Math.round(
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
const buttonCenterY = buttonY + buttonSize / 2;
|
|
3630
|
-
const containerRadius = labelRadius;
|
|
3631
|
-
ctx.fillStyle = labelBackground;
|
|
3632
|
-
fillRoundedRect(Math.round(containerX), Math.round(containerY), containerWidth, labelHeight, containerRadius);
|
|
3633
|
-
if (labelBorderWidth > 0) {
|
|
3634
|
-
ctx.save();
|
|
3635
|
-
ctx.strokeStyle = labelBorderColor;
|
|
3636
|
-
ctx.lineWidth = labelBorderWidth;
|
|
3637
|
-
ctx.setLineDash([]);
|
|
3638
|
-
strokeRoundedRect(Math.round(containerX), Math.round(containerY), containerWidth, labelHeight, containerRadius);
|
|
3639
|
-
ctx.restore();
|
|
3640
|
-
}
|
|
3641
|
-
if (buttonBorderWidth > 0) {
|
|
3642
|
-
ctx.save();
|
|
3643
|
-
ctx.strokeStyle = labelBorderColor;
|
|
3644
|
-
ctx.lineWidth = buttonBorderWidth;
|
|
3645
|
-
ctx.setLineDash([]);
|
|
3646
|
-
if (crosshair.priceActionButtonRounded) {
|
|
3647
|
-
ctx.beginPath();
|
|
3648
|
-
ctx.arc(
|
|
3649
|
-
buttonCenterX,
|
|
3650
|
-
buttonCenterY,
|
|
3651
|
-
Math.max(1, buttonSize / 2 - buttonBorderWidth / 2),
|
|
3652
|
-
0,
|
|
3653
|
-
Math.PI * 2
|
|
3654
|
-
);
|
|
3655
|
-
ctx.stroke();
|
|
3656
|
-
} else {
|
|
3657
|
-
strokeRoundedRect(Math.round(buttonX), Math.round(buttonY), buttonSize, buttonSize, buttonRadius);
|
|
3658
|
-
}
|
|
3659
|
-
ctx.restore();
|
|
3660
|
-
}
|
|
3661
|
-
if (crosshair.priceActionButtonIcon !== "text" && crosshair.priceActionButtonText === "+") {
|
|
3662
|
-
const plusHalf = Math.max(3, Math.round(buttonSize * 0.22));
|
|
3663
|
-
const plusThickness = crosshair.priceActionButtonIcon === "plusThin" ? Math.max(1, Math.round(buttonSize * 0.07)) : Math.max(1, Math.round(buttonSize * 0.1));
|
|
3664
|
-
ctx.save();
|
|
3665
|
-
ctx.strokeStyle = labelTextColor;
|
|
3666
|
-
ctx.lineWidth = plusThickness;
|
|
3667
|
-
ctx.lineCap = "round";
|
|
3668
|
-
ctx.setLineDash([]);
|
|
4005
|
+
fillRoundedRect(Math.round(containerX), Math.round(containerY), containerWidth, labelHeight, containerRadius);
|
|
4006
|
+
if (labelBorderWidth > 0) {
|
|
4007
|
+
ctx.save();
|
|
4008
|
+
ctx.strokeStyle = labelBorderColor;
|
|
4009
|
+
ctx.lineWidth = labelBorderWidth;
|
|
4010
|
+
ctx.setLineDash([]);
|
|
4011
|
+
strokeRoundedRect(Math.round(containerX), Math.round(containerY), containerWidth, labelHeight, containerRadius);
|
|
4012
|
+
ctx.restore();
|
|
4013
|
+
}
|
|
4014
|
+
if (buttonBorderWidth > 0) {
|
|
4015
|
+
ctx.save();
|
|
4016
|
+
ctx.strokeStyle = labelBorderColor;
|
|
4017
|
+
ctx.lineWidth = buttonBorderWidth;
|
|
4018
|
+
ctx.setLineDash([]);
|
|
4019
|
+
if (crosshair.priceActionButtonRounded) {
|
|
3669
4020
|
ctx.beginPath();
|
|
3670
|
-
ctx.
|
|
3671
|
-
ctx.lineTo(buttonCenterX + plusHalf, buttonCenterY);
|
|
3672
|
-
ctx.moveTo(buttonCenterX, buttonCenterY - plusHalf);
|
|
3673
|
-
ctx.lineTo(buttonCenterX, buttonCenterY + plusHalf);
|
|
3674
|
-
ctx.stroke();
|
|
3675
|
-
ctx.restore();
|
|
3676
|
-
} else {
|
|
3677
|
-
drawText(
|
|
3678
|
-
crosshair.priceActionButtonText,
|
|
4021
|
+
ctx.arc(
|
|
3679
4022
|
buttonCenterX,
|
|
3680
4023
|
buttonCenterY,
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
4024
|
+
Math.max(1, buttonSize / 2 - buttonBorderWidth / 2),
|
|
4025
|
+
0,
|
|
4026
|
+
Math.PI * 2
|
|
3684
4027
|
);
|
|
4028
|
+
ctx.stroke();
|
|
4029
|
+
} else {
|
|
4030
|
+
strokeRoundedRect(Math.round(buttonX), Math.round(buttonY), buttonSize, buttonSize, buttonRadius);
|
|
3685
4031
|
}
|
|
3686
|
-
|
|
3687
|
-
x: containerX,
|
|
3688
|
-
y: containerY,
|
|
3689
|
-
width: containerWidth,
|
|
3690
|
-
height: labelHeight,
|
|
3691
|
-
price: Number(hoverPrice.toFixed(mergedOptions.priceDecimals))
|
|
3692
|
-
};
|
|
4032
|
+
ctx.restore();
|
|
3693
4033
|
}
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
ctx.
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
4034
|
+
if (crosshair.priceActionButtonIcon !== "text" && crosshair.priceActionButtonText === "+") {
|
|
4035
|
+
const plusHalf = Math.max(3, Math.round(buttonSize * 0.22));
|
|
4036
|
+
const plusThickness = crosshair.priceActionButtonIcon === "plusThin" ? Math.max(1, Math.round(buttonSize * 0.07)) : Math.max(1, Math.round(buttonSize * 0.1));
|
|
4037
|
+
ctx.save();
|
|
4038
|
+
ctx.strokeStyle = labelTextColor;
|
|
4039
|
+
ctx.lineWidth = plusThickness;
|
|
4040
|
+
ctx.lineCap = "round";
|
|
4041
|
+
ctx.setLineDash([]);
|
|
4042
|
+
ctx.beginPath();
|
|
4043
|
+
ctx.moveTo(buttonCenterX - plusHalf, buttonCenterY);
|
|
4044
|
+
ctx.lineTo(buttonCenterX + plusHalf, buttonCenterY);
|
|
4045
|
+
ctx.moveTo(buttonCenterX, buttonCenterY - plusHalf);
|
|
4046
|
+
ctx.lineTo(buttonCenterX, buttonCenterY + plusHalf);
|
|
4047
|
+
ctx.stroke();
|
|
4048
|
+
ctx.restore();
|
|
4049
|
+
} else {
|
|
4050
|
+
drawText(
|
|
4051
|
+
crosshair.priceActionButtonText,
|
|
4052
|
+
buttonCenterX,
|
|
4053
|
+
buttonCenterY,
|
|
4054
|
+
"center",
|
|
4055
|
+
"middle",
|
|
4056
|
+
labelTextColor
|
|
4057
|
+
);
|
|
3709
4058
|
}
|
|
4059
|
+
crosshairPriceActionRegion = {
|
|
4060
|
+
x: containerX,
|
|
4061
|
+
y: containerY,
|
|
4062
|
+
width: containerWidth,
|
|
4063
|
+
height: labelHeight,
|
|
4064
|
+
price: Number(hoverPrice.toFixed(mergedOptions.priceDecimals))
|
|
4065
|
+
};
|
|
4066
|
+
}
|
|
4067
|
+
}
|
|
4068
|
+
if (crosshair.showTimeLabel) {
|
|
4069
|
+
const ratio = clamp((cx - chartLeft) / chartWidth, 0, 1);
|
|
4070
|
+
const hoverIndex = Math.round(xStart + ratio * xSpan2 - 0.5);
|
|
4071
|
+
const hoverTime = getTimeForIndex(hoverIndex);
|
|
4072
|
+
if (hoverTime) {
|
|
4073
|
+
const timeText = formatHoverTimeLabel(hoverTime, crosshair.timeLabelFormat);
|
|
4074
|
+
const timeWidth = Math.ceil(measureTextWidth(timeText)) + labelPaddingX * 2;
|
|
4075
|
+
const timeX = clamp(cx - timeWidth / 2, chartLeft, chartRight - timeWidth);
|
|
4076
|
+
const timeY = fullChartBottom + 1;
|
|
4077
|
+
const timeHeight = Math.max(labelHeight, Math.floor(height - timeY));
|
|
4078
|
+
ctx.fillStyle = labelBackground;
|
|
4079
|
+
fillRoundedRect(Math.round(timeX), Math.round(timeY), timeWidth, timeHeight, labelRadius);
|
|
4080
|
+
strokeCrosshairLabel(timeX, timeY, timeWidth, timeHeight);
|
|
4081
|
+
drawText(timeText, timeX + labelPaddingX, timeY + timeHeight / 2, "left", "middle", labelTextColor);
|
|
3710
4082
|
}
|
|
3711
4083
|
}
|
|
3712
4084
|
};
|
|
4085
|
+
const drawOverlayOnly = () => {
|
|
4086
|
+
if (!baseCtx || !overlayLayout || baseCanvas.width !== canvas.width || baseCanvas.height !== canvas.height || baseCanvas.width === 0) {
|
|
4087
|
+
draw({ updateAutoScale: false });
|
|
4088
|
+
return;
|
|
4089
|
+
}
|
|
4090
|
+
const pixelRatio = getPixelRatio();
|
|
4091
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
4092
|
+
ctx.drawImage(baseCanvas, 0, 0);
|
|
4093
|
+
ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
|
|
4094
|
+
paintCrosshairOverlay();
|
|
4095
|
+
};
|
|
3713
4096
|
const zoomX = (factor, anchorX) => {
|
|
3714
4097
|
if (!drawState || data.length === 0) {
|
|
3715
4098
|
return;
|
|
@@ -3799,6 +4182,59 @@ function createChart(element, options = {}) {
|
|
|
3799
4182
|
}
|
|
3800
4183
|
scheduleDraw();
|
|
3801
4184
|
};
|
|
4185
|
+
let kineticRafId = null;
|
|
4186
|
+
const panVelocitySamples = [];
|
|
4187
|
+
const cancelKineticPan = () => {
|
|
4188
|
+
if (kineticRafId !== null) {
|
|
4189
|
+
cancelAnimationFrame(kineticRafId);
|
|
4190
|
+
kineticRafId = null;
|
|
4191
|
+
}
|
|
4192
|
+
};
|
|
4193
|
+
const recordPanVelocitySample = (x) => {
|
|
4194
|
+
const now = performance.now();
|
|
4195
|
+
panVelocitySamples.push({ t: now, x });
|
|
4196
|
+
while (panVelocitySamples.length > 0 && now - panVelocitySamples[0].t > 120) {
|
|
4197
|
+
panVelocitySamples.shift();
|
|
4198
|
+
}
|
|
4199
|
+
};
|
|
4200
|
+
const startKineticPan = (initialVelocityPxPerMs) => {
|
|
4201
|
+
cancelKineticPan();
|
|
4202
|
+
let velocity = initialVelocityPxPerMs;
|
|
4203
|
+
let lastTime = performance.now();
|
|
4204
|
+
const stepFn = (now) => {
|
|
4205
|
+
kineticRafId = null;
|
|
4206
|
+
const dt = Math.min(64, Math.max(1, now - lastTime));
|
|
4207
|
+
lastTime = now;
|
|
4208
|
+
const centerBefore = xCenter;
|
|
4209
|
+
pan(velocity * dt, 0, true, false);
|
|
4210
|
+
velocity *= Math.exp(-dt / 325);
|
|
4211
|
+
if (Math.abs(velocity) < 0.02 || xCenter === centerBefore) {
|
|
4212
|
+
return;
|
|
4213
|
+
}
|
|
4214
|
+
kineticRafId = requestAnimationFrame(stepFn);
|
|
4215
|
+
};
|
|
4216
|
+
kineticRafId = requestAnimationFrame(stepFn);
|
|
4217
|
+
};
|
|
4218
|
+
const maybeStartKineticPan = (pointerType) => {
|
|
4219
|
+
const config = mergedOptions.kineticScroll ?? DEFAULT_OPTIONS.kineticScroll;
|
|
4220
|
+
const enabled = pointerType === "touch" || pointerType === "pen" ? config?.touch !== false : config?.mouse === true;
|
|
4221
|
+
if (!enabled || panVelocitySamples.length < 2) {
|
|
4222
|
+
panVelocitySamples.length = 0;
|
|
4223
|
+
return;
|
|
4224
|
+
}
|
|
4225
|
+
const first = panVelocitySamples[0];
|
|
4226
|
+
const last = panVelocitySamples[panVelocitySamples.length - 1];
|
|
4227
|
+
panVelocitySamples.length = 0;
|
|
4228
|
+
const elapsed = last.t - first.t;
|
|
4229
|
+
if (elapsed <= 0 || performance.now() - last.t > 100) {
|
|
4230
|
+
return;
|
|
4231
|
+
}
|
|
4232
|
+
const velocity = (last.x - first.x) / elapsed;
|
|
4233
|
+
if (Math.abs(velocity) < 0.1) {
|
|
4234
|
+
return;
|
|
4235
|
+
}
|
|
4236
|
+
startKineticPan(clamp(velocity, -4, 4));
|
|
4237
|
+
};
|
|
3802
4238
|
const resetYViewport = () => {
|
|
3803
4239
|
yMinOverride = null;
|
|
3804
4240
|
yMaxOverride = null;
|
|
@@ -3858,6 +4294,7 @@ function createChart(element, options = {}) {
|
|
|
3858
4294
|
scheduleDraw();
|
|
3859
4295
|
};
|
|
3860
4296
|
const resetViewport = () => {
|
|
4297
|
+
cancelKineticPan();
|
|
3861
4298
|
fitXViewport();
|
|
3862
4299
|
resetYViewport();
|
|
3863
4300
|
updateFollowLatest(true);
|
|
@@ -3867,6 +4304,7 @@ function createChart(element, options = {}) {
|
|
|
3867
4304
|
const isFollowingLatest = () => followLatest;
|
|
3868
4305
|
const setFollowingLatest = (follow) => {
|
|
3869
4306
|
if (follow && data.length > 0) {
|
|
4307
|
+
cancelKineticPan();
|
|
3870
4308
|
xCenter = data.length - xSpan / 2 + rightEdgePaddingBars;
|
|
3871
4309
|
clampXViewport();
|
|
3872
4310
|
updateFollowLatest(true);
|
|
@@ -4236,7 +4674,7 @@ function createChart(element, options = {}) {
|
|
|
4236
4674
|
const prevFont = ctx.font;
|
|
4237
4675
|
ctx.font = `500 ${fontSize}px ${mergedOptions.fontFamily}`;
|
|
4238
4676
|
const lines = (drawing.label ?? "").split("\n");
|
|
4239
|
-
const textW = Math.max(1, ...lines.map((line) =>
|
|
4677
|
+
const textW = Math.max(1, ...lines.map((line) => measureTextWidth(line)));
|
|
4240
4678
|
ctx.font = prevFont;
|
|
4241
4679
|
const isNote = drawing.type === "note";
|
|
4242
4680
|
const padX = isNote ? 8 : 2;
|
|
@@ -4505,7 +4943,9 @@ function createChart(element, options = {}) {
|
|
|
4505
4943
|
points: [point, normalizeDrawingPoint(point.index + width2, point.price - priceOffset)],
|
|
4506
4944
|
color: defaults.color ?? "#2962ff",
|
|
4507
4945
|
style: defaults.style ?? "solid",
|
|
4508
|
-
width: defaults.width ?? 1
|
|
4946
|
+
width: defaults.width ?? 1,
|
|
4947
|
+
// pointValue enables the dollar-P&L line in the measure label.
|
|
4948
|
+
...defaults.pointValue === void 0 ? {} : { pointValue: defaults.pointValue }
|
|
4509
4949
|
})
|
|
4510
4950
|
);
|
|
4511
4951
|
emitDrawingsChange();
|
|
@@ -4707,16 +5147,24 @@ function createChart(element, options = {}) {
|
|
|
4707
5147
|
emitViewportChange();
|
|
4708
5148
|
scheduleDraw();
|
|
4709
5149
|
};
|
|
5150
|
+
const capturePointer = (pointerId) => {
|
|
5151
|
+
try {
|
|
5152
|
+
canvas.setPointerCapture(pointerId);
|
|
5153
|
+
} catch {
|
|
5154
|
+
}
|
|
5155
|
+
};
|
|
4710
5156
|
const onPointerDown = (event) => {
|
|
4711
5157
|
if (event.pointerType === "touch" || event.pointerType === "pen") {
|
|
4712
5158
|
event.preventDefault();
|
|
4713
5159
|
}
|
|
5160
|
+
cancelKineticPan();
|
|
5161
|
+
panVelocitySamples.length = 0;
|
|
4714
5162
|
magnetModifierActive = event.metaKey || event.ctrlKey;
|
|
4715
5163
|
shiftKeyActive = event.shiftKey;
|
|
4716
5164
|
const point = getCanvasPoint(event);
|
|
4717
5165
|
if (event.pointerType === "touch") {
|
|
4718
5166
|
touchPointers.set(event.pointerId, point);
|
|
4719
|
-
|
|
5167
|
+
capturePointer(event.pointerId);
|
|
4720
5168
|
const touchPair = getTouchPair();
|
|
4721
5169
|
if (touchPair) {
|
|
4722
5170
|
beginPinchZoom(touchPair[0], touchPair[1]);
|
|
@@ -4746,7 +5194,7 @@ function createChart(element, options = {}) {
|
|
|
4746
5194
|
lastPrice: startPrice,
|
|
4747
5195
|
moved: false
|
|
4748
5196
|
};
|
|
4749
|
-
|
|
5197
|
+
capturePointer(event.pointerId);
|
|
4750
5198
|
canvas.style.cursor = "ns-resize";
|
|
4751
5199
|
setCrosshairPoint(null);
|
|
4752
5200
|
return;
|
|
@@ -4767,7 +5215,7 @@ function createChart(element, options = {}) {
|
|
|
4767
5215
|
startPrice: orderDragRegion.price,
|
|
4768
5216
|
lastPrice: orderDragRegion.price
|
|
4769
5217
|
};
|
|
4770
|
-
|
|
5218
|
+
capturePointer(event.pointerId);
|
|
4771
5219
|
canvas.style.cursor = "ns-resize";
|
|
4772
5220
|
setCrosshairPoint(null);
|
|
4773
5221
|
return;
|
|
@@ -4799,7 +5247,7 @@ function createChart(element, options = {}) {
|
|
|
4799
5247
|
startPoints: drawingHit.drawing.points.map((drawingPoint) => ({ ...drawingPoint }))
|
|
4800
5248
|
};
|
|
4801
5249
|
activePointerId = event.pointerId;
|
|
4802
|
-
|
|
5250
|
+
capturePointer(event.pointerId);
|
|
4803
5251
|
}
|
|
4804
5252
|
}
|
|
4805
5253
|
setCrosshairPoint(null);
|
|
@@ -4827,7 +5275,7 @@ function createChart(element, options = {}) {
|
|
|
4827
5275
|
};
|
|
4828
5276
|
lastPointerX = point.x;
|
|
4829
5277
|
lastPointerY = point.y;
|
|
4830
|
-
|
|
5278
|
+
capturePointer(event.pointerId);
|
|
4831
5279
|
if (crosshairDrag) {
|
|
4832
5280
|
canvas.style.cursor = "crosshair";
|
|
4833
5281
|
setCrosshairPoint(point);
|
|
@@ -5013,6 +5461,7 @@ function createChart(element, options = {}) {
|
|
|
5013
5461
|
const deltaY = point.y - lastPointerY;
|
|
5014
5462
|
if (dragMode === "plot") {
|
|
5015
5463
|
canvas.style.cursor = "grabbing";
|
|
5464
|
+
recordPanVelocitySample(point.x);
|
|
5016
5465
|
pan(deltaX, deltaY, true, true);
|
|
5017
5466
|
setCrosshairPoint(null);
|
|
5018
5467
|
} else if (dragMode === "x-axis") {
|
|
@@ -5086,10 +5535,16 @@ function createChart(element, options = {}) {
|
|
|
5086
5535
|
drawingDragState = null;
|
|
5087
5536
|
emitDrawingsChange();
|
|
5088
5537
|
}
|
|
5538
|
+
const endedPlotDrag = isDragging && dragMode === "plot";
|
|
5089
5539
|
isDragging = false;
|
|
5090
5540
|
dragMode = null;
|
|
5091
5541
|
activePointerId = null;
|
|
5092
5542
|
canvas.style.cursor = "default";
|
|
5543
|
+
if (endedPlotDrag) {
|
|
5544
|
+
maybeStartKineticPan(event?.pointerType);
|
|
5545
|
+
} else {
|
|
5546
|
+
panVelocitySamples.length = 0;
|
|
5547
|
+
}
|
|
5093
5548
|
if (event && pointerDownInfo && event.pointerId === pointerDownInfo.pointerId) {
|
|
5094
5549
|
if (pointerDownInfo.crosshairDrag) {
|
|
5095
5550
|
const point = getCanvasPoint(event);
|
|
@@ -5127,6 +5582,7 @@ function createChart(element, options = {}) {
|
|
|
5127
5582
|
if (!drawState) {
|
|
5128
5583
|
return;
|
|
5129
5584
|
}
|
|
5585
|
+
cancelKineticPan();
|
|
5130
5586
|
const point = getCanvasPoint(event);
|
|
5131
5587
|
const region = getHitRegion(point.x, point.y);
|
|
5132
5588
|
if (region === "outside") {
|
|
@@ -5226,6 +5682,7 @@ function createChart(element, options = {}) {
|
|
|
5226
5682
|
width = nextOptions.width !== void 0 && nextOptions.width > 0 ? mergedOptions.width : previousWidth;
|
|
5227
5683
|
height = nextOptions.height !== void 0 && nextOptions.height > 0 ? mergedOptions.height : previousHeight;
|
|
5228
5684
|
mergedOptions = { ...mergedOptions, width, height };
|
|
5685
|
+
refreshResolvedOptions();
|
|
5229
5686
|
resetRightMarginCache();
|
|
5230
5687
|
doubleClickEnabled = mergedOptions.doubleClickEnabled;
|
|
5231
5688
|
doubleClickAction = mergedOptions.doubleClickAction;
|
|
@@ -5570,6 +6027,7 @@ function createChart(element, options = {}) {
|
|
|
5570
6027
|
drawingHoverHandler = handler;
|
|
5571
6028
|
};
|
|
5572
6029
|
const destroy = () => {
|
|
6030
|
+
cancelKineticPan();
|
|
5573
6031
|
if (smoothingRafId !== null) {
|
|
5574
6032
|
cancelAnimationFrame(smoothingRafId);
|
|
5575
6033
|
smoothingRafId = null;
|