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
|
@@ -215,8 +215,12 @@ var DEFAULT_OPTIONS = {
|
|
|
215
215
|
tickSize: 0,
|
|
216
216
|
candleColorMode: "openClose",
|
|
217
217
|
candleColorEpsilon: -1,
|
|
218
|
-
|
|
218
|
+
// 0 = deterministic scale (lightweight-charts behavior): the y-range is a
|
|
219
|
+
// pure function of the visible data, so it can never drift or "breathe".
|
|
220
|
+
// Values > 0 re-enable eased contraction for hosts that prefer it.
|
|
221
|
+
autoScaleSmoothing: 0,
|
|
219
222
|
autoScaleIgnoreLatestCandle: true,
|
|
223
|
+
kineticScroll: { touch: true, mouse: false },
|
|
220
224
|
pinOutOfRangeLines: false,
|
|
221
225
|
doubleClickEnabled: true,
|
|
222
226
|
doubleClickAction: "reset",
|
|
@@ -435,6 +439,60 @@ var computeStdDevSeries = (data, length, source) => {
|
|
|
435
439
|
}
|
|
436
440
|
return result;
|
|
437
441
|
};
|
|
442
|
+
var VWAP_SESSION_ANCHOR_MS = 22 * 36e5;
|
|
443
|
+
var vwapSessionDayOf = (ms) => Math.floor((ms - VWAP_SESSION_ANCHOR_MS) / 864e5);
|
|
444
|
+
var computeVwapSeries = (data, band) => {
|
|
445
|
+
const result = new Array(data.length).fill(null);
|
|
446
|
+
let session = Number.NaN;
|
|
447
|
+
let cumPv = 0;
|
|
448
|
+
let cumPv2 = 0;
|
|
449
|
+
let cumV = 0;
|
|
450
|
+
for (let i = 0; i < data.length; i += 1) {
|
|
451
|
+
const point = data[i];
|
|
452
|
+
if (!point) continue;
|
|
453
|
+
const day = vwapSessionDayOf(point.time.getTime());
|
|
454
|
+
if (day !== session) {
|
|
455
|
+
session = day;
|
|
456
|
+
cumPv = 0;
|
|
457
|
+
cumPv2 = 0;
|
|
458
|
+
cumV = 0;
|
|
459
|
+
}
|
|
460
|
+
const typical = (point.h + point.l + point.c) / 3;
|
|
461
|
+
const volume = Math.max(0, point.v ?? 0);
|
|
462
|
+
cumPv += typical * volume;
|
|
463
|
+
cumPv2 += typical * typical * volume;
|
|
464
|
+
cumV += volume;
|
|
465
|
+
if (cumV <= 0) continue;
|
|
466
|
+
const vwap = cumPv / cumV;
|
|
467
|
+
if (band === "vwap") {
|
|
468
|
+
result[i] = vwap;
|
|
469
|
+
} else {
|
|
470
|
+
const variance = Math.max(0, cumPv2 / cumV - vwap * vwap);
|
|
471
|
+
result[i] = Math.sqrt(variance);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
for (let i = 1; i < data.length; i += 1) {
|
|
475
|
+
const point = data[i];
|
|
476
|
+
const prev = data[i - 1];
|
|
477
|
+
const next = data[i + 1];
|
|
478
|
+
if (!point || !prev || !next) continue;
|
|
479
|
+
const day = vwapSessionDayOf(point.time.getTime());
|
|
480
|
+
if (day !== vwapSessionDayOf(prev.time.getTime()) && day === vwapSessionDayOf(next.time.getTime())) {
|
|
481
|
+
result[i] = null;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
return result;
|
|
485
|
+
};
|
|
486
|
+
var computeBollingerSeries = (data, length, multiplier, source, band) => {
|
|
487
|
+
const basis = computeSmaSeries(data, length, source);
|
|
488
|
+
if (band === "basis") return basis;
|
|
489
|
+
const dev = computeStdDevSeries(data, length, source);
|
|
490
|
+
return basis.map((value, idx) => {
|
|
491
|
+
const sigma = dev[idx];
|
|
492
|
+
if (value == null || sigma == null) return null;
|
|
493
|
+
return band === "upper" ? value + multiplier * sigma : value - multiplier * sigma;
|
|
494
|
+
});
|
|
495
|
+
};
|
|
438
496
|
var computeRsiSeries = (data, length) => {
|
|
439
497
|
const result = new Array(data.length).fill(null);
|
|
440
498
|
if (data.length < 2) return result;
|
|
@@ -561,6 +619,59 @@ var drawOverlaySeries = (ctx, renderContext, values, color, width) => {
|
|
|
561
619
|
}
|
|
562
620
|
ctx.restore();
|
|
563
621
|
};
|
|
622
|
+
var rangeOfSeries = (seriesList, startIndex, endIndex, skipIndex) => {
|
|
623
|
+
let min = Number.POSITIVE_INFINITY;
|
|
624
|
+
let max = Number.NEGATIVE_INFINITY;
|
|
625
|
+
for (const series of seriesList) {
|
|
626
|
+
for (let idx = startIndex; idx <= endIndex; idx += 1) {
|
|
627
|
+
if (idx === skipIndex) continue;
|
|
628
|
+
const value = series[idx];
|
|
629
|
+
if (value == null || !Number.isFinite(value)) continue;
|
|
630
|
+
if (value < min) min = value;
|
|
631
|
+
if (value > max) max = value;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
return min <= max ? { min, max } : null;
|
|
635
|
+
};
|
|
636
|
+
var maAutoscaleRange = (cacheKeyPrefix, compute, defaultLength) => (data, startIndex, endIndex, inputs, skipIndex) => {
|
|
637
|
+
const length = clampIndicatorLength(inputs.length, defaultLength);
|
|
638
|
+
const source = inputs.source ?? "close";
|
|
639
|
+
const series = withCachedSeries(`${cacheKeyPrefix}|${length}|${source}`, data, () => compute(data, length, source));
|
|
640
|
+
return rangeOfSeries([series], startIndex, endIndex, skipIndex);
|
|
641
|
+
};
|
|
642
|
+
var fillBetweenSeries = (ctx, renderContext, upper, lower, color, opacity) => {
|
|
643
|
+
if (!renderContext.yFromPrice || opacity <= 0) return;
|
|
644
|
+
const yFromPrice = renderContext.yFromPrice;
|
|
645
|
+
ctx.save();
|
|
646
|
+
ctx.globalAlpha = Math.min(1, Math.max(0, opacity));
|
|
647
|
+
ctx.fillStyle = color;
|
|
648
|
+
let runStart = -1;
|
|
649
|
+
const flushRun = (endIndexExclusive) => {
|
|
650
|
+
if (runStart < 0) return;
|
|
651
|
+
ctx.beginPath();
|
|
652
|
+
for (let i = runStart; i < endIndexExclusive; i += 1) {
|
|
653
|
+
const x = renderContext.xFromIndex(i);
|
|
654
|
+
const y = yFromPrice(upper[i]);
|
|
655
|
+
if (i === runStart) ctx.moveTo(x, y);
|
|
656
|
+
else ctx.lineTo(x, y);
|
|
657
|
+
}
|
|
658
|
+
for (let i = endIndexExclusive - 1; i >= runStart; i -= 1) {
|
|
659
|
+
ctx.lineTo(renderContext.xFromIndex(i), yFromPrice(lower[i]));
|
|
660
|
+
}
|
|
661
|
+
ctx.closePath();
|
|
662
|
+
ctx.fill();
|
|
663
|
+
runStart = -1;
|
|
664
|
+
};
|
|
665
|
+
for (let index = renderContext.startIndex; index <= renderContext.endIndex; index += 1) {
|
|
666
|
+
const up = upper[index];
|
|
667
|
+
const lo = lower[index];
|
|
668
|
+
const valid = Number.isFinite(up ?? Number.NaN) && Number.isFinite(lo ?? Number.NaN);
|
|
669
|
+
if (valid && runStart < 0) runStart = index;
|
|
670
|
+
if (!valid) flushRun(index);
|
|
671
|
+
}
|
|
672
|
+
flushRun(renderContext.endIndex + 1);
|
|
673
|
+
ctx.restore();
|
|
674
|
+
};
|
|
564
675
|
var drawSeparateSeries = (ctx, renderContext, values, color, width, minOverride, maxOverride, guideLines, options = {}) => {
|
|
565
676
|
const visible = [];
|
|
566
677
|
for (let index = renderContext.startIndex; index <= renderContext.endIndex; index += 1) {
|
|
@@ -745,7 +856,8 @@ var BUILTIN_SMA_INDICATOR = {
|
|
|
745
856
|
() => computeSmaSeries(renderContext.data, length, inputs.source ?? "close")
|
|
746
857
|
);
|
|
747
858
|
drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#60a5fa", Number(inputs.width) || 2);
|
|
748
|
-
}
|
|
859
|
+
},
|
|
860
|
+
getAutoscaleRange: maAutoscaleRange("sma", computeSmaSeries, 20)
|
|
749
861
|
};
|
|
750
862
|
var BUILTIN_EMA_INDICATOR = {
|
|
751
863
|
id: "ema",
|
|
@@ -760,7 +872,8 @@ var BUILTIN_EMA_INDICATOR = {
|
|
|
760
872
|
() => computeEmaSeries(renderContext.data, length, inputs.source ?? "close")
|
|
761
873
|
);
|
|
762
874
|
drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#f59e0b", Number(inputs.width) || 2);
|
|
763
|
-
}
|
|
875
|
+
},
|
|
876
|
+
getAutoscaleRange: maAutoscaleRange("ema", computeEmaSeries, 20)
|
|
764
877
|
};
|
|
765
878
|
var BUILTIN_WMA_INDICATOR = {
|
|
766
879
|
id: "wma",
|
|
@@ -775,7 +888,8 @@ var BUILTIN_WMA_INDICATOR = {
|
|
|
775
888
|
() => computeWmaSeries(renderContext.data, length, inputs.source ?? "close")
|
|
776
889
|
);
|
|
777
890
|
drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#a78bfa", Number(inputs.width) || 2);
|
|
778
|
-
}
|
|
891
|
+
},
|
|
892
|
+
getAutoscaleRange: maAutoscaleRange("wma", computeWmaSeries, 20)
|
|
779
893
|
};
|
|
780
894
|
var BUILTIN_VWMA_INDICATOR = {
|
|
781
895
|
id: "vwma",
|
|
@@ -790,7 +904,8 @@ var BUILTIN_VWMA_INDICATOR = {
|
|
|
790
904
|
() => computeVwmaSeries(renderContext.data, length, inputs.source ?? "close")
|
|
791
905
|
);
|
|
792
906
|
drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#ef4444", Number(inputs.width) || 2);
|
|
793
|
-
}
|
|
907
|
+
},
|
|
908
|
+
getAutoscaleRange: maAutoscaleRange("vwma", computeVwmaSeries, 20)
|
|
794
909
|
};
|
|
795
910
|
var BUILTIN_RMA_INDICATOR = {
|
|
796
911
|
id: "rma",
|
|
@@ -805,7 +920,8 @@ var BUILTIN_RMA_INDICATOR = {
|
|
|
805
920
|
() => computeRmaSeries(renderContext.data, length, inputs.source ?? "close")
|
|
806
921
|
);
|
|
807
922
|
drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#22c55e", Number(inputs.width) || 2);
|
|
808
|
-
}
|
|
923
|
+
},
|
|
924
|
+
getAutoscaleRange: maAutoscaleRange("rma", computeRmaSeries, 14)
|
|
809
925
|
};
|
|
810
926
|
var BUILTIN_HMA_INDICATOR = {
|
|
811
927
|
id: "hma",
|
|
@@ -820,6 +936,127 @@ var BUILTIN_HMA_INDICATOR = {
|
|
|
820
936
|
() => computeHmaSeries(renderContext.data, length, inputs.source ?? "close")
|
|
821
937
|
);
|
|
822
938
|
drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#14b8a6", Number(inputs.width) || 2);
|
|
939
|
+
},
|
|
940
|
+
getAutoscaleRange: maAutoscaleRange("hma", computeHmaSeries, 21)
|
|
941
|
+
};
|
|
942
|
+
var BUILTIN_VWAP_INDICATOR = {
|
|
943
|
+
id: "vwap",
|
|
944
|
+
name: "VWAP",
|
|
945
|
+
pane: "overlay",
|
|
946
|
+
defaultInputs: {
|
|
947
|
+
color: "#f59e0b",
|
|
948
|
+
width: 2,
|
|
949
|
+
showBands: false,
|
|
950
|
+
bandMultiplier: 1,
|
|
951
|
+
bandColor: "#94a3b8",
|
|
952
|
+
bandFillOpacity: 0.06
|
|
953
|
+
},
|
|
954
|
+
draw: (ctx, renderContext, inputs) => {
|
|
955
|
+
const vwap = withCachedSeries(
|
|
956
|
+
"vwap|line",
|
|
957
|
+
renderContext.data,
|
|
958
|
+
() => computeVwapSeries(renderContext.data, "vwap")
|
|
959
|
+
);
|
|
960
|
+
if (inputs.showBands) {
|
|
961
|
+
const sigma = withCachedSeries(
|
|
962
|
+
"vwap|sigma",
|
|
963
|
+
renderContext.data,
|
|
964
|
+
() => computeVwapSeries(renderContext.data, "sigma")
|
|
965
|
+
);
|
|
966
|
+
const multiplier = Math.max(0.1, Number(inputs.bandMultiplier) || 1);
|
|
967
|
+
const upper = vwap.map((value, idx) => {
|
|
968
|
+
const s = sigma[idx];
|
|
969
|
+
return value == null || s == null ? null : value + multiplier * s;
|
|
970
|
+
});
|
|
971
|
+
const lower = vwap.map((value, idx) => {
|
|
972
|
+
const s = sigma[idx];
|
|
973
|
+
return value == null || s == null ? null : value - multiplier * s;
|
|
974
|
+
});
|
|
975
|
+
const bandColor = inputs.bandColor ?? "#94a3b8";
|
|
976
|
+
fillBetweenSeries(ctx, renderContext, upper, lower, bandColor, Number(inputs.bandFillOpacity) || 0.06);
|
|
977
|
+
drawOverlaySeries(ctx, renderContext, upper, bandColor, 1);
|
|
978
|
+
drawOverlaySeries(ctx, renderContext, lower, bandColor, 1);
|
|
979
|
+
}
|
|
980
|
+
drawOverlaySeries(ctx, renderContext, vwap, inputs.color ?? "#f59e0b", Number(inputs.width) || 2);
|
|
981
|
+
},
|
|
982
|
+
getAutoscaleRange: (data, startIndex, endIndex, inputs, skipIndex) => {
|
|
983
|
+
const vwap = withCachedSeries("vwap|line", data, () => computeVwapSeries(data, "vwap"));
|
|
984
|
+
if (!inputs.showBands) {
|
|
985
|
+
return rangeOfSeries([vwap], startIndex, endIndex, skipIndex);
|
|
986
|
+
}
|
|
987
|
+
const sigma = withCachedSeries("vwap|sigma", data, () => computeVwapSeries(data, "sigma"));
|
|
988
|
+
const multiplier = Math.max(0.1, Number(inputs.bandMultiplier) || 1);
|
|
989
|
+
let min = Number.POSITIVE_INFINITY;
|
|
990
|
+
let max = Number.NEGATIVE_INFINITY;
|
|
991
|
+
for (let idx = startIndex; idx <= endIndex; idx += 1) {
|
|
992
|
+
if (idx === skipIndex) continue;
|
|
993
|
+
const value = vwap[idx];
|
|
994
|
+
const s = sigma[idx];
|
|
995
|
+
if (value == null || s == null) continue;
|
|
996
|
+
const lo = value - multiplier * s;
|
|
997
|
+
const hi = value + multiplier * s;
|
|
998
|
+
if (lo < min) min = lo;
|
|
999
|
+
if (hi > max) max = hi;
|
|
1000
|
+
}
|
|
1001
|
+
return min <= max ? { min, max } : null;
|
|
1002
|
+
}
|
|
1003
|
+
};
|
|
1004
|
+
var BUILTIN_BOLLINGER_INDICATOR = {
|
|
1005
|
+
id: "bollinger",
|
|
1006
|
+
name: "BB",
|
|
1007
|
+
pane: "overlay",
|
|
1008
|
+
// Basis is pink so it stays distinguishable from the (orange) VWAP line
|
|
1009
|
+
// when both overlays are active.
|
|
1010
|
+
defaultInputs: {
|
|
1011
|
+
length: 20,
|
|
1012
|
+
multiplier: 2,
|
|
1013
|
+
source: "close",
|
|
1014
|
+
basisColor: "#ec4899",
|
|
1015
|
+
bandColor: "#3b82f6",
|
|
1016
|
+
width: 1.5,
|
|
1017
|
+
fillOpacity: 0.05
|
|
1018
|
+
},
|
|
1019
|
+
draw: (ctx, renderContext, inputs) => {
|
|
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 basis = withCachedSeries(
|
|
1024
|
+
`bb|basis|${length}|${source}`,
|
|
1025
|
+
renderContext.data,
|
|
1026
|
+
() => computeBollingerSeries(renderContext.data, length, multiplier, source, "basis")
|
|
1027
|
+
);
|
|
1028
|
+
const upper = withCachedSeries(
|
|
1029
|
+
`bb|upper|${length}|${multiplier}|${source}`,
|
|
1030
|
+
renderContext.data,
|
|
1031
|
+
() => computeBollingerSeries(renderContext.data, length, multiplier, source, "upper")
|
|
1032
|
+
);
|
|
1033
|
+
const lower = withCachedSeries(
|
|
1034
|
+
`bb|lower|${length}|${multiplier}|${source}`,
|
|
1035
|
+
renderContext.data,
|
|
1036
|
+
() => computeBollingerSeries(renderContext.data, length, multiplier, source, "lower")
|
|
1037
|
+
);
|
|
1038
|
+
const bandColor = inputs.bandColor ?? "#3b82f6";
|
|
1039
|
+
const width = Number(inputs.width) || 1.5;
|
|
1040
|
+
fillBetweenSeries(ctx, renderContext, upper, lower, bandColor, Number(inputs.fillOpacity) || 0.05);
|
|
1041
|
+
drawOverlaySeries(ctx, renderContext, upper, bandColor, width);
|
|
1042
|
+
drawOverlaySeries(ctx, renderContext, lower, bandColor, width);
|
|
1043
|
+
drawOverlaySeries(ctx, renderContext, basis, inputs.basisColor ?? "#ec4899", width);
|
|
1044
|
+
},
|
|
1045
|
+
getAutoscaleRange: (data, startIndex, endIndex, inputs, skipIndex) => {
|
|
1046
|
+
const length = clampIndicatorLength(inputs.length, 20);
|
|
1047
|
+
const multiplier = Math.max(0.1, Number(inputs.multiplier) || 2);
|
|
1048
|
+
const source = inputs.source ?? "close";
|
|
1049
|
+
const upper = withCachedSeries(
|
|
1050
|
+
`bb|upper|${length}|${multiplier}|${source}`,
|
|
1051
|
+
data,
|
|
1052
|
+
() => computeBollingerSeries(data, length, multiplier, source, "upper")
|
|
1053
|
+
);
|
|
1054
|
+
const lower = withCachedSeries(
|
|
1055
|
+
`bb|lower|${length}|${multiplier}|${source}`,
|
|
1056
|
+
data,
|
|
1057
|
+
() => computeBollingerSeries(data, length, multiplier, source, "lower")
|
|
1058
|
+
);
|
|
1059
|
+
return rangeOfSeries([upper, lower], startIndex, endIndex, skipIndex);
|
|
823
1060
|
}
|
|
824
1061
|
};
|
|
825
1062
|
var BUILTIN_STDDEV_INDICATOR = {
|
|
@@ -905,11 +1142,27 @@ var BUILTIN_INDICATORS = [
|
|
|
905
1142
|
BUILTIN_VWMA_INDICATOR,
|
|
906
1143
|
BUILTIN_RMA_INDICATOR,
|
|
907
1144
|
BUILTIN_HMA_INDICATOR,
|
|
1145
|
+
BUILTIN_VWAP_INDICATOR,
|
|
1146
|
+
BUILTIN_BOLLINGER_INDICATOR,
|
|
908
1147
|
BUILTIN_STDDEV_INDICATOR,
|
|
909
1148
|
BUILTIN_ATR_INDICATOR
|
|
910
1149
|
];
|
|
911
1150
|
function createChart(element, options = {}) {
|
|
912
1151
|
let mergedOptions = mergeChartOptions(DEFAULT_OPTIONS, options);
|
|
1152
|
+
let resolvedAxis = { ...DEFAULT_AXIS_OPTIONS, ...mergedOptions.axis ?? {} };
|
|
1153
|
+
let resolvedXAxis = { ...DEFAULT_AXIS_OPTIONS, ...mergedOptions.xAxis ?? {} };
|
|
1154
|
+
let resolvedYAxis = { ...DEFAULT_AXIS_OPTIONS, ...mergedOptions.yAxis ?? {} };
|
|
1155
|
+
let resolvedLabels = { ...DEFAULT_LABELS_OPTIONS, ...mergedOptions.labels ?? {} };
|
|
1156
|
+
let resolvedGrid = { ...DEFAULT_GRID_OPTIONS, ...mergedOptions.grid ?? {} };
|
|
1157
|
+
let resolvedCrosshair = { ...DEFAULT_CROSSHAIR_OPTIONS, ...mergedOptions.crosshair ?? {} };
|
|
1158
|
+
const refreshResolvedOptions = () => {
|
|
1159
|
+
resolvedAxis = { ...DEFAULT_AXIS_OPTIONS, ...mergedOptions.axis ?? {} };
|
|
1160
|
+
resolvedXAxis = { ...DEFAULT_AXIS_OPTIONS, ...mergedOptions.xAxis ?? {} };
|
|
1161
|
+
resolvedYAxis = { ...DEFAULT_AXIS_OPTIONS, ...mergedOptions.yAxis ?? {} };
|
|
1162
|
+
resolvedLabels = { ...DEFAULT_LABELS_OPTIONS, ...mergedOptions.labels ?? {} };
|
|
1163
|
+
resolvedGrid = { ...DEFAULT_GRID_OPTIONS, ...mergedOptions.grid ?? {} };
|
|
1164
|
+
resolvedCrosshair = { ...DEFAULT_CROSSHAIR_OPTIONS, ...mergedOptions.crosshair ?? {} };
|
|
1165
|
+
};
|
|
913
1166
|
let width = mergedOptions.width;
|
|
914
1167
|
let height = mergedOptions.height;
|
|
915
1168
|
let data = [];
|
|
@@ -946,8 +1199,11 @@ function createChart(element, options = {}) {
|
|
|
946
1199
|
pane: indicator.pane ?? plugin?.pane ?? "overlay",
|
|
947
1200
|
...indicator.paneHeightRatio === void 0 ? {} : { paneHeightRatio: indicator.paneHeightRatio },
|
|
948
1201
|
zIndex: Math.round(Number(indicator.zIndex) || 0),
|
|
949
|
-
|
|
950
|
-
|
|
1202
|
+
// Overlay series participate in autoscale by default (lightweight-charts
|
|
1203
|
+
// semantics): bands and MAs expand the price range instead of clipping
|
|
1204
|
+
// at the chart edge. Hosts can still opt out per indicator.
|
|
1205
|
+
excludeFromAutoscale: indicator.excludeFromAutoscale ?? false,
|
|
1206
|
+
overlayScaleWeight: indicator.overlayScaleWeight === void 0 ? 1 : Math.min(1, Math.max(0, Number(indicator.overlayScaleWeight) || 0)),
|
|
951
1207
|
inputs: {
|
|
952
1208
|
...defaults,
|
|
953
1209
|
...indicator.inputs ?? {}
|
|
@@ -1149,6 +1405,9 @@ function createChart(element, options = {}) {
|
|
|
1149
1405
|
canvas.setAttribute("draggable", "false");
|
|
1150
1406
|
element.innerHTML = "";
|
|
1151
1407
|
element.appendChild(canvas);
|
|
1408
|
+
const baseCanvas = document.createElement("canvas");
|
|
1409
|
+
const baseCtx = baseCanvas.getContext("2d");
|
|
1410
|
+
let overlayLayout = null;
|
|
1152
1411
|
const margin = { top: 16, right: 72, bottom: 26, left: 12 };
|
|
1153
1412
|
let cachedRightMargin = margin.right;
|
|
1154
1413
|
const minVisibleBars = Math.max(1, Math.floor(mergedOptions.minVisibleBars));
|
|
@@ -1188,6 +1447,20 @@ function createChart(element, options = {}) {
|
|
|
1188
1447
|
const clamp = (value, min, max) => {
|
|
1189
1448
|
return Math.min(max, Math.max(min, value));
|
|
1190
1449
|
};
|
|
1450
|
+
const textWidthCache = /* @__PURE__ */ new Map();
|
|
1451
|
+
const measureTextWidth = (text) => {
|
|
1452
|
+
const key = `${ctx.font}\0${text}`;
|
|
1453
|
+
const cached = textWidthCache.get(key);
|
|
1454
|
+
if (cached !== void 0) {
|
|
1455
|
+
return cached;
|
|
1456
|
+
}
|
|
1457
|
+
const width2 = ctx.measureText(text).width;
|
|
1458
|
+
if (textWidthCache.size >= 4096) {
|
|
1459
|
+
textWidthCache.clear();
|
|
1460
|
+
}
|
|
1461
|
+
textWidthCache.set(key, width2);
|
|
1462
|
+
return width2;
|
|
1463
|
+
};
|
|
1191
1464
|
const resetLabelSlots = (enabled) => {
|
|
1192
1465
|
noOverlappingLineLabels = enabled;
|
|
1193
1466
|
rightAxisLabelSlots = [];
|
|
@@ -1390,7 +1663,7 @@ function createChart(element, options = {}) {
|
|
|
1390
1663
|
return `${integerPart}${decimalPart}`;
|
|
1391
1664
|
};
|
|
1392
1665
|
const getMeasuredLabelWidth = (text, paddingX) => {
|
|
1393
|
-
return Math.ceil(
|
|
1666
|
+
return Math.ceil(measureTextWidth(text)) + paddingX * 2;
|
|
1394
1667
|
};
|
|
1395
1668
|
const getStabilizedNumericLabelWidth = (text, paddingX) => {
|
|
1396
1669
|
const measured = getMeasuredLabelWidth(text, paddingX);
|
|
@@ -1631,11 +1904,11 @@ function createChart(element, options = {}) {
|
|
|
1631
1904
|
return;
|
|
1632
1905
|
}
|
|
1633
1906
|
const labelText = mergedLine.label ?? formatPrice(mergedLine.price);
|
|
1634
|
-
const axis =
|
|
1907
|
+
const axis = resolvedAxis;
|
|
1635
1908
|
ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
|
|
1636
1909
|
const labelPaddingX = 8;
|
|
1637
1910
|
const labelHeight = 20;
|
|
1638
|
-
const measuredLabelWidth = mergedLine.label === void 0 ? getPriceLabelWidth(labelText, labelPaddingX) : Math.ceil(
|
|
1911
|
+
const measuredLabelWidth = mergedLine.label === void 0 ? getPriceLabelWidth(labelText, labelPaddingX) : Math.ceil(measureTextWidth(labelText)) + labelPaddingX * 2;
|
|
1639
1912
|
const labelWidth = getRightAxisLabelWidth(chartRight, measuredLabelWidth);
|
|
1640
1913
|
const labelX = getRightAxisLabelX(chartRight);
|
|
1641
1914
|
const labelY = placeRightAxisLabel(lineY - labelHeight / 2, labelHeight, chartTop, chartBottom - labelHeight);
|
|
@@ -1730,7 +2003,7 @@ function createChart(element, options = {}) {
|
|
|
1730
2003
|
const closeAction = mergedLine.type === "market" ? "close" : "cancel";
|
|
1731
2004
|
const showCloseButton = mergedLine.showCloseButton;
|
|
1732
2005
|
const actionButtonText = mergedLine.actionButtonText;
|
|
1733
|
-
const axis =
|
|
2006
|
+
const axis = resolvedAxis;
|
|
1734
2007
|
ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
|
|
1735
2008
|
const legacyButton = actionButtonText ? [
|
|
1736
2009
|
{
|
|
@@ -1752,7 +2025,7 @@ function createChart(element, options = {}) {
|
|
|
1752
2025
|
const actionButtonMetrics = actionButtons.map((button) => {
|
|
1753
2026
|
const paddingX = Math.max(2, button.paddingX ?? mergedLine.actionButtonPaddingX);
|
|
1754
2027
|
const minWidth = Math.max(16, button.minWidth ?? mergedLine.actionButtonMinWidth);
|
|
1755
|
-
const width2 = Math.max(minWidth, Math.ceil(
|
|
2028
|
+
const width2 = Math.max(minWidth, Math.ceil(measureTextWidth(button.text)) + paddingX * 2);
|
|
1756
2029
|
return { button, width: width2 };
|
|
1757
2030
|
});
|
|
1758
2031
|
const actionButtonInnerGap = actionButtonMetrics.length > 1 ? Math.max(0, mergedLine.actionButtonsInnerGap) : 0;
|
|
@@ -1760,8 +2033,8 @@ function createChart(element, options = {}) {
|
|
|
1760
2033
|
const actionButtonsGap = actionButtonMetrics.length > 0 ? Math.max(0, mergedLine.actionButtonsGroupGap) : 0;
|
|
1761
2034
|
const segmentPaddingX = 8;
|
|
1762
2035
|
const labelHeight = 22;
|
|
1763
|
-
const qtyWidth = qtyText ? Math.ceil(
|
|
1764
|
-
const centerMeasuredWidth = Math.ceil(
|
|
2036
|
+
const qtyWidth = qtyText ? Math.ceil(measureTextWidth(qtyText)) + segmentPaddingX * 2 : 0;
|
|
2037
|
+
const centerMeasuredWidth = Math.ceil(measureTextWidth(centerText)) + segmentPaddingX * 2;
|
|
1765
2038
|
const centerWidth = mergedLine.id === void 0 ? centerMeasuredWidth : Math.max(centerMeasuredWidth, orderWidgetWidthById.get(mergedLine.id) ?? 0);
|
|
1766
2039
|
if (mergedLine.id) {
|
|
1767
2040
|
orderWidgetWidthById.set(mergedLine.id, centerWidth);
|
|
@@ -1769,7 +2042,7 @@ function createChart(element, options = {}) {
|
|
|
1769
2042
|
const closeWidth = showCloseButton ? 24 : 0;
|
|
1770
2043
|
const mainWidgetWidth = qtyWidth + centerWidth + closeWidth;
|
|
1771
2044
|
const totalWidth = mainWidgetWidth + actionButtonsGap + actionButtonsTotalWidth;
|
|
1772
|
-
const crosshair =
|
|
2045
|
+
const crosshair = resolvedCrosshair;
|
|
1773
2046
|
const crosshairActionInset = crosshair.visible && crosshair.showPriceLabel && crosshair.showPriceActionButton ? Math.max(
|
|
1774
2047
|
0,
|
|
1775
2048
|
Math.round(crosshair.priceActionButtonGap) + (clamp(Math.round(crosshair.priceActionButtonSize), 12, Math.max(12, labelHeight - 4)) + 10) - 4
|
|
@@ -1950,20 +2223,29 @@ function createChart(element, options = {}) {
|
|
|
1950
2223
|
}
|
|
1951
2224
|
}
|
|
1952
2225
|
crosshairPoint = point;
|
|
1953
|
-
scheduleDraw();
|
|
2226
|
+
scheduleDraw({ overlayOnly: true });
|
|
1954
2227
|
};
|
|
1955
2228
|
let drawRafId = null;
|
|
1956
2229
|
let pendingDrawUpdateAutoScale = false;
|
|
2230
|
+
let pendingDrawOverlayOnly = true;
|
|
1957
2231
|
const scheduleDraw = (options2 = {}) => {
|
|
1958
|
-
|
|
2232
|
+
const overlayOnly = options2.overlayOnly ?? false;
|
|
2233
|
+
pendingDrawOverlayOnly = pendingDrawOverlayOnly && overlayOnly;
|
|
2234
|
+
pendingDrawUpdateAutoScale = pendingDrawUpdateAutoScale || !overlayOnly && (options2.updateAutoScale ?? true);
|
|
1959
2235
|
if (drawRafId !== null) {
|
|
1960
2236
|
return;
|
|
1961
2237
|
}
|
|
1962
2238
|
drawRafId = requestAnimationFrame(() => {
|
|
1963
2239
|
drawRafId = null;
|
|
1964
2240
|
const updateAutoScale = pendingDrawUpdateAutoScale;
|
|
2241
|
+
const flushOverlayOnly = pendingDrawOverlayOnly;
|
|
1965
2242
|
pendingDrawUpdateAutoScale = false;
|
|
1966
|
-
|
|
2243
|
+
pendingDrawOverlayOnly = true;
|
|
2244
|
+
if (flushOverlayOnly) {
|
|
2245
|
+
drawOverlayOnly();
|
|
2246
|
+
} else {
|
|
2247
|
+
draw({ updateAutoScale });
|
|
2248
|
+
}
|
|
1967
2249
|
});
|
|
1968
2250
|
};
|
|
1969
2251
|
const draw = (options2 = {}) => {
|
|
@@ -1977,15 +2259,15 @@ function createChart(element, options = {}) {
|
|
|
1977
2259
|
canvas.width = Math.floor(width * pixelRatio);
|
|
1978
2260
|
canvas.height = Math.floor(height * pixelRatio);
|
|
1979
2261
|
ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
|
|
1980
|
-
const axis =
|
|
1981
|
-
const xAxis =
|
|
1982
|
-
const yAxis =
|
|
2262
|
+
const axis = resolvedAxis;
|
|
2263
|
+
const xAxis = resolvedXAxis;
|
|
2264
|
+
const yAxis = resolvedYAxis;
|
|
1983
2265
|
const xAxisFontSize = Math.max(8, xAxis.fontSize);
|
|
1984
2266
|
const yAxisFontSize = Math.max(8, yAxis.fontSize);
|
|
1985
2267
|
ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
|
|
1986
2268
|
ctx.fillStyle = mergedOptions.backgroundColor;
|
|
1987
2269
|
ctx.fillRect(0, 0, width, height);
|
|
1988
|
-
const labels =
|
|
2270
|
+
const labels = resolvedLabels;
|
|
1989
2271
|
const estimateRightMargin = () => {
|
|
1990
2272
|
const paddingX = Math.max(4, labels.labelPaddingX);
|
|
1991
2273
|
let required = margin.right - 1;
|
|
@@ -2098,6 +2380,7 @@ function createChart(element, options = {}) {
|
|
|
2098
2380
|
yMax: 0
|
|
2099
2381
|
};
|
|
2100
2382
|
drawText("Load OHLC data to render chart", chartLeft + 10, chartTop + 20, "left", "middle", "#64748b");
|
|
2383
|
+
overlayLayout = null;
|
|
2101
2384
|
return;
|
|
2102
2385
|
}
|
|
2103
2386
|
clampXViewport();
|
|
@@ -2105,62 +2388,39 @@ function createChart(element, options = {}) {
|
|
|
2105
2388
|
const xEnd = xStart + xSpan;
|
|
2106
2389
|
const startIndex = Math.max(0, Math.floor(xStart));
|
|
2107
2390
|
const endIndex = Math.min(data.length - 1, Math.ceil(xEnd) - 1);
|
|
2108
|
-
const
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
const fallback = data.slice(fallbackStart, latestIndex);
|
|
2119
|
-
if (fallback.length > 0) {
|
|
2120
|
-
priceSource = fallback;
|
|
2121
|
-
}
|
|
2391
|
+
const skipLatestIndex = mergedOptions.autoScaleIgnoreLatestCandle && data.length > 1 ? data.length - 1 : -1;
|
|
2392
|
+
const scanCandleRange = (from, to) => {
|
|
2393
|
+
let min = Number.POSITIVE_INFINITY;
|
|
2394
|
+
let max = Number.NEGATIVE_INFINITY;
|
|
2395
|
+
for (let idx = from; idx <= to; idx += 1) {
|
|
2396
|
+
if (idx === skipLatestIndex) continue;
|
|
2397
|
+
const point = data[idx];
|
|
2398
|
+
if (!point) continue;
|
|
2399
|
+
if (point.l < min) min = point.l;
|
|
2400
|
+
if (point.h > max) max = point.h;
|
|
2122
2401
|
}
|
|
2402
|
+
return min <= max ? { min, max } : null;
|
|
2403
|
+
};
|
|
2404
|
+
let candleRange = scanCandleRange(startIndex, endIndex);
|
|
2405
|
+
if (!candleRange) {
|
|
2406
|
+
const latestIndex = data.length - 1;
|
|
2407
|
+
candleRange = scanCandleRange(Math.max(0, latestIndex - 120), latestIndex - 1) ?? scanCandleRange(0, latestIndex) ?? { min: data[latestIndex].l, max: data[latestIndex].h };
|
|
2123
2408
|
}
|
|
2124
|
-
let minPrice =
|
|
2125
|
-
let maxPrice =
|
|
2409
|
+
let minPrice = candleRange.min;
|
|
2410
|
+
let maxPrice = candleRange.max;
|
|
2126
2411
|
if (overlayIndicatorsForScale.length > 0) {
|
|
2127
|
-
for (const { indicator } of overlayIndicatorsForScale) {
|
|
2412
|
+
for (const { indicator, plugin } of overlayIndicatorsForScale) {
|
|
2128
2413
|
if (indicator.excludeFromAutoscale) {
|
|
2129
2414
|
continue;
|
|
2130
2415
|
}
|
|
2131
|
-
const
|
|
2132
|
-
|
|
2133
|
-
const source = inputs.source ?? "close";
|
|
2134
|
-
const length = clampIndicatorLength(inputs.length ?? 14, 14);
|
|
2135
|
-
let series = null;
|
|
2136
|
-
if (type === "sma") series = withCachedSeries(`sma|${length}|${source}`, data, () => computeSmaSeries(data, length, source));
|
|
2137
|
-
if (type === "ema") series = withCachedSeries(`ema|${length}|${source}`, data, () => computeEmaSeries(data, length, source));
|
|
2138
|
-
if (type === "wma") series = withCachedSeries(`wma|${length}|${source}`, data, () => computeWmaSeries(data, length, source));
|
|
2139
|
-
if (type === "vwma") series = withCachedSeries(`vwma|${length}|${source}`, data, () => computeVwmaSeries(data, length, source));
|
|
2140
|
-
if (type === "rma") series = withCachedSeries(`rma|${length}|${source}`, data, () => computeRmaSeries(data, length, source));
|
|
2141
|
-
if (type === "hma") series = withCachedSeries(`hma|${length}|${source}`, data, () => computeHmaSeries(data, length, source));
|
|
2142
|
-
if (!series) {
|
|
2416
|
+
const range = plugin.getAutoscaleRange?.(data, startIndex, endIndex, indicator.inputs, skipLatestIndex);
|
|
2417
|
+
if (!range || !Number.isFinite(range.min) || !Number.isFinite(range.max)) {
|
|
2143
2418
|
continue;
|
|
2144
2419
|
}
|
|
2145
|
-
const visibleValues = [];
|
|
2146
|
-
for (let idx = startIndex; idx <= endIndex; idx += 1) {
|
|
2147
|
-
if (mergedOptions.autoScaleIgnoreLatestCandle && data.length > 1 && idx === data.length - 1) {
|
|
2148
|
-
continue;
|
|
2149
|
-
}
|
|
2150
|
-
const value = series[idx];
|
|
2151
|
-
if (Number.isFinite(value ?? Number.NaN)) {
|
|
2152
|
-
visibleValues.push(value);
|
|
2153
|
-
}
|
|
2154
|
-
}
|
|
2155
|
-
if (visibleValues.length === 0) {
|
|
2156
|
-
continue;
|
|
2157
|
-
}
|
|
2158
|
-
const seriesMin = Math.min(...visibleValues);
|
|
2159
|
-
const seriesMax = Math.max(...visibleValues);
|
|
2160
2420
|
const weight = Math.min(1, Math.max(0, indicator.overlayScaleWeight));
|
|
2161
2421
|
const currentMid = (minPrice + maxPrice) / 2;
|
|
2162
|
-
const weightedMin = currentMid + (
|
|
2163
|
-
const weightedMax = currentMid + (
|
|
2422
|
+
const weightedMin = currentMid + (range.min - currentMid) * weight;
|
|
2423
|
+
const weightedMax = currentMid + (range.max - currentMid) * weight;
|
|
2164
2424
|
minPrice = Math.min(minPrice, weightedMin);
|
|
2165
2425
|
maxPrice = Math.max(maxPrice, weightedMax);
|
|
2166
2426
|
}
|
|
@@ -2235,7 +2495,7 @@ function createChart(element, options = {}) {
|
|
|
2235
2495
|
ctx.save();
|
|
2236
2496
|
const prevFont = ctx.font;
|
|
2237
2497
|
ctx.font = `500 12px ${mergedOptions.fontFamily}`;
|
|
2238
|
-
const textWidth =
|
|
2498
|
+
const textWidth = measureTextWidth(labelText);
|
|
2239
2499
|
const paddingX = 6;
|
|
2240
2500
|
const bgWidth = textWidth + paddingX * 2;
|
|
2241
2501
|
const bgHeight = 18;
|
|
@@ -2401,7 +2661,7 @@ function createChart(element, options = {}) {
|
|
|
2401
2661
|
ctx.font = `500 11px ${mergedOptions.fontFamily}`;
|
|
2402
2662
|
levelLines.forEach((level, index) => {
|
|
2403
2663
|
const labelText = `${level.ratio} (${formatPrice(level.price)})`;
|
|
2404
|
-
const textWidth =
|
|
2664
|
+
const textWidth = measureTextWidth(labelText);
|
|
2405
2665
|
const padding = 4;
|
|
2406
2666
|
const bgX = lineLeft + 4;
|
|
2407
2667
|
const bgY = level.y - 9;
|
|
@@ -2474,7 +2734,7 @@ function createChart(element, options = {}) {
|
|
|
2474
2734
|
ctx.font = `500 11px ${mergedOptions.fontFamily}`;
|
|
2475
2735
|
levelLines.forEach((level, index) => {
|
|
2476
2736
|
const labelText = `${level.ratio} (${formatPrice(level.price)})`;
|
|
2477
|
-
const textWidth =
|
|
2737
|
+
const textWidth = measureTextWidth(labelText);
|
|
2478
2738
|
const padding = 4;
|
|
2479
2739
|
const bgX = chartLeft + 4;
|
|
2480
2740
|
const bgY = level.y - 9;
|
|
@@ -2617,8 +2877,8 @@ function createChart(element, options = {}) {
|
|
|
2617
2877
|
const padding = 6;
|
|
2618
2878
|
const lineH = 14;
|
|
2619
2879
|
const isDigit = (ch) => ch >= "0" && ch <= "9";
|
|
2620
|
-
const digitW =
|
|
2621
|
-
const charAdvance = (ch) => isDigit(ch) ? digitW :
|
|
2880
|
+
const digitW = measureTextWidth("0");
|
|
2881
|
+
const charAdvance = (ch) => isDigit(ch) ? digitW : measureTextWidth(ch);
|
|
2622
2882
|
const lineWidth = (line) => {
|
|
2623
2883
|
let w = 0;
|
|
2624
2884
|
for (const ch of line) w += charAdvance(ch);
|
|
@@ -2641,11 +2901,11 @@ function createChart(element, options = {}) {
|
|
|
2641
2901
|
const y = startY + lineIndex * lineH;
|
|
2642
2902
|
for (const ch of line) {
|
|
2643
2903
|
if (isDigit(ch)) {
|
|
2644
|
-
ctx.fillText(ch, x + (digitW -
|
|
2904
|
+
ctx.fillText(ch, x + (digitW - measureTextWidth(ch)) / 2, y);
|
|
2645
2905
|
x += digitW;
|
|
2646
2906
|
} else {
|
|
2647
2907
|
ctx.fillText(ch, x, y);
|
|
2648
|
-
x +=
|
|
2908
|
+
x += measureTextWidth(ch);
|
|
2649
2909
|
}
|
|
2650
2910
|
}
|
|
2651
2911
|
});
|
|
@@ -2757,15 +3017,31 @@ function createChart(element, options = {}) {
|
|
|
2757
3017
|
const pct = diff / base * 100;
|
|
2758
3018
|
const ticks = tick > 0 ? Math.round(diff / tick) : 0;
|
|
2759
3019
|
const signed = (value, text) => `${value < 0 ? "\u2212" : value > 0 ? "+" : ""}${text}`;
|
|
2760
|
-
const
|
|
3020
|
+
const labelLines = [
|
|
3021
|
+
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)}%`)})`
|
|
3022
|
+
];
|
|
3023
|
+
const barSpan = Math.abs(Math.round(p1.index) - Math.round(p0.index));
|
|
3024
|
+
if (barSpan > 0) {
|
|
3025
|
+
const t0 = getTimeForIndex(Math.round(p0.index));
|
|
3026
|
+
const t1 = getTimeForIndex(Math.round(p1.index));
|
|
3027
|
+
const spanMs = t0 && t1 ? Math.abs(t1.getTime() - t0.getTime()) : 0;
|
|
3028
|
+
labelLines.push(spanMs > 0 ? `${barSpan} bars, ${formatDuration(spanMs)}` : `${barSpan} bars`);
|
|
3029
|
+
}
|
|
3030
|
+
const pointValue = Number(drawing.pointValue);
|
|
3031
|
+
if (Number.isFinite(pointValue) && pointValue > 0) {
|
|
3032
|
+
const dollars = diff * pointValue;
|
|
3033
|
+
const money = Math.abs(dollars).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
3034
|
+
labelLines.push(`${signed(dollars, `$${money}`)} / contract`);
|
|
3035
|
+
}
|
|
2761
3036
|
const prevFont = ctx.font;
|
|
2762
3037
|
ctx.font = `500 11px ${mergedOptions.fontFamily}`;
|
|
2763
3038
|
const padding = 6;
|
|
2764
|
-
const
|
|
3039
|
+
const lineHeight = 15;
|
|
3040
|
+
const textW = Math.max(...labelLines.map((line) => measureTextWidth(line)));
|
|
2765
3041
|
const pillW = textW + padding * 2;
|
|
2766
|
-
const pillH =
|
|
3042
|
+
const pillH = labelLines.length * lineHeight + padding;
|
|
2767
3043
|
const pillX = midX - pillW / 2;
|
|
2768
|
-
const pillY = botY + 6;
|
|
3044
|
+
const pillY = botY + 6 + pillH <= fullChartBottom - 2 ? botY + 6 : Math.max(chartTop + 2, topY - 6 - pillH);
|
|
2769
3045
|
ctx.fillStyle = mergedOptions.backgroundColor;
|
|
2770
3046
|
fillRoundedRect(pillX, pillY, pillW, pillH, 4);
|
|
2771
3047
|
ctx.save();
|
|
@@ -2776,7 +3052,9 @@ function createChart(element, options = {}) {
|
|
|
2776
3052
|
ctx.fillStyle = drawing.color;
|
|
2777
3053
|
ctx.textAlign = "center";
|
|
2778
3054
|
ctx.textBaseline = "middle";
|
|
2779
|
-
|
|
3055
|
+
labelLines.forEach((line, lineIndex) => {
|
|
3056
|
+
ctx.fillText(line, midX, pillY + padding / 2 + lineIndex * lineHeight + lineHeight / 2);
|
|
3057
|
+
});
|
|
2780
3058
|
ctx.font = prevFont;
|
|
2781
3059
|
if (drawing.label) {
|
|
2782
3060
|
drawDrawingLabel(drawing.label, midX, topY - 4, drawing.color);
|
|
@@ -2795,7 +3073,7 @@ function createChart(element, options = {}) {
|
|
|
2795
3073
|
ctx.textBaseline = "middle";
|
|
2796
3074
|
const lines = (drawing.label ?? "").split("\n");
|
|
2797
3075
|
const lineH = Math.round(fontSize * 1.35);
|
|
2798
|
-
const textW = Math.max(1, ...lines.map((line) =>
|
|
3076
|
+
const textW = Math.max(1, ...lines.map((line) => measureTextWidth(line)));
|
|
2799
3077
|
const padX = isNote ? 8 : 2;
|
|
2800
3078
|
const padY = isNote ? 6 : 1;
|
|
2801
3079
|
const blockW = textW + padX * 2;
|
|
@@ -2877,47 +3155,114 @@ function createChart(element, options = {}) {
|
|
|
2877
3155
|
Math.max(1, candleSpacing - 1),
|
|
2878
3156
|
Math.max(candleMinWidth, Math.floor(candleSpacing * candleBodyWidthRatio))
|
|
2879
3157
|
);
|
|
2880
|
-
const grid =
|
|
3158
|
+
const grid = resolvedGrid;
|
|
2881
3159
|
const gridOpacity = clamp(grid.opacity, 0, 1);
|
|
2882
3160
|
const yTickCountInput = grid.yTickCount ?? grid.horizontalTickCount;
|
|
2883
3161
|
const yTicks = Math.max(1, Math.floor(yTickCountInput));
|
|
2884
3162
|
const gridColor = grid.color ?? mergedOptions.gridColor;
|
|
3163
|
+
const priceTicks = [];
|
|
3164
|
+
{
|
|
3165
|
+
const targetStep = yRange / Math.max(1, yTicks);
|
|
3166
|
+
let step;
|
|
3167
|
+
if (targetStep > 0 && Number.isFinite(targetStep)) {
|
|
3168
|
+
const base = Math.pow(10, Math.floor(Math.log10(targetStep)));
|
|
3169
|
+
const frac = targetStep / base;
|
|
3170
|
+
step = (frac <= 1 ? 1 : frac <= 2 ? 2 : frac <= 2.5 ? 2.5 : frac <= 5 ? 5 : 10) * base;
|
|
3171
|
+
const instrumentTick = getConfiguredTickSize();
|
|
3172
|
+
if (instrumentTick > 0 && step < instrumentTick * 1e6) {
|
|
3173
|
+
step = Math.max(instrumentTick, Math.round(step / instrumentTick) * instrumentTick);
|
|
3174
|
+
}
|
|
3175
|
+
} else {
|
|
3176
|
+
step = 1;
|
|
3177
|
+
}
|
|
3178
|
+
const firstTick = Math.ceil(yMin / step) * step;
|
|
3179
|
+
for (let i = 0; ; i += 1) {
|
|
3180
|
+
const price = firstTick + i * step;
|
|
3181
|
+
if (price > yMax + step * 1e-6) break;
|
|
3182
|
+
priceTicks.push(price);
|
|
3183
|
+
if (priceTicks.length > 200) break;
|
|
3184
|
+
}
|
|
3185
|
+
}
|
|
2885
3186
|
if (grid.horizontalLines) {
|
|
2886
3187
|
ctx.save();
|
|
2887
3188
|
ctx.globalAlpha = gridOpacity;
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
3189
|
+
ctx.strokeStyle = gridColor;
|
|
3190
|
+
ctx.lineWidth = 1;
|
|
3191
|
+
ctx.beginPath();
|
|
3192
|
+
for (const price of priceTicks) {
|
|
2891
3193
|
const y = yFromPrice(price);
|
|
2892
|
-
ctx.strokeStyle = gridColor;
|
|
2893
|
-
ctx.lineWidth = 1;
|
|
2894
|
-
ctx.beginPath();
|
|
2895
3194
|
ctx.moveTo(crisp(chartLeft), crisp(y));
|
|
2896
3195
|
ctx.lineTo(crisp(chartRight), crisp(y));
|
|
2897
|
-
ctx.stroke();
|
|
2898
3196
|
}
|
|
3197
|
+
ctx.stroke();
|
|
2899
3198
|
ctx.restore();
|
|
2900
3199
|
}
|
|
2901
|
-
const minLabelSpacingPx = Math.max(
|
|
2902
|
-
const
|
|
2903
|
-
const xTickCountInput = grid.xTickCount ?? autoLabelCount;
|
|
2904
|
-
const xTickCount = Math.max(2, Math.floor(xTickCountInput));
|
|
2905
|
-
const rawStep = xSpan / xTickCount;
|
|
2906
|
-
const xStep = Math.max(1, Math.ceil(rawStep));
|
|
2907
|
-
const visibleTickStart = Math.floor(xStart);
|
|
3200
|
+
const minLabelSpacingPx = Math.max(60, Math.min(120, chartWidth / Math.max(2, Math.floor(grid.xTickCount ?? 8))));
|
|
3201
|
+
const visibleTickStart = Math.max(0, Math.floor(xStart));
|
|
2908
3202
|
const visibleTickEnd = Math.ceil(xEnd) - 1;
|
|
2909
|
-
const
|
|
2910
|
-
|
|
3203
|
+
const timeTicks = [];
|
|
3204
|
+
{
|
|
3205
|
+
const weightBuckets = [[], [], [], [], [], [], [], [], [], []];
|
|
3206
|
+
let prevTime = visibleTickStart > 0 ? getTimeForIndex(visibleTickStart - 1) : null;
|
|
3207
|
+
for (let index = visibleTickStart; index <= visibleTickEnd; index += 1) {
|
|
3208
|
+
const time = getTimeForIndex(index);
|
|
3209
|
+
if (!time) {
|
|
3210
|
+
prevTime = null;
|
|
3211
|
+
continue;
|
|
3212
|
+
}
|
|
3213
|
+
let weight = 0;
|
|
3214
|
+
if (!prevTime) {
|
|
3215
|
+
weight = 7;
|
|
3216
|
+
} else {
|
|
3217
|
+
if (prevTime.getFullYear() !== time.getFullYear()) weight = 9;
|
|
3218
|
+
else if (prevTime.getMonth() !== time.getMonth()) weight = 8;
|
|
3219
|
+
else if (prevTime.getDate() !== time.getDate()) weight = 7;
|
|
3220
|
+
else {
|
|
3221
|
+
const prevMinutes = prevTime.getHours() * 60 + prevTime.getMinutes();
|
|
3222
|
+
const curMinutes = time.getHours() * 60 + time.getMinutes();
|
|
3223
|
+
const crossed = (span) => Math.floor(prevMinutes / span) !== Math.floor(curMinutes / span);
|
|
3224
|
+
if (crossed(360)) weight = 6;
|
|
3225
|
+
else if (crossed(60)) weight = 5;
|
|
3226
|
+
else if (crossed(30)) weight = 4;
|
|
3227
|
+
else if (crossed(15)) weight = 3;
|
|
3228
|
+
else if (crossed(5)) weight = 2;
|
|
3229
|
+
else weight = 1;
|
|
3230
|
+
}
|
|
3231
|
+
}
|
|
3232
|
+
weightBuckets[weight].push(index);
|
|
3233
|
+
prevTime = time;
|
|
3234
|
+
}
|
|
3235
|
+
const placedXs = [];
|
|
3236
|
+
const maxMarks = Math.floor(chartWidth / minLabelSpacingPx) + 2;
|
|
3237
|
+
for (let weight = 9; weight >= 1 && timeTicks.length < maxMarks; weight -= 1) {
|
|
3238
|
+
for (const index of weightBuckets[weight]) {
|
|
3239
|
+
const x = chartLeft + (index + 0.5 - xStart) / xSpan * chartWidth;
|
|
3240
|
+
let fits = true;
|
|
3241
|
+
for (const placedX of placedXs) {
|
|
3242
|
+
if (Math.abs(placedX - x) < minLabelSpacingPx) {
|
|
3243
|
+
fits = false;
|
|
3244
|
+
break;
|
|
3245
|
+
}
|
|
3246
|
+
}
|
|
3247
|
+
if (!fits) continue;
|
|
3248
|
+
placedXs.push(x);
|
|
3249
|
+
timeTicks.push({ index, weight, x });
|
|
3250
|
+
if (timeTicks.length >= maxMarks) break;
|
|
3251
|
+
}
|
|
3252
|
+
}
|
|
3253
|
+
timeTicks.sort((a, b) => a.index - b.index);
|
|
3254
|
+
}
|
|
3255
|
+
if (grid.verticalLines && timeTicks.length > 0) {
|
|
2911
3256
|
ctx.save();
|
|
2912
3257
|
ctx.globalAlpha = gridOpacity;
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
ctx.moveTo(crisp(x), crisp(chartTop));
|
|
2918
|
-
ctx.lineTo(crisp(x), crisp(fullChartBottom));
|
|
2919
|
-
ctx.stroke();
|
|
3258
|
+
ctx.strokeStyle = gridColor;
|
|
3259
|
+
ctx.lineWidth = 1;
|
|
3260
|
+
ctx.beginPath();
|
|
3261
|
+
for (const tick of timeTicks) {
|
|
3262
|
+
ctx.moveTo(crisp(tick.x), crisp(chartTop));
|
|
3263
|
+
ctx.lineTo(crisp(tick.x), crisp(fullChartBottom));
|
|
2920
3264
|
}
|
|
3265
|
+
ctx.stroke();
|
|
2921
3266
|
ctx.restore();
|
|
2922
3267
|
}
|
|
2923
3268
|
if (grid.sessionSeparators && data.length > 1) {
|
|
@@ -2955,38 +3300,45 @@ function createChart(element, options = {}) {
|
|
|
2955
3300
|
}
|
|
2956
3301
|
return data[index]?.v;
|
|
2957
3302
|
};
|
|
2958
|
-
|
|
2959
|
-
const
|
|
2960
|
-
|
|
2961
|
-
|
|
3303
|
+
{
|
|
3304
|
+
const upWicks = new Path2D();
|
|
3305
|
+
const downWicks = new Path2D();
|
|
3306
|
+
const upBodies = new Path2D();
|
|
3307
|
+
const downBodies = new Path2D();
|
|
3308
|
+
for (let index = startIndex; index <= endIndex; index += 1) {
|
|
3309
|
+
const point = data[index];
|
|
3310
|
+
if (!point) {
|
|
3311
|
+
continue;
|
|
3312
|
+
}
|
|
3313
|
+
const isLastCandle = useSmoothedCandle && index === lastDataIndex;
|
|
3314
|
+
const actualDirection = point.c >= point.o ? "up" : "down";
|
|
3315
|
+
const displayClose = isLastCandle ? actualDirection === "up" ? Math.max(point.o, smoothedTickerPrice) : Math.min(point.o, smoothedTickerPrice) : point.c;
|
|
3316
|
+
const displayHigh = isLastCandle ? actualDirection === "up" ? Math.max(point.h, displayClose) : point.h : point.h;
|
|
3317
|
+
const displayLow = isLastCandle ? actualDirection === "up" ? point.l : Math.min(point.l, displayClose) : point.l;
|
|
3318
|
+
const centerX = chartLeft + (index + 0.5 - xStart) / xSpan * chartWidth;
|
|
3319
|
+
const direction = isLastCandle ? actualDirection : getCandleDirectionByIndex(index);
|
|
3320
|
+
const isUp = direction === "up";
|
|
3321
|
+
const roundedCenterX = Math.round(centerX);
|
|
3322
|
+
const wicks = isUp ? upWicks : downWicks;
|
|
3323
|
+
wicks.moveTo(roundedCenterX + 0.5, crisp(yFromPrice(displayHigh)));
|
|
3324
|
+
wicks.lineTo(roundedCenterX + 0.5, crisp(yFromPrice(displayLow)));
|
|
3325
|
+
const bodyLeft = roundedCenterX - Math.floor(bodyWidth / 2);
|
|
3326
|
+
const openYPx = Math.round(yFromPrice(point.o));
|
|
3327
|
+
const closeYPx = Math.round(yFromPrice(displayClose));
|
|
3328
|
+
const bodyIsUp = displayClose >= point.o;
|
|
3329
|
+
const bodyTop = bodyIsUp ? Math.min(closeYPx, openYPx - 1) : openYPx;
|
|
3330
|
+
const bodyBottom = bodyIsUp ? openYPx : Math.max(closeYPx, openYPx + 1);
|
|
3331
|
+
(isUp ? upBodies : downBodies).rect(bodyLeft, bodyTop, bodyWidth, bodyBottom - bodyTop);
|
|
2962
3332
|
}
|
|
2963
|
-
const isLastCandle = useSmoothedCandle && index === lastDataIndex;
|
|
2964
|
-
const actualDirection = point.c >= point.o ? "up" : "down";
|
|
2965
|
-
const displayClose = isLastCandle ? actualDirection === "up" ? Math.max(point.o, smoothedTickerPrice) : Math.min(point.o, smoothedTickerPrice) : point.c;
|
|
2966
|
-
const displayHigh = isLastCandle ? actualDirection === "up" ? Math.max(point.h, displayClose) : point.h : point.h;
|
|
2967
|
-
const displayLow = isLastCandle ? actualDirection === "up" ? point.l : Math.min(point.l, displayClose) : point.l;
|
|
2968
|
-
const centerX = chartLeft + (index + 0.5 - xStart) / xSpan * chartWidth;
|
|
2969
|
-
const openY = yFromPrice(point.o);
|
|
2970
|
-
const closeY = yFromPrice(displayClose);
|
|
2971
|
-
const highY = yFromPrice(displayHigh);
|
|
2972
|
-
const lowY = yFromPrice(displayLow);
|
|
2973
|
-
const direction = isLastCandle ? actualDirection : getCandleDirectionByIndex(index);
|
|
2974
|
-
const candleColor = direction === "up" ? mergedOptions.upColor : mergedOptions.downColor;
|
|
2975
|
-
const roundedCenterX = Math.round(centerX);
|
|
2976
|
-
ctx.strokeStyle = candleColor;
|
|
2977
3333
|
ctx.lineWidth = candleWickWidth;
|
|
2978
|
-
ctx.
|
|
2979
|
-
ctx.
|
|
2980
|
-
ctx.
|
|
2981
|
-
ctx.
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
const bodyTop = bodyIsUp ? Math.min(closeYPx, openYPx - 1) : openYPx;
|
|
2987
|
-
const bodyBottom = bodyIsUp ? openYPx : Math.max(closeYPx, openYPx + 1);
|
|
2988
|
-
ctx.fillStyle = candleColor;
|
|
2989
|
-
ctx.fillRect(bodyLeft, bodyTop, bodyWidth, bodyBottom - bodyTop);
|
|
3334
|
+
ctx.strokeStyle = mergedOptions.upColor;
|
|
3335
|
+
ctx.stroke(upWicks);
|
|
3336
|
+
ctx.fillStyle = mergedOptions.upColor;
|
|
3337
|
+
ctx.fill(upBodies);
|
|
3338
|
+
ctx.strokeStyle = mergedOptions.downColor;
|
|
3339
|
+
ctx.stroke(downWicks);
|
|
3340
|
+
ctx.fillStyle = mergedOptions.downColor;
|
|
3341
|
+
ctx.fill(downBodies);
|
|
2990
3342
|
}
|
|
2991
3343
|
const activeOverlayIndicators = indicators.filter((indicator) => indicator.visible).map((indicator) => ({ indicator, plugin: indicatorRegistry.get(indicator.type) })).filter(
|
|
2992
3344
|
(value) => value.plugin !== void 0 && (value.indicator.pane ?? value.plugin.pane ?? "overlay") === "overlay"
|
|
@@ -3104,56 +3456,6 @@ function createChart(element, options = {}) {
|
|
|
3104
3456
|
ctx.textAlign = prevAlign;
|
|
3105
3457
|
ctx.textBaseline = prevBaseline;
|
|
3106
3458
|
}
|
|
3107
|
-
const crosshair = { ...DEFAULT_CROSSHAIR_OPTIONS, ...mergedOptions.crosshair ?? {} };
|
|
3108
|
-
if (crosshair.visible && crosshairPoint) {
|
|
3109
|
-
const cx = clamp(crosshairPoint.x, chartLeft, chartRight);
|
|
3110
|
-
const cy = clamp(crosshairPoint.y, chartTop, chartBottom);
|
|
3111
|
-
if (crosshair.mode === "dot") {
|
|
3112
|
-
ctx.save();
|
|
3113
|
-
ctx.fillStyle = crosshair.color;
|
|
3114
|
-
ctx.beginPath();
|
|
3115
|
-
ctx.arc(cx, cy, Math.max(1, crosshair.dotRadius), 0, Math.PI * 2);
|
|
3116
|
-
ctx.fill();
|
|
3117
|
-
ctx.restore();
|
|
3118
|
-
} else {
|
|
3119
|
-
ctx.save();
|
|
3120
|
-
ctx.strokeStyle = crosshair.color;
|
|
3121
|
-
ctx.lineWidth = Math.max(1, crosshair.width);
|
|
3122
|
-
applyDashPattern(crosshair.style, dashPatterns.dotted, dashPatterns.dashed);
|
|
3123
|
-
if (crosshair.showVertical) {
|
|
3124
|
-
ctx.beginPath();
|
|
3125
|
-
ctx.moveTo(crisp(cx), crisp(chartTop));
|
|
3126
|
-
ctx.lineTo(crisp(cx), crisp(chartBottom));
|
|
3127
|
-
ctx.stroke();
|
|
3128
|
-
}
|
|
3129
|
-
if (crosshair.showHorizontal) {
|
|
3130
|
-
ctx.beginPath();
|
|
3131
|
-
ctx.moveTo(crisp(chartLeft), crisp(cy));
|
|
3132
|
-
ctx.lineTo(crisp(chartRight), crisp(cy));
|
|
3133
|
-
ctx.stroke();
|
|
3134
|
-
}
|
|
3135
|
-
ctx.restore();
|
|
3136
|
-
}
|
|
3137
|
-
if (crosshair.sideHintLeft || crosshair.sideHintRight) {
|
|
3138
|
-
ctx.save();
|
|
3139
|
-
ctx.font = `600 11px ${mergedOptions.fontFamily}`;
|
|
3140
|
-
ctx.textBaseline = "middle";
|
|
3141
|
-
ctx.setLineDash([]);
|
|
3142
|
-
const hintY = clamp(cy - 14, chartTop + 8, chartBottom - 8);
|
|
3143
|
-
const hintGap = 8;
|
|
3144
|
-
if (crosshair.sideHintLeft) {
|
|
3145
|
-
ctx.fillStyle = crosshair.sideHintLeftColor;
|
|
3146
|
-
ctx.textAlign = "right";
|
|
3147
|
-
ctx.fillText(crosshair.sideHintLeft, cx - hintGap, hintY);
|
|
3148
|
-
}
|
|
3149
|
-
if (crosshair.sideHintRight) {
|
|
3150
|
-
ctx.fillStyle = crosshair.sideHintRightColor;
|
|
3151
|
-
ctx.textAlign = "left";
|
|
3152
|
-
ctx.fillText(crosshair.sideHintRight, cx + hintGap, hintY);
|
|
3153
|
-
}
|
|
3154
|
-
ctx.restore();
|
|
3155
|
-
}
|
|
3156
|
-
}
|
|
3157
3459
|
ctx.restore();
|
|
3158
3460
|
const positionAxisGutter = Math.max(0, width - chartRight);
|
|
3159
3461
|
if (positionAxisGutter > 0) {
|
|
@@ -3266,7 +3568,7 @@ function createChart(element, options = {}) {
|
|
|
3266
3568
|
const text = label.text ?? formatPaneValue(label.value);
|
|
3267
3569
|
const labelPaddingX = 7;
|
|
3268
3570
|
const labelHeight = Math.max(16, yAxisFontSize + 8);
|
|
3269
|
-
const labelWidth = getRightAxisLabelWidth(chartRight, Math.ceil(
|
|
3571
|
+
const labelWidth = getRightAxisLabelWidth(chartRight, Math.ceil(measureTextWidth(text)) + labelPaddingX * 2);
|
|
3270
3572
|
const labelX = getRightAxisLabelX(chartRight);
|
|
3271
3573
|
const labelY = clamp(yFromPaneValue(label.value) - labelHeight / 2, paneTop + 2, paneBottom - labelHeight - 2);
|
|
3272
3574
|
ctx.fillStyle = label.backgroundColor ?? label.color ?? labels.backgroundColor;
|
|
@@ -3278,18 +3580,6 @@ function createChart(element, options = {}) {
|
|
|
3278
3580
|
}
|
|
3279
3581
|
});
|
|
3280
3582
|
}
|
|
3281
|
-
if (crosshair.visible && crosshairPoint && crosshair.showVertical) {
|
|
3282
|
-
const cx = clamp(crosshairPoint.x, chartLeft, chartRight);
|
|
3283
|
-
ctx.save();
|
|
3284
|
-
ctx.strokeStyle = crosshair.color;
|
|
3285
|
-
ctx.lineWidth = Math.max(1, crosshair.width);
|
|
3286
|
-
applyDashPattern(crosshair.style, dashPatterns.dotted, dashPatterns.dashed);
|
|
3287
|
-
ctx.beginPath();
|
|
3288
|
-
ctx.moveTo(crisp(cx), crisp(chartTop));
|
|
3289
|
-
ctx.lineTo(crisp(cx), crisp(fullChartBottom));
|
|
3290
|
-
ctx.stroke();
|
|
3291
|
-
ctx.restore();
|
|
3292
|
-
}
|
|
3293
3583
|
ctx.strokeStyle = axis.lineColor;
|
|
3294
3584
|
ctx.lineWidth = Math.max(1, axis.lineWidth);
|
|
3295
3585
|
ctx.beginPath();
|
|
@@ -3299,13 +3589,13 @@ function createChart(element, options = {}) {
|
|
|
3299
3589
|
ctx.lineTo(crisp(chartRight), crisp(fullChartBottom));
|
|
3300
3590
|
ctx.stroke();
|
|
3301
3591
|
const priceScaleTickLabelInset = yAxisFontSize / 2 + 3;
|
|
3302
|
-
|
|
3303
|
-
const ratio = tick / yTicks;
|
|
3304
|
-
const price = yMin + yRange * ratio;
|
|
3305
|
-
const y = clamp(yFromPrice(price), chartTop + priceScaleTickLabelInset, chartBottom - priceScaleTickLabelInset);
|
|
3592
|
+
{
|
|
3306
3593
|
const prevFont = ctx.font;
|
|
3307
3594
|
ctx.font = `${yAxisFontSize}px ${mergedOptions.fontFamily}`;
|
|
3308
|
-
|
|
3595
|
+
for (const price of priceTicks) {
|
|
3596
|
+
const y = clamp(yFromPrice(price), chartTop + priceScaleTickLabelInset, chartBottom - priceScaleTickLabelInset);
|
|
3597
|
+
drawText(formatPrice(price), chartRight + 6, y, "left", "middle", yAxis.textColor);
|
|
3598
|
+
}
|
|
3309
3599
|
ctx.font = prevFont;
|
|
3310
3600
|
}
|
|
3311
3601
|
resetLabelSlots(labels.noOverlapping);
|
|
@@ -3414,9 +3704,15 @@ function createChart(element, options = {}) {
|
|
|
3414
3704
|
});
|
|
3415
3705
|
}
|
|
3416
3706
|
}
|
|
3417
|
-
if (labels.showHighLow &&
|
|
3418
|
-
|
|
3419
|
-
|
|
3707
|
+
if (labels.showHighLow && endIndex >= startIndex) {
|
|
3708
|
+
let visibleHigh = Number.NEGATIVE_INFINITY;
|
|
3709
|
+
let visibleLow = Number.POSITIVE_INFINITY;
|
|
3710
|
+
for (let idx = startIndex; idx <= endIndex; idx += 1) {
|
|
3711
|
+
const point = data[idx];
|
|
3712
|
+
if (!point) continue;
|
|
3713
|
+
if (point.h > visibleHigh) visibleHigh = point.h;
|
|
3714
|
+
if (point.l < visibleLow) visibleLow = point.l;
|
|
3715
|
+
}
|
|
3420
3716
|
addPriceAxisLabel({
|
|
3421
3717
|
text: `H ${formatPrice(visibleHigh)}`,
|
|
3422
3718
|
price: visibleHigh,
|
|
@@ -3469,12 +3765,12 @@ function createChart(element, options = {}) {
|
|
|
3469
3765
|
const subtextFontSize = label.subtextFontSize !== void 0 && label.subtextFontSize > 0 ? Math.max(8, Math.round(label.subtextFontSize)) : priceLabelFontSize;
|
|
3470
3766
|
const subtextLineGap = 5;
|
|
3471
3767
|
const labelHeight = baseLabelHeight + (subtexts.length > 0 ? subtexts.length * subtextFontSize + subtexts.length * subtextLineGap : 0);
|
|
3472
|
-
const primaryWidth = label.text === formatPrice(label.price) ? getPriceLabelWidth(label.text, labelPaddingX) : Math.ceil(
|
|
3768
|
+
const primaryWidth = label.text === formatPrice(label.price) ? getPriceLabelWidth(label.text, labelPaddingX) : Math.ceil(measureTextWidth(label.text)) + labelPaddingX * 2;
|
|
3473
3769
|
let subtextWidth = 0;
|
|
3474
3770
|
if (subtexts.length > 0) {
|
|
3475
3771
|
const baseFont = ctx.font;
|
|
3476
3772
|
ctx.font = `${subtextFontSize}px ${mergedOptions.fontFamily}`;
|
|
3477
|
-
subtextWidth = Math.max(...subtexts.map((subtext) => Math.ceil(
|
|
3773
|
+
subtextWidth = Math.max(...subtexts.map((subtext) => Math.ceil(measureTextWidth(subtext)) + labelPaddingX * 2));
|
|
3478
3774
|
ctx.font = baseFont;
|
|
3479
3775
|
}
|
|
3480
3776
|
const labelTextWidth = Math.max(primaryWidth, subtextWidth);
|
|
@@ -3552,8 +3848,9 @@ function createChart(element, options = {}) {
|
|
|
3552
3848
|
}
|
|
3553
3849
|
return typeof value === "string" && !/^#(?:[0-9a-fA-F]{3}){1,2}$/.test(value.trim());
|
|
3554
3850
|
};
|
|
3851
|
+
const LEGEND_EXCLUDED_INPUT_KEYS = /* @__PURE__ */ new Set(["width", "bandMultiplier", "bandFillOpacity", "fillOpacity"]);
|
|
3555
3852
|
const labelEntries = activeOverlayIndicators.map(({ indicator, plugin }) => {
|
|
3556
|
-
const inputValues = Object.entries(indicator.inputs).filter(([, value]) => isLegendInputValue(value)).slice(0, 2).map(([, value]) => String(value));
|
|
3853
|
+
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));
|
|
3557
3854
|
if (labels.showIndicatorNames && labels.showIndicatorValues && inputValues.length > 0) {
|
|
3558
3855
|
return `${plugin.name} ${inputValues.join(" ")}`;
|
|
3559
3856
|
}
|
|
@@ -3577,21 +3874,15 @@ function createChart(element, options = {}) {
|
|
|
3577
3874
|
ctx.font = prevFont;
|
|
3578
3875
|
}
|
|
3579
3876
|
}
|
|
3580
|
-
|
|
3581
|
-
const axisIntraday = axisStepMs > 0 && axisStepMs < 24 * 60 * 60 * 1e3;
|
|
3582
|
-
let prevAxisTickTime = null;
|
|
3583
|
-
for (let index = tickStartIndex; index <= visibleTickEnd; index += xStep) {
|
|
3584
|
-
const tickTime = getTimeForIndex(index);
|
|
3585
|
-
if (!tickTime) {
|
|
3586
|
-
continue;
|
|
3587
|
-
}
|
|
3588
|
-
const x = chartLeft + (index + 0.5 - xStart) / xSpan * chartWidth;
|
|
3589
|
-
const isNewDay = !prevAxisTickTime || prevAxisTickTime.getDate() !== tickTime.getDate() || prevAxisTickTime.getMonth() !== tickTime.getMonth() || prevAxisTickTime.getFullYear() !== tickTime.getFullYear();
|
|
3590
|
-
const timeLabel = axisIntraday && !isNewDay ? tickTime.toLocaleTimeString(void 0, { hour: "2-digit", minute: "2-digit", hour12: false }) : tickTime.toLocaleDateString(void 0, { month: "short", day: "numeric" });
|
|
3591
|
-
prevAxisTickTime = tickTime;
|
|
3877
|
+
{
|
|
3592
3878
|
const prevFont = ctx.font;
|
|
3593
3879
|
ctx.font = `${xAxisFontSize}px ${mergedOptions.fontFamily}`;
|
|
3594
|
-
|
|
3880
|
+
for (const tick of timeTicks) {
|
|
3881
|
+
const tickTime = getTimeForIndex(tick.index);
|
|
3882
|
+
if (!tickTime) continue;
|
|
3883
|
+
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 });
|
|
3884
|
+
drawText(timeLabel, tick.x, (fullChartBottom + height) / 2, "center", "middle", xAxis.textColor);
|
|
3885
|
+
}
|
|
3595
3886
|
ctx.font = prevFont;
|
|
3596
3887
|
}
|
|
3597
3888
|
if (labels.visible && labels.showCountdownToBarClose && lastPoint) {
|
|
@@ -3604,138 +3895,230 @@ function createChart(element, options = {}) {
|
|
|
3604
3895
|
drawText(countdownText, chartRight + 6, (fullChartBottom + height) / 2, "left", "middle", xAxis.textColor);
|
|
3605
3896
|
ctx.font = prevFont;
|
|
3606
3897
|
}
|
|
3607
|
-
if (
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3898
|
+
if (baseCtx) {
|
|
3899
|
+
if (baseCanvas.width !== canvas.width || baseCanvas.height !== canvas.height) {
|
|
3900
|
+
baseCanvas.width = canvas.width;
|
|
3901
|
+
baseCanvas.height = canvas.height;
|
|
3902
|
+
}
|
|
3903
|
+
baseCtx.setTransform(1, 0, 0, 1, 0, 0);
|
|
3904
|
+
baseCtx.clearRect(0, 0, baseCanvas.width, baseCanvas.height);
|
|
3905
|
+
baseCtx.drawImage(canvas, 0, 0);
|
|
3906
|
+
}
|
|
3907
|
+
overlayLayout = {
|
|
3908
|
+
chartLeft,
|
|
3909
|
+
chartTop,
|
|
3910
|
+
chartRight,
|
|
3911
|
+
chartBottom,
|
|
3912
|
+
chartWidth,
|
|
3913
|
+
chartHeight,
|
|
3914
|
+
fullChartBottom,
|
|
3915
|
+
yMin,
|
|
3916
|
+
yRange,
|
|
3917
|
+
xStart,
|
|
3918
|
+
xSpan
|
|
3919
|
+
};
|
|
3920
|
+
paintCrosshairOverlay();
|
|
3921
|
+
};
|
|
3922
|
+
const paintCrosshairOverlay = () => {
|
|
3923
|
+
crosshairPriceActionRegion = null;
|
|
3924
|
+
const layout = overlayLayout;
|
|
3925
|
+
const crosshair = resolvedCrosshair;
|
|
3926
|
+
if (!layout || !crosshair.visible || !crosshairPoint) {
|
|
3927
|
+
return;
|
|
3928
|
+
}
|
|
3929
|
+
const { chartLeft, chartTop, chartRight, chartBottom, chartWidth, chartHeight, fullChartBottom, yMin, yRange, xStart, xSpan: xSpan2 } = layout;
|
|
3930
|
+
const cx = clamp(crosshairPoint.x, chartLeft, chartRight);
|
|
3931
|
+
const cy = clamp(crosshairPoint.y, chartTop, chartBottom);
|
|
3932
|
+
ctx.save();
|
|
3933
|
+
ctx.beginPath();
|
|
3934
|
+
ctx.rect(chartLeft, chartTop, chartWidth, Math.max(0, chartBottom - chartTop));
|
|
3935
|
+
ctx.clip();
|
|
3936
|
+
if (crosshair.mode === "dot") {
|
|
3937
|
+
ctx.fillStyle = crosshair.color;
|
|
3938
|
+
ctx.beginPath();
|
|
3939
|
+
ctx.arc(cx, cy, Math.max(1, crosshair.dotRadius), 0, Math.PI * 2);
|
|
3940
|
+
ctx.fill();
|
|
3941
|
+
} else {
|
|
3942
|
+
ctx.strokeStyle = crosshair.color;
|
|
3943
|
+
ctx.lineWidth = Math.max(1, crosshair.width);
|
|
3944
|
+
applyDashPattern(crosshair.style, dashPatterns.dotted, dashPatterns.dashed);
|
|
3945
|
+
if (crosshair.showHorizontal) {
|
|
3946
|
+
ctx.beginPath();
|
|
3947
|
+
ctx.moveTo(crisp(chartLeft), crisp(cy));
|
|
3948
|
+
ctx.lineTo(crisp(chartRight), crisp(cy));
|
|
3949
|
+
ctx.stroke();
|
|
3950
|
+
}
|
|
3951
|
+
}
|
|
3952
|
+
if (crosshair.sideHintLeft || crosshair.sideHintRight) {
|
|
3953
|
+
ctx.save();
|
|
3954
|
+
ctx.font = `600 11px ${mergedOptions.fontFamily}`;
|
|
3955
|
+
ctx.textBaseline = "middle";
|
|
3956
|
+
ctx.setLineDash([]);
|
|
3957
|
+
const hintY = clamp(cy - 14, chartTop + 8, chartBottom - 8);
|
|
3958
|
+
const hintGap = 8;
|
|
3959
|
+
if (crosshair.sideHintLeft) {
|
|
3960
|
+
ctx.fillStyle = crosshair.sideHintLeftColor;
|
|
3961
|
+
ctx.textAlign = "right";
|
|
3962
|
+
ctx.fillText(crosshair.sideHintLeft, cx - hintGap, hintY);
|
|
3963
|
+
}
|
|
3964
|
+
if (crosshair.sideHintRight) {
|
|
3965
|
+
ctx.fillStyle = crosshair.sideHintRightColor;
|
|
3966
|
+
ctx.textAlign = "left";
|
|
3967
|
+
ctx.fillText(crosshair.sideHintRight, cx + hintGap, hintY);
|
|
3968
|
+
}
|
|
3969
|
+
ctx.restore();
|
|
3970
|
+
}
|
|
3971
|
+
ctx.restore();
|
|
3972
|
+
if (crosshair.showVertical) {
|
|
3973
|
+
ctx.save();
|
|
3974
|
+
ctx.strokeStyle = crosshair.color;
|
|
3975
|
+
ctx.lineWidth = Math.max(1, crosshair.width);
|
|
3976
|
+
applyDashPattern(crosshair.style, dashPatterns.dotted, dashPatterns.dashed);
|
|
3977
|
+
ctx.beginPath();
|
|
3978
|
+
ctx.moveTo(crisp(cx), crisp(chartTop));
|
|
3979
|
+
ctx.lineTo(crisp(cx), crisp(fullChartBottom));
|
|
3980
|
+
ctx.stroke();
|
|
3981
|
+
ctx.restore();
|
|
3982
|
+
}
|
|
3983
|
+
const labelPaddingX = 8;
|
|
3984
|
+
const labelHeight = 20;
|
|
3985
|
+
const labelRadius = Math.max(0, crosshair.labelBorderRadius);
|
|
3986
|
+
const labelBackground = crosshair.labelBackgroundColor;
|
|
3987
|
+
const labelTextColor = crosshair.labelTextColor;
|
|
3988
|
+
const labelBorderColor = crosshair.labelBorderColor;
|
|
3989
|
+
const labelBorderWidth = Math.max(0, crosshair.labelBorderWidth);
|
|
3990
|
+
const labelBorderStyle = crosshair.labelBorderStyle;
|
|
3991
|
+
const strokeCrosshairLabel = (x, y, widthValue, heightValue = labelHeight) => {
|
|
3992
|
+
if (labelBorderWidth <= 0) {
|
|
3993
|
+
return;
|
|
3994
|
+
}
|
|
3995
|
+
ctx.save();
|
|
3996
|
+
ctx.strokeStyle = labelBorderColor;
|
|
3997
|
+
ctx.lineWidth = labelBorderWidth;
|
|
3998
|
+
applyDashPattern(
|
|
3999
|
+
labelBorderStyle,
|
|
4000
|
+
dashPatterns.borderDotted,
|
|
4001
|
+
dashPatterns.borderDashed
|
|
4002
|
+
);
|
|
4003
|
+
strokeRoundedRect(Math.round(x), Math.round(y), widthValue, heightValue, labelRadius);
|
|
4004
|
+
ctx.restore();
|
|
4005
|
+
};
|
|
4006
|
+
if (crosshair.showPriceLabel) {
|
|
4007
|
+
const hoverPrice = yMin + (chartBottom - cy) / chartHeight * yRange;
|
|
4008
|
+
const priceText = formatPrice(hoverPrice);
|
|
4009
|
+
const priceWidth = getRightAxisLabelWidth(chartRight, getPriceLabelWidth(priceText, labelPaddingX));
|
|
4010
|
+
const priceX = getRightAxisLabelX(chartRight);
|
|
4011
|
+
const priceY = clamp(cy - labelHeight / 2, chartTop, chartBottom - labelHeight);
|
|
4012
|
+
ctx.fillStyle = labelBackground;
|
|
4013
|
+
fillRoundedRect(Math.round(priceX), Math.round(priceY), priceWidth, labelHeight, labelRadius);
|
|
4014
|
+
strokeCrosshairLabel(priceX, priceY, priceWidth);
|
|
4015
|
+
drawText(priceText, priceX + labelPaddingX, priceY + labelHeight / 2, "left", "middle", labelTextColor);
|
|
4016
|
+
if (crosshair.showPriceActionButton) {
|
|
4017
|
+
const buttonSize = clamp(Math.round(crosshair.priceActionButtonSize), 12, Math.max(12, labelHeight - 4));
|
|
4018
|
+
const buttonGap = Math.max(0, Math.round(crosshair.priceActionButtonGap));
|
|
4019
|
+
const containerPaddingX = 5;
|
|
4020
|
+
const containerWidth = buttonSize + containerPaddingX * 2;
|
|
4021
|
+
const containerX = priceX - buttonGap - containerWidth;
|
|
4022
|
+
const containerY = priceY;
|
|
4023
|
+
const buttonX = containerX + containerPaddingX;
|
|
4024
|
+
const buttonY = priceY + (labelHeight - buttonSize) / 2;
|
|
4025
|
+
const buttonRadius = crosshair.priceActionButtonRounded ? clamp(Math.round(crosshair.priceActionButtonBorderRadius), 0, buttonSize / 2) : 0;
|
|
4026
|
+
const buttonBorderWidth = Math.max(1, Math.round(labelBorderWidth || 1));
|
|
4027
|
+
const buttonCenterX = buttonX + buttonSize / 2;
|
|
4028
|
+
const buttonCenterY = buttonY + buttonSize / 2;
|
|
4029
|
+
const containerRadius = labelRadius;
|
|
3639
4030
|
ctx.fillStyle = labelBackground;
|
|
3640
|
-
fillRoundedRect(Math.round(
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
const buttonCenterY = buttonY + buttonSize / 2;
|
|
3656
|
-
const containerRadius = labelRadius;
|
|
3657
|
-
ctx.fillStyle = labelBackground;
|
|
3658
|
-
fillRoundedRect(Math.round(containerX), Math.round(containerY), containerWidth, labelHeight, containerRadius);
|
|
3659
|
-
if (labelBorderWidth > 0) {
|
|
3660
|
-
ctx.save();
|
|
3661
|
-
ctx.strokeStyle = labelBorderColor;
|
|
3662
|
-
ctx.lineWidth = labelBorderWidth;
|
|
3663
|
-
ctx.setLineDash([]);
|
|
3664
|
-
strokeRoundedRect(Math.round(containerX), Math.round(containerY), containerWidth, labelHeight, containerRadius);
|
|
3665
|
-
ctx.restore();
|
|
3666
|
-
}
|
|
3667
|
-
if (buttonBorderWidth > 0) {
|
|
3668
|
-
ctx.save();
|
|
3669
|
-
ctx.strokeStyle = labelBorderColor;
|
|
3670
|
-
ctx.lineWidth = buttonBorderWidth;
|
|
3671
|
-
ctx.setLineDash([]);
|
|
3672
|
-
if (crosshair.priceActionButtonRounded) {
|
|
3673
|
-
ctx.beginPath();
|
|
3674
|
-
ctx.arc(
|
|
3675
|
-
buttonCenterX,
|
|
3676
|
-
buttonCenterY,
|
|
3677
|
-
Math.max(1, buttonSize / 2 - buttonBorderWidth / 2),
|
|
3678
|
-
0,
|
|
3679
|
-
Math.PI * 2
|
|
3680
|
-
);
|
|
3681
|
-
ctx.stroke();
|
|
3682
|
-
} else {
|
|
3683
|
-
strokeRoundedRect(Math.round(buttonX), Math.round(buttonY), buttonSize, buttonSize, buttonRadius);
|
|
3684
|
-
}
|
|
3685
|
-
ctx.restore();
|
|
3686
|
-
}
|
|
3687
|
-
if (crosshair.priceActionButtonIcon !== "text" && crosshair.priceActionButtonText === "+") {
|
|
3688
|
-
const plusHalf = Math.max(3, Math.round(buttonSize * 0.22));
|
|
3689
|
-
const plusThickness = crosshair.priceActionButtonIcon === "plusThin" ? Math.max(1, Math.round(buttonSize * 0.07)) : Math.max(1, Math.round(buttonSize * 0.1));
|
|
3690
|
-
ctx.save();
|
|
3691
|
-
ctx.strokeStyle = labelTextColor;
|
|
3692
|
-
ctx.lineWidth = plusThickness;
|
|
3693
|
-
ctx.lineCap = "round";
|
|
3694
|
-
ctx.setLineDash([]);
|
|
4031
|
+
fillRoundedRect(Math.round(containerX), Math.round(containerY), containerWidth, labelHeight, containerRadius);
|
|
4032
|
+
if (labelBorderWidth > 0) {
|
|
4033
|
+
ctx.save();
|
|
4034
|
+
ctx.strokeStyle = labelBorderColor;
|
|
4035
|
+
ctx.lineWidth = labelBorderWidth;
|
|
4036
|
+
ctx.setLineDash([]);
|
|
4037
|
+
strokeRoundedRect(Math.round(containerX), Math.round(containerY), containerWidth, labelHeight, containerRadius);
|
|
4038
|
+
ctx.restore();
|
|
4039
|
+
}
|
|
4040
|
+
if (buttonBorderWidth > 0) {
|
|
4041
|
+
ctx.save();
|
|
4042
|
+
ctx.strokeStyle = labelBorderColor;
|
|
4043
|
+
ctx.lineWidth = buttonBorderWidth;
|
|
4044
|
+
ctx.setLineDash([]);
|
|
4045
|
+
if (crosshair.priceActionButtonRounded) {
|
|
3695
4046
|
ctx.beginPath();
|
|
3696
|
-
ctx.
|
|
3697
|
-
ctx.lineTo(buttonCenterX + plusHalf, buttonCenterY);
|
|
3698
|
-
ctx.moveTo(buttonCenterX, buttonCenterY - plusHalf);
|
|
3699
|
-
ctx.lineTo(buttonCenterX, buttonCenterY + plusHalf);
|
|
3700
|
-
ctx.stroke();
|
|
3701
|
-
ctx.restore();
|
|
3702
|
-
} else {
|
|
3703
|
-
drawText(
|
|
3704
|
-
crosshair.priceActionButtonText,
|
|
4047
|
+
ctx.arc(
|
|
3705
4048
|
buttonCenterX,
|
|
3706
4049
|
buttonCenterY,
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
4050
|
+
Math.max(1, buttonSize / 2 - buttonBorderWidth / 2),
|
|
4051
|
+
0,
|
|
4052
|
+
Math.PI * 2
|
|
3710
4053
|
);
|
|
4054
|
+
ctx.stroke();
|
|
4055
|
+
} else {
|
|
4056
|
+
strokeRoundedRect(Math.round(buttonX), Math.round(buttonY), buttonSize, buttonSize, buttonRadius);
|
|
3711
4057
|
}
|
|
3712
|
-
|
|
3713
|
-
x: containerX,
|
|
3714
|
-
y: containerY,
|
|
3715
|
-
width: containerWidth,
|
|
3716
|
-
height: labelHeight,
|
|
3717
|
-
price: Number(hoverPrice.toFixed(mergedOptions.priceDecimals))
|
|
3718
|
-
};
|
|
4058
|
+
ctx.restore();
|
|
3719
4059
|
}
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
ctx.
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
4060
|
+
if (crosshair.priceActionButtonIcon !== "text" && crosshair.priceActionButtonText === "+") {
|
|
4061
|
+
const plusHalf = Math.max(3, Math.round(buttonSize * 0.22));
|
|
4062
|
+
const plusThickness = crosshair.priceActionButtonIcon === "plusThin" ? Math.max(1, Math.round(buttonSize * 0.07)) : Math.max(1, Math.round(buttonSize * 0.1));
|
|
4063
|
+
ctx.save();
|
|
4064
|
+
ctx.strokeStyle = labelTextColor;
|
|
4065
|
+
ctx.lineWidth = plusThickness;
|
|
4066
|
+
ctx.lineCap = "round";
|
|
4067
|
+
ctx.setLineDash([]);
|
|
4068
|
+
ctx.beginPath();
|
|
4069
|
+
ctx.moveTo(buttonCenterX - plusHalf, buttonCenterY);
|
|
4070
|
+
ctx.lineTo(buttonCenterX + plusHalf, buttonCenterY);
|
|
4071
|
+
ctx.moveTo(buttonCenterX, buttonCenterY - plusHalf);
|
|
4072
|
+
ctx.lineTo(buttonCenterX, buttonCenterY + plusHalf);
|
|
4073
|
+
ctx.stroke();
|
|
4074
|
+
ctx.restore();
|
|
4075
|
+
} else {
|
|
4076
|
+
drawText(
|
|
4077
|
+
crosshair.priceActionButtonText,
|
|
4078
|
+
buttonCenterX,
|
|
4079
|
+
buttonCenterY,
|
|
4080
|
+
"center",
|
|
4081
|
+
"middle",
|
|
4082
|
+
labelTextColor
|
|
4083
|
+
);
|
|
3735
4084
|
}
|
|
4085
|
+
crosshairPriceActionRegion = {
|
|
4086
|
+
x: containerX,
|
|
4087
|
+
y: containerY,
|
|
4088
|
+
width: containerWidth,
|
|
4089
|
+
height: labelHeight,
|
|
4090
|
+
price: Number(hoverPrice.toFixed(mergedOptions.priceDecimals))
|
|
4091
|
+
};
|
|
4092
|
+
}
|
|
4093
|
+
}
|
|
4094
|
+
if (crosshair.showTimeLabel) {
|
|
4095
|
+
const ratio = clamp((cx - chartLeft) / chartWidth, 0, 1);
|
|
4096
|
+
const hoverIndex = Math.round(xStart + ratio * xSpan2 - 0.5);
|
|
4097
|
+
const hoverTime = getTimeForIndex(hoverIndex);
|
|
4098
|
+
if (hoverTime) {
|
|
4099
|
+
const timeText = formatHoverTimeLabel(hoverTime, crosshair.timeLabelFormat);
|
|
4100
|
+
const timeWidth = Math.ceil(measureTextWidth(timeText)) + labelPaddingX * 2;
|
|
4101
|
+
const timeX = clamp(cx - timeWidth / 2, chartLeft, chartRight - timeWidth);
|
|
4102
|
+
const timeY = fullChartBottom + 1;
|
|
4103
|
+
const timeHeight = Math.max(labelHeight, Math.floor(height - timeY));
|
|
4104
|
+
ctx.fillStyle = labelBackground;
|
|
4105
|
+
fillRoundedRect(Math.round(timeX), Math.round(timeY), timeWidth, timeHeight, labelRadius);
|
|
4106
|
+
strokeCrosshairLabel(timeX, timeY, timeWidth, timeHeight);
|
|
4107
|
+
drawText(timeText, timeX + labelPaddingX, timeY + timeHeight / 2, "left", "middle", labelTextColor);
|
|
3736
4108
|
}
|
|
3737
4109
|
}
|
|
3738
4110
|
};
|
|
4111
|
+
const drawOverlayOnly = () => {
|
|
4112
|
+
if (!baseCtx || !overlayLayout || baseCanvas.width !== canvas.width || baseCanvas.height !== canvas.height || baseCanvas.width === 0) {
|
|
4113
|
+
draw({ updateAutoScale: false });
|
|
4114
|
+
return;
|
|
4115
|
+
}
|
|
4116
|
+
const pixelRatio = getPixelRatio();
|
|
4117
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
4118
|
+
ctx.drawImage(baseCanvas, 0, 0);
|
|
4119
|
+
ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
|
|
4120
|
+
paintCrosshairOverlay();
|
|
4121
|
+
};
|
|
3739
4122
|
const zoomX = (factor, anchorX) => {
|
|
3740
4123
|
if (!drawState || data.length === 0) {
|
|
3741
4124
|
return;
|
|
@@ -3825,6 +4208,59 @@ function createChart(element, options = {}) {
|
|
|
3825
4208
|
}
|
|
3826
4209
|
scheduleDraw();
|
|
3827
4210
|
};
|
|
4211
|
+
let kineticRafId = null;
|
|
4212
|
+
const panVelocitySamples = [];
|
|
4213
|
+
const cancelKineticPan = () => {
|
|
4214
|
+
if (kineticRafId !== null) {
|
|
4215
|
+
cancelAnimationFrame(kineticRafId);
|
|
4216
|
+
kineticRafId = null;
|
|
4217
|
+
}
|
|
4218
|
+
};
|
|
4219
|
+
const recordPanVelocitySample = (x) => {
|
|
4220
|
+
const now = performance.now();
|
|
4221
|
+
panVelocitySamples.push({ t: now, x });
|
|
4222
|
+
while (panVelocitySamples.length > 0 && now - panVelocitySamples[0].t > 120) {
|
|
4223
|
+
panVelocitySamples.shift();
|
|
4224
|
+
}
|
|
4225
|
+
};
|
|
4226
|
+
const startKineticPan = (initialVelocityPxPerMs) => {
|
|
4227
|
+
cancelKineticPan();
|
|
4228
|
+
let velocity = initialVelocityPxPerMs;
|
|
4229
|
+
let lastTime = performance.now();
|
|
4230
|
+
const stepFn = (now) => {
|
|
4231
|
+
kineticRafId = null;
|
|
4232
|
+
const dt = Math.min(64, Math.max(1, now - lastTime));
|
|
4233
|
+
lastTime = now;
|
|
4234
|
+
const centerBefore = xCenter;
|
|
4235
|
+
pan(velocity * dt, 0, true, false);
|
|
4236
|
+
velocity *= Math.exp(-dt / 325);
|
|
4237
|
+
if (Math.abs(velocity) < 0.02 || xCenter === centerBefore) {
|
|
4238
|
+
return;
|
|
4239
|
+
}
|
|
4240
|
+
kineticRafId = requestAnimationFrame(stepFn);
|
|
4241
|
+
};
|
|
4242
|
+
kineticRafId = requestAnimationFrame(stepFn);
|
|
4243
|
+
};
|
|
4244
|
+
const maybeStartKineticPan = (pointerType) => {
|
|
4245
|
+
const config = mergedOptions.kineticScroll ?? DEFAULT_OPTIONS.kineticScroll;
|
|
4246
|
+
const enabled = pointerType === "touch" || pointerType === "pen" ? config?.touch !== false : config?.mouse === true;
|
|
4247
|
+
if (!enabled || panVelocitySamples.length < 2) {
|
|
4248
|
+
panVelocitySamples.length = 0;
|
|
4249
|
+
return;
|
|
4250
|
+
}
|
|
4251
|
+
const first = panVelocitySamples[0];
|
|
4252
|
+
const last = panVelocitySamples[panVelocitySamples.length - 1];
|
|
4253
|
+
panVelocitySamples.length = 0;
|
|
4254
|
+
const elapsed = last.t - first.t;
|
|
4255
|
+
if (elapsed <= 0 || performance.now() - last.t > 100) {
|
|
4256
|
+
return;
|
|
4257
|
+
}
|
|
4258
|
+
const velocity = (last.x - first.x) / elapsed;
|
|
4259
|
+
if (Math.abs(velocity) < 0.1) {
|
|
4260
|
+
return;
|
|
4261
|
+
}
|
|
4262
|
+
startKineticPan(clamp(velocity, -4, 4));
|
|
4263
|
+
};
|
|
3828
4264
|
const resetYViewport = () => {
|
|
3829
4265
|
yMinOverride = null;
|
|
3830
4266
|
yMaxOverride = null;
|
|
@@ -3884,6 +4320,7 @@ function createChart(element, options = {}) {
|
|
|
3884
4320
|
scheduleDraw();
|
|
3885
4321
|
};
|
|
3886
4322
|
const resetViewport = () => {
|
|
4323
|
+
cancelKineticPan();
|
|
3887
4324
|
fitXViewport();
|
|
3888
4325
|
resetYViewport();
|
|
3889
4326
|
updateFollowLatest(true);
|
|
@@ -3893,6 +4330,7 @@ function createChart(element, options = {}) {
|
|
|
3893
4330
|
const isFollowingLatest = () => followLatest;
|
|
3894
4331
|
const setFollowingLatest = (follow) => {
|
|
3895
4332
|
if (follow && data.length > 0) {
|
|
4333
|
+
cancelKineticPan();
|
|
3896
4334
|
xCenter = data.length - xSpan / 2 + rightEdgePaddingBars;
|
|
3897
4335
|
clampXViewport();
|
|
3898
4336
|
updateFollowLatest(true);
|
|
@@ -4262,7 +4700,7 @@ function createChart(element, options = {}) {
|
|
|
4262
4700
|
const prevFont = ctx.font;
|
|
4263
4701
|
ctx.font = `500 ${fontSize}px ${mergedOptions.fontFamily}`;
|
|
4264
4702
|
const lines = (drawing.label ?? "").split("\n");
|
|
4265
|
-
const textW = Math.max(1, ...lines.map((line) =>
|
|
4703
|
+
const textW = Math.max(1, ...lines.map((line) => measureTextWidth(line)));
|
|
4266
4704
|
ctx.font = prevFont;
|
|
4267
4705
|
const isNote = drawing.type === "note";
|
|
4268
4706
|
const padX = isNote ? 8 : 2;
|
|
@@ -4531,7 +4969,9 @@ function createChart(element, options = {}) {
|
|
|
4531
4969
|
points: [point, normalizeDrawingPoint(point.index + width2, point.price - priceOffset)],
|
|
4532
4970
|
color: defaults.color ?? "#2962ff",
|
|
4533
4971
|
style: defaults.style ?? "solid",
|
|
4534
|
-
width: defaults.width ?? 1
|
|
4972
|
+
width: defaults.width ?? 1,
|
|
4973
|
+
// pointValue enables the dollar-P&L line in the measure label.
|
|
4974
|
+
...defaults.pointValue === void 0 ? {} : { pointValue: defaults.pointValue }
|
|
4535
4975
|
})
|
|
4536
4976
|
);
|
|
4537
4977
|
emitDrawingsChange();
|
|
@@ -4733,16 +5173,24 @@ function createChart(element, options = {}) {
|
|
|
4733
5173
|
emitViewportChange();
|
|
4734
5174
|
scheduleDraw();
|
|
4735
5175
|
};
|
|
5176
|
+
const capturePointer = (pointerId) => {
|
|
5177
|
+
try {
|
|
5178
|
+
canvas.setPointerCapture(pointerId);
|
|
5179
|
+
} catch {
|
|
5180
|
+
}
|
|
5181
|
+
};
|
|
4736
5182
|
const onPointerDown = (event) => {
|
|
4737
5183
|
if (event.pointerType === "touch" || event.pointerType === "pen") {
|
|
4738
5184
|
event.preventDefault();
|
|
4739
5185
|
}
|
|
5186
|
+
cancelKineticPan();
|
|
5187
|
+
panVelocitySamples.length = 0;
|
|
4740
5188
|
magnetModifierActive = event.metaKey || event.ctrlKey;
|
|
4741
5189
|
shiftKeyActive = event.shiftKey;
|
|
4742
5190
|
const point = getCanvasPoint(event);
|
|
4743
5191
|
if (event.pointerType === "touch") {
|
|
4744
5192
|
touchPointers.set(event.pointerId, point);
|
|
4745
|
-
|
|
5193
|
+
capturePointer(event.pointerId);
|
|
4746
5194
|
const touchPair = getTouchPair();
|
|
4747
5195
|
if (touchPair) {
|
|
4748
5196
|
beginPinchZoom(touchPair[0], touchPair[1]);
|
|
@@ -4772,7 +5220,7 @@ function createChart(element, options = {}) {
|
|
|
4772
5220
|
lastPrice: startPrice,
|
|
4773
5221
|
moved: false
|
|
4774
5222
|
};
|
|
4775
|
-
|
|
5223
|
+
capturePointer(event.pointerId);
|
|
4776
5224
|
canvas.style.cursor = "ns-resize";
|
|
4777
5225
|
setCrosshairPoint(null);
|
|
4778
5226
|
return;
|
|
@@ -4793,7 +5241,7 @@ function createChart(element, options = {}) {
|
|
|
4793
5241
|
startPrice: orderDragRegion.price,
|
|
4794
5242
|
lastPrice: orderDragRegion.price
|
|
4795
5243
|
};
|
|
4796
|
-
|
|
5244
|
+
capturePointer(event.pointerId);
|
|
4797
5245
|
canvas.style.cursor = "ns-resize";
|
|
4798
5246
|
setCrosshairPoint(null);
|
|
4799
5247
|
return;
|
|
@@ -4825,7 +5273,7 @@ function createChart(element, options = {}) {
|
|
|
4825
5273
|
startPoints: drawingHit.drawing.points.map((drawingPoint) => ({ ...drawingPoint }))
|
|
4826
5274
|
};
|
|
4827
5275
|
activePointerId = event.pointerId;
|
|
4828
|
-
|
|
5276
|
+
capturePointer(event.pointerId);
|
|
4829
5277
|
}
|
|
4830
5278
|
}
|
|
4831
5279
|
setCrosshairPoint(null);
|
|
@@ -4853,7 +5301,7 @@ function createChart(element, options = {}) {
|
|
|
4853
5301
|
};
|
|
4854
5302
|
lastPointerX = point.x;
|
|
4855
5303
|
lastPointerY = point.y;
|
|
4856
|
-
|
|
5304
|
+
capturePointer(event.pointerId);
|
|
4857
5305
|
if (crosshairDrag) {
|
|
4858
5306
|
canvas.style.cursor = "crosshair";
|
|
4859
5307
|
setCrosshairPoint(point);
|
|
@@ -5039,6 +5487,7 @@ function createChart(element, options = {}) {
|
|
|
5039
5487
|
const deltaY = point.y - lastPointerY;
|
|
5040
5488
|
if (dragMode === "plot") {
|
|
5041
5489
|
canvas.style.cursor = "grabbing";
|
|
5490
|
+
recordPanVelocitySample(point.x);
|
|
5042
5491
|
pan(deltaX, deltaY, true, true);
|
|
5043
5492
|
setCrosshairPoint(null);
|
|
5044
5493
|
} else if (dragMode === "x-axis") {
|
|
@@ -5112,10 +5561,16 @@ function createChart(element, options = {}) {
|
|
|
5112
5561
|
drawingDragState = null;
|
|
5113
5562
|
emitDrawingsChange();
|
|
5114
5563
|
}
|
|
5564
|
+
const endedPlotDrag = isDragging && dragMode === "plot";
|
|
5115
5565
|
isDragging = false;
|
|
5116
5566
|
dragMode = null;
|
|
5117
5567
|
activePointerId = null;
|
|
5118
5568
|
canvas.style.cursor = "default";
|
|
5569
|
+
if (endedPlotDrag) {
|
|
5570
|
+
maybeStartKineticPan(event?.pointerType);
|
|
5571
|
+
} else {
|
|
5572
|
+
panVelocitySamples.length = 0;
|
|
5573
|
+
}
|
|
5119
5574
|
if (event && pointerDownInfo && event.pointerId === pointerDownInfo.pointerId) {
|
|
5120
5575
|
if (pointerDownInfo.crosshairDrag) {
|
|
5121
5576
|
const point = getCanvasPoint(event);
|
|
@@ -5153,6 +5608,7 @@ function createChart(element, options = {}) {
|
|
|
5153
5608
|
if (!drawState) {
|
|
5154
5609
|
return;
|
|
5155
5610
|
}
|
|
5611
|
+
cancelKineticPan();
|
|
5156
5612
|
const point = getCanvasPoint(event);
|
|
5157
5613
|
const region = getHitRegion(point.x, point.y);
|
|
5158
5614
|
if (region === "outside") {
|
|
@@ -5252,6 +5708,7 @@ function createChart(element, options = {}) {
|
|
|
5252
5708
|
width = nextOptions.width !== void 0 && nextOptions.width > 0 ? mergedOptions.width : previousWidth;
|
|
5253
5709
|
height = nextOptions.height !== void 0 && nextOptions.height > 0 ? mergedOptions.height : previousHeight;
|
|
5254
5710
|
mergedOptions = { ...mergedOptions, width, height };
|
|
5711
|
+
refreshResolvedOptions();
|
|
5255
5712
|
resetRightMarginCache();
|
|
5256
5713
|
doubleClickEnabled = mergedOptions.doubleClickEnabled;
|
|
5257
5714
|
doubleClickAction = mergedOptions.doubleClickAction;
|
|
@@ -5596,6 +6053,7 @@ function createChart(element, options = {}) {
|
|
|
5596
6053
|
drawingHoverHandler = handler;
|
|
5597
6054
|
};
|
|
5598
6055
|
const destroy = () => {
|
|
6056
|
+
cancelKineticPan();
|
|
5599
6057
|
if (smoothingRafId !== null) {
|
|
5600
6058
|
cancelAnimationFrame(smoothingRafId);
|
|
5601
6059
|
smoothingRafId = null;
|