hyperprop-charting-library 0.1.13 → 0.1.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -0
- package/dist/hyperprop-charting-library.cjs +36 -6
- package/dist/hyperprop-charting-library.d.ts +5 -0
- package/dist/hyperprop-charting-library.js +36 -6
- package/dist/index.cjs +36 -6
- package/dist/index.d.cts +5 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +36 -6
- package/docs/API.md +5 -0
- package/docs/RECIPES.md +12 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -46,6 +46,20 @@ const chart = createChart(root, {
|
|
|
46
46
|
});
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
+
## Stable Price Labels (No Shake)
|
|
50
|
+
|
|
51
|
+
If fast ticks make right-side labels jitter, keep width stable:
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
const chart = createChart(root, {
|
|
55
|
+
priceDecimals: 2,
|
|
56
|
+
stabilizePriceLabels: true,
|
|
57
|
+
priceLabelMinIntegerDigits: 4,
|
|
58
|
+
// strongest lock (optional):
|
|
59
|
+
// priceLabelWidthTemplate: "88888.88"
|
|
60
|
+
});
|
|
61
|
+
```
|
|
62
|
+
|
|
49
63
|
## Full Documentation
|
|
50
64
|
|
|
51
65
|
- API reference: `docs/API.md`
|
|
@@ -108,6 +108,8 @@ var DEFAULT_ORDER_LINE_OPTIONS = {
|
|
|
108
108
|
actionButtonFontWeight: 500,
|
|
109
109
|
actionButtonBorderColor: "#2563eb",
|
|
110
110
|
actionButtonBorderStyle: "solid",
|
|
111
|
+
actionButtonsInnerGap: 6,
|
|
112
|
+
actionButtonsGroupGap: 8,
|
|
111
113
|
actionButtons: [],
|
|
112
114
|
connectorToPrice: Number.NaN,
|
|
113
115
|
connectorColor: "#2563eb",
|
|
@@ -124,6 +126,9 @@ var DEFAULT_OPTIONS = {
|
|
|
124
126
|
axisColor: "#7f8289",
|
|
125
127
|
axis: DEFAULT_AXIS_OPTIONS,
|
|
126
128
|
priceDecimals: 2,
|
|
129
|
+
stabilizePriceLabels: true,
|
|
130
|
+
priceLabelMinIntegerDigits: 3,
|
|
131
|
+
priceLabelWidthTemplate: "",
|
|
127
132
|
initialViewport: "latest",
|
|
128
133
|
initialVisibleBars: 60,
|
|
129
134
|
minVisibleBars: 5,
|
|
@@ -363,6 +368,31 @@ function createChart(element, options = {}) {
|
|
|
363
368
|
const decimals = clamp(Math.round(mergedOptions.priceDecimals), 0, 8);
|
|
364
369
|
return price.toFixed(decimals);
|
|
365
370
|
};
|
|
371
|
+
const getStabilizedPriceTemplate = () => {
|
|
372
|
+
const explicitTemplate = mergedOptions.priceLabelWidthTemplate.trim();
|
|
373
|
+
if (explicitTemplate.length > 0) {
|
|
374
|
+
return explicitTemplate;
|
|
375
|
+
}
|
|
376
|
+
const decimals = clamp(Math.round(mergedOptions.priceDecimals), 0, 8);
|
|
377
|
+
const configuredDigits = Math.max(1, Math.floor(mergedOptions.priceLabelMinIntegerDigits));
|
|
378
|
+
let maxAbsPrice = 0;
|
|
379
|
+
for (const point of data) {
|
|
380
|
+
maxAbsPrice = Math.max(maxAbsPrice, Math.abs(point.o), Math.abs(point.h), Math.abs(point.l), Math.abs(point.c));
|
|
381
|
+
}
|
|
382
|
+
const observedDigits = maxAbsPrice >= 1 ? Math.floor(Math.log10(maxAbsPrice)) + 1 : 1;
|
|
383
|
+
const integerDigits = Math.max(configuredDigits, observedDigits);
|
|
384
|
+
const integerPart = "8".repeat(integerDigits);
|
|
385
|
+
const decimalPart = decimals > 0 ? `.${"8".repeat(decimals)}` : "";
|
|
386
|
+
return `${integerPart}${decimalPart}`;
|
|
387
|
+
};
|
|
388
|
+
const getPriceLabelWidth = (priceText, paddingX) => {
|
|
389
|
+
const measured = Math.ceil(ctx.measureText(priceText).width) + paddingX * 2;
|
|
390
|
+
if (!mergedOptions.stabilizePriceLabels) {
|
|
391
|
+
return measured;
|
|
392
|
+
}
|
|
393
|
+
const templateWidth = Math.ceil(ctx.measureText(getStabilizedPriceTemplate()).width) + paddingX * 2;
|
|
394
|
+
return Math.max(measured, templateWidth);
|
|
395
|
+
};
|
|
366
396
|
const parseData = (nextData) => {
|
|
367
397
|
return nextData.map((point) => ({
|
|
368
398
|
time: new Date(point.t),
|
|
@@ -481,7 +511,7 @@ function createChart(element, options = {}) {
|
|
|
481
511
|
ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
|
|
482
512
|
const labelPaddingX = 8;
|
|
483
513
|
const labelHeight = 20;
|
|
484
|
-
const labelWidth = Math.ceil(ctx.measureText(labelText).width) + labelPaddingX * 2;
|
|
514
|
+
const labelWidth = mergedLine.label === void 0 ? getPriceLabelWidth(labelText, labelPaddingX) : Math.ceil(ctx.measureText(labelText).width) + labelPaddingX * 2;
|
|
485
515
|
const labelX = chartRight + 4;
|
|
486
516
|
const labelY = clamp(lineY - labelHeight / 2, chartTop, chartBottom - labelHeight);
|
|
487
517
|
ctx.fillStyle = mergedLine.labelBackgroundColor;
|
|
@@ -583,9 +613,9 @@ function createChart(element, options = {}) {
|
|
|
583
613
|
const width2 = Math.max(minWidth, Math.ceil(ctx.measureText(button.text).width) + paddingX * 2);
|
|
584
614
|
return { button, width: width2 };
|
|
585
615
|
});
|
|
586
|
-
const actionButtonInnerGap = actionButtonMetrics.length > 1 ?
|
|
616
|
+
const actionButtonInnerGap = actionButtonMetrics.length > 1 ? Math.max(0, mergedLine.actionButtonsInnerGap) : 0;
|
|
587
617
|
const actionButtonsTotalWidth = actionButtonMetrics.reduce((sum, metric) => sum + metric.width, 0) + Math.max(0, actionButtonMetrics.length - 1) * actionButtonInnerGap;
|
|
588
|
-
const actionButtonsGap = actionButtonMetrics.length > 0 ?
|
|
618
|
+
const actionButtonsGap = actionButtonMetrics.length > 0 ? Math.max(0, mergedLine.actionButtonsGroupGap) : 0;
|
|
589
619
|
const segmentPaddingX = 8;
|
|
590
620
|
const labelHeight = 22;
|
|
591
621
|
const qtyWidth = qtyText ? Math.ceil(ctx.measureText(qtyText).width) + segmentPaddingX * 2 : 0;
|
|
@@ -709,7 +739,7 @@ function createChart(element, options = {}) {
|
|
|
709
739
|
}
|
|
710
740
|
const priceText = formatPrice(renderPrice);
|
|
711
741
|
const pricePaddingX = 8;
|
|
712
|
-
const measuredPriceWidth =
|
|
742
|
+
const measuredPriceWidth = getPriceLabelWidth(priceText, pricePaddingX);
|
|
713
743
|
const priceWidth = mergedLine.id === void 0 ? measuredPriceWidth : Math.max(measuredPriceWidth, orderPriceTagWidthById.get(mergedLine.id) ?? 0);
|
|
714
744
|
if (mergedLine.id) {
|
|
715
745
|
orderPriceTagWidthById.set(mergedLine.id, priceWidth);
|
|
@@ -1036,7 +1066,7 @@ function createChart(element, options = {}) {
|
|
|
1036
1066
|
ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
|
|
1037
1067
|
const labelPaddingX = 8;
|
|
1038
1068
|
const labelHeight = 20;
|
|
1039
|
-
const labelWidth =
|
|
1069
|
+
const labelWidth = getPriceLabelWidth(tickerLabel, labelPaddingX);
|
|
1040
1070
|
const labelX = chartRight + 4;
|
|
1041
1071
|
const labelY = clamp(lineY - labelHeight / 2, chartTop, chartBottom - labelHeight);
|
|
1042
1072
|
const labelRadius = Math.max(0, ticker.labelBorderRadius ?? 0);
|
|
@@ -1109,7 +1139,7 @@ function createChart(element, options = {}) {
|
|
|
1109
1139
|
if (crosshair.showPriceLabel) {
|
|
1110
1140
|
const hoverPrice = yMin + (chartBottom - cy) / chartHeight * yRange;
|
|
1111
1141
|
const priceText = formatPrice(hoverPrice);
|
|
1112
|
-
const priceWidth =
|
|
1142
|
+
const priceWidth = getPriceLabelWidth(priceText, labelPaddingX);
|
|
1113
1143
|
const priceX = chartRight + 4;
|
|
1114
1144
|
const priceY = clamp(cy - labelHeight / 2, chartTop, chartBottom - labelHeight);
|
|
1115
1145
|
ctx.fillStyle = labelBackground;
|
|
@@ -5,6 +5,9 @@ interface ChartOptions {
|
|
|
5
5
|
axisColor?: string;
|
|
6
6
|
axis?: AxisOptions;
|
|
7
7
|
priceDecimals?: number;
|
|
8
|
+
stabilizePriceLabels?: boolean;
|
|
9
|
+
priceLabelMinIntegerDigits?: number;
|
|
10
|
+
priceLabelWidthTemplate?: string;
|
|
8
11
|
initialViewport?: "latest" | "center";
|
|
9
12
|
initialVisibleBars?: number;
|
|
10
13
|
minVisibleBars?: number;
|
|
@@ -128,6 +131,8 @@ interface OrderLineOptions {
|
|
|
128
131
|
actionButtonFontWeight?: number | string;
|
|
129
132
|
actionButtonBorderColor?: string;
|
|
130
133
|
actionButtonBorderStyle?: "solid" | "dotted" | "dashed";
|
|
134
|
+
actionButtonsInnerGap?: number;
|
|
135
|
+
actionButtonsGroupGap?: number;
|
|
131
136
|
actionButtons?: OrderActionButton[];
|
|
132
137
|
connectorToPrice?: number;
|
|
133
138
|
connectorColor?: string;
|
|
@@ -84,6 +84,8 @@ var DEFAULT_ORDER_LINE_OPTIONS = {
|
|
|
84
84
|
actionButtonFontWeight: 500,
|
|
85
85
|
actionButtonBorderColor: "#2563eb",
|
|
86
86
|
actionButtonBorderStyle: "solid",
|
|
87
|
+
actionButtonsInnerGap: 6,
|
|
88
|
+
actionButtonsGroupGap: 8,
|
|
87
89
|
actionButtons: [],
|
|
88
90
|
connectorToPrice: Number.NaN,
|
|
89
91
|
connectorColor: "#2563eb",
|
|
@@ -100,6 +102,9 @@ var DEFAULT_OPTIONS = {
|
|
|
100
102
|
axisColor: "#7f8289",
|
|
101
103
|
axis: DEFAULT_AXIS_OPTIONS,
|
|
102
104
|
priceDecimals: 2,
|
|
105
|
+
stabilizePriceLabels: true,
|
|
106
|
+
priceLabelMinIntegerDigits: 3,
|
|
107
|
+
priceLabelWidthTemplate: "",
|
|
103
108
|
initialViewport: "latest",
|
|
104
109
|
initialVisibleBars: 60,
|
|
105
110
|
minVisibleBars: 5,
|
|
@@ -339,6 +344,31 @@ function createChart(element, options = {}) {
|
|
|
339
344
|
const decimals = clamp(Math.round(mergedOptions.priceDecimals), 0, 8);
|
|
340
345
|
return price.toFixed(decimals);
|
|
341
346
|
};
|
|
347
|
+
const getStabilizedPriceTemplate = () => {
|
|
348
|
+
const explicitTemplate = mergedOptions.priceLabelWidthTemplate.trim();
|
|
349
|
+
if (explicitTemplate.length > 0) {
|
|
350
|
+
return explicitTemplate;
|
|
351
|
+
}
|
|
352
|
+
const decimals = clamp(Math.round(mergedOptions.priceDecimals), 0, 8);
|
|
353
|
+
const configuredDigits = Math.max(1, Math.floor(mergedOptions.priceLabelMinIntegerDigits));
|
|
354
|
+
let maxAbsPrice = 0;
|
|
355
|
+
for (const point of data) {
|
|
356
|
+
maxAbsPrice = Math.max(maxAbsPrice, Math.abs(point.o), Math.abs(point.h), Math.abs(point.l), Math.abs(point.c));
|
|
357
|
+
}
|
|
358
|
+
const observedDigits = maxAbsPrice >= 1 ? Math.floor(Math.log10(maxAbsPrice)) + 1 : 1;
|
|
359
|
+
const integerDigits = Math.max(configuredDigits, observedDigits);
|
|
360
|
+
const integerPart = "8".repeat(integerDigits);
|
|
361
|
+
const decimalPart = decimals > 0 ? `.${"8".repeat(decimals)}` : "";
|
|
362
|
+
return `${integerPart}${decimalPart}`;
|
|
363
|
+
};
|
|
364
|
+
const getPriceLabelWidth = (priceText, paddingX) => {
|
|
365
|
+
const measured = Math.ceil(ctx.measureText(priceText).width) + paddingX * 2;
|
|
366
|
+
if (!mergedOptions.stabilizePriceLabels) {
|
|
367
|
+
return measured;
|
|
368
|
+
}
|
|
369
|
+
const templateWidth = Math.ceil(ctx.measureText(getStabilizedPriceTemplate()).width) + paddingX * 2;
|
|
370
|
+
return Math.max(measured, templateWidth);
|
|
371
|
+
};
|
|
342
372
|
const parseData = (nextData) => {
|
|
343
373
|
return nextData.map((point) => ({
|
|
344
374
|
time: new Date(point.t),
|
|
@@ -457,7 +487,7 @@ function createChart(element, options = {}) {
|
|
|
457
487
|
ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
|
|
458
488
|
const labelPaddingX = 8;
|
|
459
489
|
const labelHeight = 20;
|
|
460
|
-
const labelWidth = Math.ceil(ctx.measureText(labelText).width) + labelPaddingX * 2;
|
|
490
|
+
const labelWidth = mergedLine.label === void 0 ? getPriceLabelWidth(labelText, labelPaddingX) : Math.ceil(ctx.measureText(labelText).width) + labelPaddingX * 2;
|
|
461
491
|
const labelX = chartRight + 4;
|
|
462
492
|
const labelY = clamp(lineY - labelHeight / 2, chartTop, chartBottom - labelHeight);
|
|
463
493
|
ctx.fillStyle = mergedLine.labelBackgroundColor;
|
|
@@ -559,9 +589,9 @@ function createChart(element, options = {}) {
|
|
|
559
589
|
const width2 = Math.max(minWidth, Math.ceil(ctx.measureText(button.text).width) + paddingX * 2);
|
|
560
590
|
return { button, width: width2 };
|
|
561
591
|
});
|
|
562
|
-
const actionButtonInnerGap = actionButtonMetrics.length > 1 ?
|
|
592
|
+
const actionButtonInnerGap = actionButtonMetrics.length > 1 ? Math.max(0, mergedLine.actionButtonsInnerGap) : 0;
|
|
563
593
|
const actionButtonsTotalWidth = actionButtonMetrics.reduce((sum, metric) => sum + metric.width, 0) + Math.max(0, actionButtonMetrics.length - 1) * actionButtonInnerGap;
|
|
564
|
-
const actionButtonsGap = actionButtonMetrics.length > 0 ?
|
|
594
|
+
const actionButtonsGap = actionButtonMetrics.length > 0 ? Math.max(0, mergedLine.actionButtonsGroupGap) : 0;
|
|
565
595
|
const segmentPaddingX = 8;
|
|
566
596
|
const labelHeight = 22;
|
|
567
597
|
const qtyWidth = qtyText ? Math.ceil(ctx.measureText(qtyText).width) + segmentPaddingX * 2 : 0;
|
|
@@ -685,7 +715,7 @@ function createChart(element, options = {}) {
|
|
|
685
715
|
}
|
|
686
716
|
const priceText = formatPrice(renderPrice);
|
|
687
717
|
const pricePaddingX = 8;
|
|
688
|
-
const measuredPriceWidth =
|
|
718
|
+
const measuredPriceWidth = getPriceLabelWidth(priceText, pricePaddingX);
|
|
689
719
|
const priceWidth = mergedLine.id === void 0 ? measuredPriceWidth : Math.max(measuredPriceWidth, orderPriceTagWidthById.get(mergedLine.id) ?? 0);
|
|
690
720
|
if (mergedLine.id) {
|
|
691
721
|
orderPriceTagWidthById.set(mergedLine.id, priceWidth);
|
|
@@ -1012,7 +1042,7 @@ function createChart(element, options = {}) {
|
|
|
1012
1042
|
ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
|
|
1013
1043
|
const labelPaddingX = 8;
|
|
1014
1044
|
const labelHeight = 20;
|
|
1015
|
-
const labelWidth =
|
|
1045
|
+
const labelWidth = getPriceLabelWidth(tickerLabel, labelPaddingX);
|
|
1016
1046
|
const labelX = chartRight + 4;
|
|
1017
1047
|
const labelY = clamp(lineY - labelHeight / 2, chartTop, chartBottom - labelHeight);
|
|
1018
1048
|
const labelRadius = Math.max(0, ticker.labelBorderRadius ?? 0);
|
|
@@ -1085,7 +1115,7 @@ function createChart(element, options = {}) {
|
|
|
1085
1115
|
if (crosshair.showPriceLabel) {
|
|
1086
1116
|
const hoverPrice = yMin + (chartBottom - cy) / chartHeight * yRange;
|
|
1087
1117
|
const priceText = formatPrice(hoverPrice);
|
|
1088
|
-
const priceWidth =
|
|
1118
|
+
const priceWidth = getPriceLabelWidth(priceText, labelPaddingX);
|
|
1089
1119
|
const priceX = chartRight + 4;
|
|
1090
1120
|
const priceY = clamp(cy - labelHeight / 2, chartTop, chartBottom - labelHeight);
|
|
1091
1121
|
ctx.fillStyle = labelBackground;
|
package/dist/index.cjs
CHANGED
|
@@ -108,6 +108,8 @@ var DEFAULT_ORDER_LINE_OPTIONS = {
|
|
|
108
108
|
actionButtonFontWeight: 500,
|
|
109
109
|
actionButtonBorderColor: "#2563eb",
|
|
110
110
|
actionButtonBorderStyle: "solid",
|
|
111
|
+
actionButtonsInnerGap: 6,
|
|
112
|
+
actionButtonsGroupGap: 8,
|
|
111
113
|
actionButtons: [],
|
|
112
114
|
connectorToPrice: Number.NaN,
|
|
113
115
|
connectorColor: "#2563eb",
|
|
@@ -124,6 +126,9 @@ var DEFAULT_OPTIONS = {
|
|
|
124
126
|
axisColor: "#7f8289",
|
|
125
127
|
axis: DEFAULT_AXIS_OPTIONS,
|
|
126
128
|
priceDecimals: 2,
|
|
129
|
+
stabilizePriceLabels: true,
|
|
130
|
+
priceLabelMinIntegerDigits: 3,
|
|
131
|
+
priceLabelWidthTemplate: "",
|
|
127
132
|
initialViewport: "latest",
|
|
128
133
|
initialVisibleBars: 60,
|
|
129
134
|
minVisibleBars: 5,
|
|
@@ -363,6 +368,31 @@ function createChart(element, options = {}) {
|
|
|
363
368
|
const decimals = clamp(Math.round(mergedOptions.priceDecimals), 0, 8);
|
|
364
369
|
return price.toFixed(decimals);
|
|
365
370
|
};
|
|
371
|
+
const getStabilizedPriceTemplate = () => {
|
|
372
|
+
const explicitTemplate = mergedOptions.priceLabelWidthTemplate.trim();
|
|
373
|
+
if (explicitTemplate.length > 0) {
|
|
374
|
+
return explicitTemplate;
|
|
375
|
+
}
|
|
376
|
+
const decimals = clamp(Math.round(mergedOptions.priceDecimals), 0, 8);
|
|
377
|
+
const configuredDigits = Math.max(1, Math.floor(mergedOptions.priceLabelMinIntegerDigits));
|
|
378
|
+
let maxAbsPrice = 0;
|
|
379
|
+
for (const point of data) {
|
|
380
|
+
maxAbsPrice = Math.max(maxAbsPrice, Math.abs(point.o), Math.abs(point.h), Math.abs(point.l), Math.abs(point.c));
|
|
381
|
+
}
|
|
382
|
+
const observedDigits = maxAbsPrice >= 1 ? Math.floor(Math.log10(maxAbsPrice)) + 1 : 1;
|
|
383
|
+
const integerDigits = Math.max(configuredDigits, observedDigits);
|
|
384
|
+
const integerPart = "8".repeat(integerDigits);
|
|
385
|
+
const decimalPart = decimals > 0 ? `.${"8".repeat(decimals)}` : "";
|
|
386
|
+
return `${integerPart}${decimalPart}`;
|
|
387
|
+
};
|
|
388
|
+
const getPriceLabelWidth = (priceText, paddingX) => {
|
|
389
|
+
const measured = Math.ceil(ctx.measureText(priceText).width) + paddingX * 2;
|
|
390
|
+
if (!mergedOptions.stabilizePriceLabels) {
|
|
391
|
+
return measured;
|
|
392
|
+
}
|
|
393
|
+
const templateWidth = Math.ceil(ctx.measureText(getStabilizedPriceTemplate()).width) + paddingX * 2;
|
|
394
|
+
return Math.max(measured, templateWidth);
|
|
395
|
+
};
|
|
366
396
|
const parseData = (nextData) => {
|
|
367
397
|
return nextData.map((point) => ({
|
|
368
398
|
time: new Date(point.t),
|
|
@@ -481,7 +511,7 @@ function createChart(element, options = {}) {
|
|
|
481
511
|
ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
|
|
482
512
|
const labelPaddingX = 8;
|
|
483
513
|
const labelHeight = 20;
|
|
484
|
-
const labelWidth = Math.ceil(ctx.measureText(labelText).width) + labelPaddingX * 2;
|
|
514
|
+
const labelWidth = mergedLine.label === void 0 ? getPriceLabelWidth(labelText, labelPaddingX) : Math.ceil(ctx.measureText(labelText).width) + labelPaddingX * 2;
|
|
485
515
|
const labelX = chartRight + 4;
|
|
486
516
|
const labelY = clamp(lineY - labelHeight / 2, chartTop, chartBottom - labelHeight);
|
|
487
517
|
ctx.fillStyle = mergedLine.labelBackgroundColor;
|
|
@@ -583,9 +613,9 @@ function createChart(element, options = {}) {
|
|
|
583
613
|
const width2 = Math.max(minWidth, Math.ceil(ctx.measureText(button.text).width) + paddingX * 2);
|
|
584
614
|
return { button, width: width2 };
|
|
585
615
|
});
|
|
586
|
-
const actionButtonInnerGap = actionButtonMetrics.length > 1 ?
|
|
616
|
+
const actionButtonInnerGap = actionButtonMetrics.length > 1 ? Math.max(0, mergedLine.actionButtonsInnerGap) : 0;
|
|
587
617
|
const actionButtonsTotalWidth = actionButtonMetrics.reduce((sum, metric) => sum + metric.width, 0) + Math.max(0, actionButtonMetrics.length - 1) * actionButtonInnerGap;
|
|
588
|
-
const actionButtonsGap = actionButtonMetrics.length > 0 ?
|
|
618
|
+
const actionButtonsGap = actionButtonMetrics.length > 0 ? Math.max(0, mergedLine.actionButtonsGroupGap) : 0;
|
|
589
619
|
const segmentPaddingX = 8;
|
|
590
620
|
const labelHeight = 22;
|
|
591
621
|
const qtyWidth = qtyText ? Math.ceil(ctx.measureText(qtyText).width) + segmentPaddingX * 2 : 0;
|
|
@@ -709,7 +739,7 @@ function createChart(element, options = {}) {
|
|
|
709
739
|
}
|
|
710
740
|
const priceText = formatPrice(renderPrice);
|
|
711
741
|
const pricePaddingX = 8;
|
|
712
|
-
const measuredPriceWidth =
|
|
742
|
+
const measuredPriceWidth = getPriceLabelWidth(priceText, pricePaddingX);
|
|
713
743
|
const priceWidth = mergedLine.id === void 0 ? measuredPriceWidth : Math.max(measuredPriceWidth, orderPriceTagWidthById.get(mergedLine.id) ?? 0);
|
|
714
744
|
if (mergedLine.id) {
|
|
715
745
|
orderPriceTagWidthById.set(mergedLine.id, priceWidth);
|
|
@@ -1036,7 +1066,7 @@ function createChart(element, options = {}) {
|
|
|
1036
1066
|
ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
|
|
1037
1067
|
const labelPaddingX = 8;
|
|
1038
1068
|
const labelHeight = 20;
|
|
1039
|
-
const labelWidth =
|
|
1069
|
+
const labelWidth = getPriceLabelWidth(tickerLabel, labelPaddingX);
|
|
1040
1070
|
const labelX = chartRight + 4;
|
|
1041
1071
|
const labelY = clamp(lineY - labelHeight / 2, chartTop, chartBottom - labelHeight);
|
|
1042
1072
|
const labelRadius = Math.max(0, ticker.labelBorderRadius ?? 0);
|
|
@@ -1109,7 +1139,7 @@ function createChart(element, options = {}) {
|
|
|
1109
1139
|
if (crosshair.showPriceLabel) {
|
|
1110
1140
|
const hoverPrice = yMin + (chartBottom - cy) / chartHeight * yRange;
|
|
1111
1141
|
const priceText = formatPrice(hoverPrice);
|
|
1112
|
-
const priceWidth =
|
|
1142
|
+
const priceWidth = getPriceLabelWidth(priceText, labelPaddingX);
|
|
1113
1143
|
const priceX = chartRight + 4;
|
|
1114
1144
|
const priceY = clamp(cy - labelHeight / 2, chartTop, chartBottom - labelHeight);
|
|
1115
1145
|
ctx.fillStyle = labelBackground;
|
package/dist/index.d.cts
CHANGED
|
@@ -5,6 +5,9 @@ interface ChartOptions {
|
|
|
5
5
|
axisColor?: string;
|
|
6
6
|
axis?: AxisOptions;
|
|
7
7
|
priceDecimals?: number;
|
|
8
|
+
stabilizePriceLabels?: boolean;
|
|
9
|
+
priceLabelMinIntegerDigits?: number;
|
|
10
|
+
priceLabelWidthTemplate?: string;
|
|
8
11
|
initialViewport?: "latest" | "center";
|
|
9
12
|
initialVisibleBars?: number;
|
|
10
13
|
minVisibleBars?: number;
|
|
@@ -128,6 +131,8 @@ interface OrderLineOptions {
|
|
|
128
131
|
actionButtonFontWeight?: number | string;
|
|
129
132
|
actionButtonBorderColor?: string;
|
|
130
133
|
actionButtonBorderStyle?: "solid" | "dotted" | "dashed";
|
|
134
|
+
actionButtonsInnerGap?: number;
|
|
135
|
+
actionButtonsGroupGap?: number;
|
|
131
136
|
actionButtons?: OrderActionButton[];
|
|
132
137
|
connectorToPrice?: number;
|
|
133
138
|
connectorColor?: string;
|
package/dist/index.d.ts
CHANGED
|
@@ -5,6 +5,9 @@ interface ChartOptions {
|
|
|
5
5
|
axisColor?: string;
|
|
6
6
|
axis?: AxisOptions;
|
|
7
7
|
priceDecimals?: number;
|
|
8
|
+
stabilizePriceLabels?: boolean;
|
|
9
|
+
priceLabelMinIntegerDigits?: number;
|
|
10
|
+
priceLabelWidthTemplate?: string;
|
|
8
11
|
initialViewport?: "latest" | "center";
|
|
9
12
|
initialVisibleBars?: number;
|
|
10
13
|
minVisibleBars?: number;
|
|
@@ -128,6 +131,8 @@ interface OrderLineOptions {
|
|
|
128
131
|
actionButtonFontWeight?: number | string;
|
|
129
132
|
actionButtonBorderColor?: string;
|
|
130
133
|
actionButtonBorderStyle?: "solid" | "dotted" | "dashed";
|
|
134
|
+
actionButtonsInnerGap?: number;
|
|
135
|
+
actionButtonsGroupGap?: number;
|
|
131
136
|
actionButtons?: OrderActionButton[];
|
|
132
137
|
connectorToPrice?: number;
|
|
133
138
|
connectorColor?: string;
|
package/dist/index.js
CHANGED
|
@@ -84,6 +84,8 @@ var DEFAULT_ORDER_LINE_OPTIONS = {
|
|
|
84
84
|
actionButtonFontWeight: 500,
|
|
85
85
|
actionButtonBorderColor: "#2563eb",
|
|
86
86
|
actionButtonBorderStyle: "solid",
|
|
87
|
+
actionButtonsInnerGap: 6,
|
|
88
|
+
actionButtonsGroupGap: 8,
|
|
87
89
|
actionButtons: [],
|
|
88
90
|
connectorToPrice: Number.NaN,
|
|
89
91
|
connectorColor: "#2563eb",
|
|
@@ -100,6 +102,9 @@ var DEFAULT_OPTIONS = {
|
|
|
100
102
|
axisColor: "#7f8289",
|
|
101
103
|
axis: DEFAULT_AXIS_OPTIONS,
|
|
102
104
|
priceDecimals: 2,
|
|
105
|
+
stabilizePriceLabels: true,
|
|
106
|
+
priceLabelMinIntegerDigits: 3,
|
|
107
|
+
priceLabelWidthTemplate: "",
|
|
103
108
|
initialViewport: "latest",
|
|
104
109
|
initialVisibleBars: 60,
|
|
105
110
|
minVisibleBars: 5,
|
|
@@ -339,6 +344,31 @@ function createChart(element, options = {}) {
|
|
|
339
344
|
const decimals = clamp(Math.round(mergedOptions.priceDecimals), 0, 8);
|
|
340
345
|
return price.toFixed(decimals);
|
|
341
346
|
};
|
|
347
|
+
const getStabilizedPriceTemplate = () => {
|
|
348
|
+
const explicitTemplate = mergedOptions.priceLabelWidthTemplate.trim();
|
|
349
|
+
if (explicitTemplate.length > 0) {
|
|
350
|
+
return explicitTemplate;
|
|
351
|
+
}
|
|
352
|
+
const decimals = clamp(Math.round(mergedOptions.priceDecimals), 0, 8);
|
|
353
|
+
const configuredDigits = Math.max(1, Math.floor(mergedOptions.priceLabelMinIntegerDigits));
|
|
354
|
+
let maxAbsPrice = 0;
|
|
355
|
+
for (const point of data) {
|
|
356
|
+
maxAbsPrice = Math.max(maxAbsPrice, Math.abs(point.o), Math.abs(point.h), Math.abs(point.l), Math.abs(point.c));
|
|
357
|
+
}
|
|
358
|
+
const observedDigits = maxAbsPrice >= 1 ? Math.floor(Math.log10(maxAbsPrice)) + 1 : 1;
|
|
359
|
+
const integerDigits = Math.max(configuredDigits, observedDigits);
|
|
360
|
+
const integerPart = "8".repeat(integerDigits);
|
|
361
|
+
const decimalPart = decimals > 0 ? `.${"8".repeat(decimals)}` : "";
|
|
362
|
+
return `${integerPart}${decimalPart}`;
|
|
363
|
+
};
|
|
364
|
+
const getPriceLabelWidth = (priceText, paddingX) => {
|
|
365
|
+
const measured = Math.ceil(ctx.measureText(priceText).width) + paddingX * 2;
|
|
366
|
+
if (!mergedOptions.stabilizePriceLabels) {
|
|
367
|
+
return measured;
|
|
368
|
+
}
|
|
369
|
+
const templateWidth = Math.ceil(ctx.measureText(getStabilizedPriceTemplate()).width) + paddingX * 2;
|
|
370
|
+
return Math.max(measured, templateWidth);
|
|
371
|
+
};
|
|
342
372
|
const parseData = (nextData) => {
|
|
343
373
|
return nextData.map((point) => ({
|
|
344
374
|
time: new Date(point.t),
|
|
@@ -457,7 +487,7 @@ function createChart(element, options = {}) {
|
|
|
457
487
|
ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
|
|
458
488
|
const labelPaddingX = 8;
|
|
459
489
|
const labelHeight = 20;
|
|
460
|
-
const labelWidth = Math.ceil(ctx.measureText(labelText).width) + labelPaddingX * 2;
|
|
490
|
+
const labelWidth = mergedLine.label === void 0 ? getPriceLabelWidth(labelText, labelPaddingX) : Math.ceil(ctx.measureText(labelText).width) + labelPaddingX * 2;
|
|
461
491
|
const labelX = chartRight + 4;
|
|
462
492
|
const labelY = clamp(lineY - labelHeight / 2, chartTop, chartBottom - labelHeight);
|
|
463
493
|
ctx.fillStyle = mergedLine.labelBackgroundColor;
|
|
@@ -559,9 +589,9 @@ function createChart(element, options = {}) {
|
|
|
559
589
|
const width2 = Math.max(minWidth, Math.ceil(ctx.measureText(button.text).width) + paddingX * 2);
|
|
560
590
|
return { button, width: width2 };
|
|
561
591
|
});
|
|
562
|
-
const actionButtonInnerGap = actionButtonMetrics.length > 1 ?
|
|
592
|
+
const actionButtonInnerGap = actionButtonMetrics.length > 1 ? Math.max(0, mergedLine.actionButtonsInnerGap) : 0;
|
|
563
593
|
const actionButtonsTotalWidth = actionButtonMetrics.reduce((sum, metric) => sum + metric.width, 0) + Math.max(0, actionButtonMetrics.length - 1) * actionButtonInnerGap;
|
|
564
|
-
const actionButtonsGap = actionButtonMetrics.length > 0 ?
|
|
594
|
+
const actionButtonsGap = actionButtonMetrics.length > 0 ? Math.max(0, mergedLine.actionButtonsGroupGap) : 0;
|
|
565
595
|
const segmentPaddingX = 8;
|
|
566
596
|
const labelHeight = 22;
|
|
567
597
|
const qtyWidth = qtyText ? Math.ceil(ctx.measureText(qtyText).width) + segmentPaddingX * 2 : 0;
|
|
@@ -685,7 +715,7 @@ function createChart(element, options = {}) {
|
|
|
685
715
|
}
|
|
686
716
|
const priceText = formatPrice(renderPrice);
|
|
687
717
|
const pricePaddingX = 8;
|
|
688
|
-
const measuredPriceWidth =
|
|
718
|
+
const measuredPriceWidth = getPriceLabelWidth(priceText, pricePaddingX);
|
|
689
719
|
const priceWidth = mergedLine.id === void 0 ? measuredPriceWidth : Math.max(measuredPriceWidth, orderPriceTagWidthById.get(mergedLine.id) ?? 0);
|
|
690
720
|
if (mergedLine.id) {
|
|
691
721
|
orderPriceTagWidthById.set(mergedLine.id, priceWidth);
|
|
@@ -1012,7 +1042,7 @@ function createChart(element, options = {}) {
|
|
|
1012
1042
|
ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
|
|
1013
1043
|
const labelPaddingX = 8;
|
|
1014
1044
|
const labelHeight = 20;
|
|
1015
|
-
const labelWidth =
|
|
1045
|
+
const labelWidth = getPriceLabelWidth(tickerLabel, labelPaddingX);
|
|
1016
1046
|
const labelX = chartRight + 4;
|
|
1017
1047
|
const labelY = clamp(lineY - labelHeight / 2, chartTop, chartBottom - labelHeight);
|
|
1018
1048
|
const labelRadius = Math.max(0, ticker.labelBorderRadius ?? 0);
|
|
@@ -1085,7 +1115,7 @@ function createChart(element, options = {}) {
|
|
|
1085
1115
|
if (crosshair.showPriceLabel) {
|
|
1086
1116
|
const hoverPrice = yMin + (chartBottom - cy) / chartHeight * yRange;
|
|
1087
1117
|
const priceText = formatPrice(hoverPrice);
|
|
1088
|
-
const priceWidth =
|
|
1118
|
+
const priceWidth = getPriceLabelWidth(priceText, labelPaddingX);
|
|
1089
1119
|
const priceX = chartRight + 4;
|
|
1090
1120
|
const priceY = clamp(cy - labelHeight / 2, chartTop, chartBottom - labelHeight);
|
|
1091
1121
|
ctx.fillStyle = labelBackground;
|
package/docs/API.md
CHANGED
|
@@ -33,6 +33,9 @@ Top-level options:
|
|
|
33
33
|
- `axisColor` (legacy shorthand for axis line/text color)
|
|
34
34
|
- `axis?: AxisOptions`
|
|
35
35
|
- `priceDecimals` (default `2`, used for axis/ticker/line price labels)
|
|
36
|
+
- `stabilizePriceLabels` (default `true`, prevents ticker/crosshair/price-tag width jitter)
|
|
37
|
+
- `priceLabelMinIntegerDigits` (default `3`, baseline integer-digit width for stabilized labels)
|
|
38
|
+
- `priceLabelWidthTemplate` (default `""`; when set, forces label width from this template, e.g. `"88888.88"`)
|
|
36
39
|
- `initialViewport` (`"latest"` | `"center"`, default `"latest"`)
|
|
37
40
|
- `initialVisibleBars` (default `60`)
|
|
38
41
|
- `minVisibleBars` (default `5`, lower clamp for x zoom)
|
|
@@ -208,6 +211,8 @@ Legacy single action button:
|
|
|
208
211
|
- `actionButtonFontWeight?: number | string`
|
|
209
212
|
- `actionButtonBorderColor?: string`
|
|
210
213
|
- `actionButtonBorderStyle?: "solid" | "dotted" | "dashed"`
|
|
214
|
+
- `actionButtonsInnerGap?: number` (default `6`, spacing between action buttons)
|
|
215
|
+
- `actionButtonsGroupGap?: number` (default `8`, spacing between action-button group and main order widget)
|
|
211
216
|
|
|
212
217
|
Multi-button actions:
|
|
213
218
|
|
package/docs/RECIPES.md
CHANGED
|
@@ -51,6 +51,18 @@ const chart = createChart(root, {
|
|
|
51
51
|
});
|
|
52
52
|
```
|
|
53
53
|
|
|
54
|
+
## Keep price labels from shaking on fast ticks
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
const chart = createChart(root, {
|
|
58
|
+
priceDecimals: 2,
|
|
59
|
+
stabilizePriceLabels: true,
|
|
60
|
+
priceLabelMinIntegerDigits: 4,
|
|
61
|
+
// If you still see any width movement, pin exact width:
|
|
62
|
+
// priceLabelWidthTemplate: "88888.88"
|
|
63
|
+
});
|
|
64
|
+
```
|
|
65
|
+
|
|
54
66
|
## Add a draggable pending limit line
|
|
55
67
|
|
|
56
68
|
```ts
|