hyperprop-charting-library 0.1.13 → 0.1.14
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 +12 -0
- package/dist/hyperprop-charting-library.cjs +31 -6
- package/dist/hyperprop-charting-library.d.ts +4 -0
- package/dist/hyperprop-charting-library.js +31 -6
- package/dist/index.cjs +31 -6
- package/dist/index.d.cts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +31 -6
- package/docs/API.md +4 -0
- package/docs/RECIPES.md +10 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -46,6 +46,18 @@ 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
|
+
});
|
|
59
|
+
```
|
|
60
|
+
|
|
49
61
|
## Full Documentation
|
|
50
62
|
|
|
51
63
|
- 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,8 @@ var DEFAULT_OPTIONS = {
|
|
|
124
126
|
axisColor: "#7f8289",
|
|
125
127
|
axis: DEFAULT_AXIS_OPTIONS,
|
|
126
128
|
priceDecimals: 2,
|
|
129
|
+
stabilizePriceLabels: true,
|
|
130
|
+
priceLabelMinIntegerDigits: 3,
|
|
127
131
|
initialViewport: "latest",
|
|
128
132
|
initialVisibleBars: 60,
|
|
129
133
|
minVisibleBars: 5,
|
|
@@ -363,6 +367,27 @@ function createChart(element, options = {}) {
|
|
|
363
367
|
const decimals = clamp(Math.round(mergedOptions.priceDecimals), 0, 8);
|
|
364
368
|
return price.toFixed(decimals);
|
|
365
369
|
};
|
|
370
|
+
const getStabilizedPriceTemplate = () => {
|
|
371
|
+
const decimals = clamp(Math.round(mergedOptions.priceDecimals), 0, 8);
|
|
372
|
+
const configuredDigits = Math.max(1, Math.floor(mergedOptions.priceLabelMinIntegerDigits));
|
|
373
|
+
let maxAbsPrice = 0;
|
|
374
|
+
for (const point of data) {
|
|
375
|
+
maxAbsPrice = Math.max(maxAbsPrice, Math.abs(point.o), Math.abs(point.h), Math.abs(point.l), Math.abs(point.c));
|
|
376
|
+
}
|
|
377
|
+
const observedDigits = maxAbsPrice >= 1 ? Math.floor(Math.log10(maxAbsPrice)) + 1 : 1;
|
|
378
|
+
const integerDigits = Math.max(configuredDigits, observedDigits);
|
|
379
|
+
const integerPart = "8".repeat(integerDigits);
|
|
380
|
+
const decimalPart = decimals > 0 ? `.${"8".repeat(decimals)}` : "";
|
|
381
|
+
return `${integerPart}${decimalPart}`;
|
|
382
|
+
};
|
|
383
|
+
const getPriceLabelWidth = (priceText, paddingX) => {
|
|
384
|
+
const measured = Math.ceil(ctx.measureText(priceText).width) + paddingX * 2;
|
|
385
|
+
if (!mergedOptions.stabilizePriceLabels) {
|
|
386
|
+
return measured;
|
|
387
|
+
}
|
|
388
|
+
const templateWidth = Math.ceil(ctx.measureText(getStabilizedPriceTemplate()).width) + paddingX * 2;
|
|
389
|
+
return Math.max(measured, templateWidth);
|
|
390
|
+
};
|
|
366
391
|
const parseData = (nextData) => {
|
|
367
392
|
return nextData.map((point) => ({
|
|
368
393
|
time: new Date(point.t),
|
|
@@ -481,7 +506,7 @@ function createChart(element, options = {}) {
|
|
|
481
506
|
ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
|
|
482
507
|
const labelPaddingX = 8;
|
|
483
508
|
const labelHeight = 20;
|
|
484
|
-
const labelWidth = Math.ceil(ctx.measureText(labelText).width) + labelPaddingX * 2;
|
|
509
|
+
const labelWidth = mergedLine.label === void 0 ? getPriceLabelWidth(labelText, labelPaddingX) : Math.ceil(ctx.measureText(labelText).width) + labelPaddingX * 2;
|
|
485
510
|
const labelX = chartRight + 4;
|
|
486
511
|
const labelY = clamp(lineY - labelHeight / 2, chartTop, chartBottom - labelHeight);
|
|
487
512
|
ctx.fillStyle = mergedLine.labelBackgroundColor;
|
|
@@ -583,9 +608,9 @@ function createChart(element, options = {}) {
|
|
|
583
608
|
const width2 = Math.max(minWidth, Math.ceil(ctx.measureText(button.text).width) + paddingX * 2);
|
|
584
609
|
return { button, width: width2 };
|
|
585
610
|
});
|
|
586
|
-
const actionButtonInnerGap = actionButtonMetrics.length > 1 ?
|
|
611
|
+
const actionButtonInnerGap = actionButtonMetrics.length > 1 ? Math.max(0, mergedLine.actionButtonsInnerGap) : 0;
|
|
587
612
|
const actionButtonsTotalWidth = actionButtonMetrics.reduce((sum, metric) => sum + metric.width, 0) + Math.max(0, actionButtonMetrics.length - 1) * actionButtonInnerGap;
|
|
588
|
-
const actionButtonsGap = actionButtonMetrics.length > 0 ?
|
|
613
|
+
const actionButtonsGap = actionButtonMetrics.length > 0 ? Math.max(0, mergedLine.actionButtonsGroupGap) : 0;
|
|
589
614
|
const segmentPaddingX = 8;
|
|
590
615
|
const labelHeight = 22;
|
|
591
616
|
const qtyWidth = qtyText ? Math.ceil(ctx.measureText(qtyText).width) + segmentPaddingX * 2 : 0;
|
|
@@ -709,7 +734,7 @@ function createChart(element, options = {}) {
|
|
|
709
734
|
}
|
|
710
735
|
const priceText = formatPrice(renderPrice);
|
|
711
736
|
const pricePaddingX = 8;
|
|
712
|
-
const measuredPriceWidth =
|
|
737
|
+
const measuredPriceWidth = getPriceLabelWidth(priceText, pricePaddingX);
|
|
713
738
|
const priceWidth = mergedLine.id === void 0 ? measuredPriceWidth : Math.max(measuredPriceWidth, orderPriceTagWidthById.get(mergedLine.id) ?? 0);
|
|
714
739
|
if (mergedLine.id) {
|
|
715
740
|
orderPriceTagWidthById.set(mergedLine.id, priceWidth);
|
|
@@ -1036,7 +1061,7 @@ function createChart(element, options = {}) {
|
|
|
1036
1061
|
ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
|
|
1037
1062
|
const labelPaddingX = 8;
|
|
1038
1063
|
const labelHeight = 20;
|
|
1039
|
-
const labelWidth =
|
|
1064
|
+
const labelWidth = getPriceLabelWidth(tickerLabel, labelPaddingX);
|
|
1040
1065
|
const labelX = chartRight + 4;
|
|
1041
1066
|
const labelY = clamp(lineY - labelHeight / 2, chartTop, chartBottom - labelHeight);
|
|
1042
1067
|
const labelRadius = Math.max(0, ticker.labelBorderRadius ?? 0);
|
|
@@ -1109,7 +1134,7 @@ function createChart(element, options = {}) {
|
|
|
1109
1134
|
if (crosshair.showPriceLabel) {
|
|
1110
1135
|
const hoverPrice = yMin + (chartBottom - cy) / chartHeight * yRange;
|
|
1111
1136
|
const priceText = formatPrice(hoverPrice);
|
|
1112
|
-
const priceWidth =
|
|
1137
|
+
const priceWidth = getPriceLabelWidth(priceText, labelPaddingX);
|
|
1113
1138
|
const priceX = chartRight + 4;
|
|
1114
1139
|
const priceY = clamp(cy - labelHeight / 2, chartTop, chartBottom - labelHeight);
|
|
1115
1140
|
ctx.fillStyle = labelBackground;
|
|
@@ -5,6 +5,8 @@ interface ChartOptions {
|
|
|
5
5
|
axisColor?: string;
|
|
6
6
|
axis?: AxisOptions;
|
|
7
7
|
priceDecimals?: number;
|
|
8
|
+
stabilizePriceLabels?: boolean;
|
|
9
|
+
priceLabelMinIntegerDigits?: number;
|
|
8
10
|
initialViewport?: "latest" | "center";
|
|
9
11
|
initialVisibleBars?: number;
|
|
10
12
|
minVisibleBars?: number;
|
|
@@ -128,6 +130,8 @@ interface OrderLineOptions {
|
|
|
128
130
|
actionButtonFontWeight?: number | string;
|
|
129
131
|
actionButtonBorderColor?: string;
|
|
130
132
|
actionButtonBorderStyle?: "solid" | "dotted" | "dashed";
|
|
133
|
+
actionButtonsInnerGap?: number;
|
|
134
|
+
actionButtonsGroupGap?: number;
|
|
131
135
|
actionButtons?: OrderActionButton[];
|
|
132
136
|
connectorToPrice?: number;
|
|
133
137
|
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,8 @@ var DEFAULT_OPTIONS = {
|
|
|
100
102
|
axisColor: "#7f8289",
|
|
101
103
|
axis: DEFAULT_AXIS_OPTIONS,
|
|
102
104
|
priceDecimals: 2,
|
|
105
|
+
stabilizePriceLabels: true,
|
|
106
|
+
priceLabelMinIntegerDigits: 3,
|
|
103
107
|
initialViewport: "latest",
|
|
104
108
|
initialVisibleBars: 60,
|
|
105
109
|
minVisibleBars: 5,
|
|
@@ -339,6 +343,27 @@ function createChart(element, options = {}) {
|
|
|
339
343
|
const decimals = clamp(Math.round(mergedOptions.priceDecimals), 0, 8);
|
|
340
344
|
return price.toFixed(decimals);
|
|
341
345
|
};
|
|
346
|
+
const getStabilizedPriceTemplate = () => {
|
|
347
|
+
const decimals = clamp(Math.round(mergedOptions.priceDecimals), 0, 8);
|
|
348
|
+
const configuredDigits = Math.max(1, Math.floor(mergedOptions.priceLabelMinIntegerDigits));
|
|
349
|
+
let maxAbsPrice = 0;
|
|
350
|
+
for (const point of data) {
|
|
351
|
+
maxAbsPrice = Math.max(maxAbsPrice, Math.abs(point.o), Math.abs(point.h), Math.abs(point.l), Math.abs(point.c));
|
|
352
|
+
}
|
|
353
|
+
const observedDigits = maxAbsPrice >= 1 ? Math.floor(Math.log10(maxAbsPrice)) + 1 : 1;
|
|
354
|
+
const integerDigits = Math.max(configuredDigits, observedDigits);
|
|
355
|
+
const integerPart = "8".repeat(integerDigits);
|
|
356
|
+
const decimalPart = decimals > 0 ? `.${"8".repeat(decimals)}` : "";
|
|
357
|
+
return `${integerPart}${decimalPart}`;
|
|
358
|
+
};
|
|
359
|
+
const getPriceLabelWidth = (priceText, paddingX) => {
|
|
360
|
+
const measured = Math.ceil(ctx.measureText(priceText).width) + paddingX * 2;
|
|
361
|
+
if (!mergedOptions.stabilizePriceLabels) {
|
|
362
|
+
return measured;
|
|
363
|
+
}
|
|
364
|
+
const templateWidth = Math.ceil(ctx.measureText(getStabilizedPriceTemplate()).width) + paddingX * 2;
|
|
365
|
+
return Math.max(measured, templateWidth);
|
|
366
|
+
};
|
|
342
367
|
const parseData = (nextData) => {
|
|
343
368
|
return nextData.map((point) => ({
|
|
344
369
|
time: new Date(point.t),
|
|
@@ -457,7 +482,7 @@ function createChart(element, options = {}) {
|
|
|
457
482
|
ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
|
|
458
483
|
const labelPaddingX = 8;
|
|
459
484
|
const labelHeight = 20;
|
|
460
|
-
const labelWidth = Math.ceil(ctx.measureText(labelText).width) + labelPaddingX * 2;
|
|
485
|
+
const labelWidth = mergedLine.label === void 0 ? getPriceLabelWidth(labelText, labelPaddingX) : Math.ceil(ctx.measureText(labelText).width) + labelPaddingX * 2;
|
|
461
486
|
const labelX = chartRight + 4;
|
|
462
487
|
const labelY = clamp(lineY - labelHeight / 2, chartTop, chartBottom - labelHeight);
|
|
463
488
|
ctx.fillStyle = mergedLine.labelBackgroundColor;
|
|
@@ -559,9 +584,9 @@ function createChart(element, options = {}) {
|
|
|
559
584
|
const width2 = Math.max(minWidth, Math.ceil(ctx.measureText(button.text).width) + paddingX * 2);
|
|
560
585
|
return { button, width: width2 };
|
|
561
586
|
});
|
|
562
|
-
const actionButtonInnerGap = actionButtonMetrics.length > 1 ?
|
|
587
|
+
const actionButtonInnerGap = actionButtonMetrics.length > 1 ? Math.max(0, mergedLine.actionButtonsInnerGap) : 0;
|
|
563
588
|
const actionButtonsTotalWidth = actionButtonMetrics.reduce((sum, metric) => sum + metric.width, 0) + Math.max(0, actionButtonMetrics.length - 1) * actionButtonInnerGap;
|
|
564
|
-
const actionButtonsGap = actionButtonMetrics.length > 0 ?
|
|
589
|
+
const actionButtonsGap = actionButtonMetrics.length > 0 ? Math.max(0, mergedLine.actionButtonsGroupGap) : 0;
|
|
565
590
|
const segmentPaddingX = 8;
|
|
566
591
|
const labelHeight = 22;
|
|
567
592
|
const qtyWidth = qtyText ? Math.ceil(ctx.measureText(qtyText).width) + segmentPaddingX * 2 : 0;
|
|
@@ -685,7 +710,7 @@ function createChart(element, options = {}) {
|
|
|
685
710
|
}
|
|
686
711
|
const priceText = formatPrice(renderPrice);
|
|
687
712
|
const pricePaddingX = 8;
|
|
688
|
-
const measuredPriceWidth =
|
|
713
|
+
const measuredPriceWidth = getPriceLabelWidth(priceText, pricePaddingX);
|
|
689
714
|
const priceWidth = mergedLine.id === void 0 ? measuredPriceWidth : Math.max(measuredPriceWidth, orderPriceTagWidthById.get(mergedLine.id) ?? 0);
|
|
690
715
|
if (mergedLine.id) {
|
|
691
716
|
orderPriceTagWidthById.set(mergedLine.id, priceWidth);
|
|
@@ -1012,7 +1037,7 @@ function createChart(element, options = {}) {
|
|
|
1012
1037
|
ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
|
|
1013
1038
|
const labelPaddingX = 8;
|
|
1014
1039
|
const labelHeight = 20;
|
|
1015
|
-
const labelWidth =
|
|
1040
|
+
const labelWidth = getPriceLabelWidth(tickerLabel, labelPaddingX);
|
|
1016
1041
|
const labelX = chartRight + 4;
|
|
1017
1042
|
const labelY = clamp(lineY - labelHeight / 2, chartTop, chartBottom - labelHeight);
|
|
1018
1043
|
const labelRadius = Math.max(0, ticker.labelBorderRadius ?? 0);
|
|
@@ -1085,7 +1110,7 @@ function createChart(element, options = {}) {
|
|
|
1085
1110
|
if (crosshair.showPriceLabel) {
|
|
1086
1111
|
const hoverPrice = yMin + (chartBottom - cy) / chartHeight * yRange;
|
|
1087
1112
|
const priceText = formatPrice(hoverPrice);
|
|
1088
|
-
const priceWidth =
|
|
1113
|
+
const priceWidth = getPriceLabelWidth(priceText, labelPaddingX);
|
|
1089
1114
|
const priceX = chartRight + 4;
|
|
1090
1115
|
const priceY = clamp(cy - labelHeight / 2, chartTop, chartBottom - labelHeight);
|
|
1091
1116
|
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,8 @@ var DEFAULT_OPTIONS = {
|
|
|
124
126
|
axisColor: "#7f8289",
|
|
125
127
|
axis: DEFAULT_AXIS_OPTIONS,
|
|
126
128
|
priceDecimals: 2,
|
|
129
|
+
stabilizePriceLabels: true,
|
|
130
|
+
priceLabelMinIntegerDigits: 3,
|
|
127
131
|
initialViewport: "latest",
|
|
128
132
|
initialVisibleBars: 60,
|
|
129
133
|
minVisibleBars: 5,
|
|
@@ -363,6 +367,27 @@ function createChart(element, options = {}) {
|
|
|
363
367
|
const decimals = clamp(Math.round(mergedOptions.priceDecimals), 0, 8);
|
|
364
368
|
return price.toFixed(decimals);
|
|
365
369
|
};
|
|
370
|
+
const getStabilizedPriceTemplate = () => {
|
|
371
|
+
const decimals = clamp(Math.round(mergedOptions.priceDecimals), 0, 8);
|
|
372
|
+
const configuredDigits = Math.max(1, Math.floor(mergedOptions.priceLabelMinIntegerDigits));
|
|
373
|
+
let maxAbsPrice = 0;
|
|
374
|
+
for (const point of data) {
|
|
375
|
+
maxAbsPrice = Math.max(maxAbsPrice, Math.abs(point.o), Math.abs(point.h), Math.abs(point.l), Math.abs(point.c));
|
|
376
|
+
}
|
|
377
|
+
const observedDigits = maxAbsPrice >= 1 ? Math.floor(Math.log10(maxAbsPrice)) + 1 : 1;
|
|
378
|
+
const integerDigits = Math.max(configuredDigits, observedDigits);
|
|
379
|
+
const integerPart = "8".repeat(integerDigits);
|
|
380
|
+
const decimalPart = decimals > 0 ? `.${"8".repeat(decimals)}` : "";
|
|
381
|
+
return `${integerPart}${decimalPart}`;
|
|
382
|
+
};
|
|
383
|
+
const getPriceLabelWidth = (priceText, paddingX) => {
|
|
384
|
+
const measured = Math.ceil(ctx.measureText(priceText).width) + paddingX * 2;
|
|
385
|
+
if (!mergedOptions.stabilizePriceLabels) {
|
|
386
|
+
return measured;
|
|
387
|
+
}
|
|
388
|
+
const templateWidth = Math.ceil(ctx.measureText(getStabilizedPriceTemplate()).width) + paddingX * 2;
|
|
389
|
+
return Math.max(measured, templateWidth);
|
|
390
|
+
};
|
|
366
391
|
const parseData = (nextData) => {
|
|
367
392
|
return nextData.map((point) => ({
|
|
368
393
|
time: new Date(point.t),
|
|
@@ -481,7 +506,7 @@ function createChart(element, options = {}) {
|
|
|
481
506
|
ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
|
|
482
507
|
const labelPaddingX = 8;
|
|
483
508
|
const labelHeight = 20;
|
|
484
|
-
const labelWidth = Math.ceil(ctx.measureText(labelText).width) + labelPaddingX * 2;
|
|
509
|
+
const labelWidth = mergedLine.label === void 0 ? getPriceLabelWidth(labelText, labelPaddingX) : Math.ceil(ctx.measureText(labelText).width) + labelPaddingX * 2;
|
|
485
510
|
const labelX = chartRight + 4;
|
|
486
511
|
const labelY = clamp(lineY - labelHeight / 2, chartTop, chartBottom - labelHeight);
|
|
487
512
|
ctx.fillStyle = mergedLine.labelBackgroundColor;
|
|
@@ -583,9 +608,9 @@ function createChart(element, options = {}) {
|
|
|
583
608
|
const width2 = Math.max(minWidth, Math.ceil(ctx.measureText(button.text).width) + paddingX * 2);
|
|
584
609
|
return { button, width: width2 };
|
|
585
610
|
});
|
|
586
|
-
const actionButtonInnerGap = actionButtonMetrics.length > 1 ?
|
|
611
|
+
const actionButtonInnerGap = actionButtonMetrics.length > 1 ? Math.max(0, mergedLine.actionButtonsInnerGap) : 0;
|
|
587
612
|
const actionButtonsTotalWidth = actionButtonMetrics.reduce((sum, metric) => sum + metric.width, 0) + Math.max(0, actionButtonMetrics.length - 1) * actionButtonInnerGap;
|
|
588
|
-
const actionButtonsGap = actionButtonMetrics.length > 0 ?
|
|
613
|
+
const actionButtonsGap = actionButtonMetrics.length > 0 ? Math.max(0, mergedLine.actionButtonsGroupGap) : 0;
|
|
589
614
|
const segmentPaddingX = 8;
|
|
590
615
|
const labelHeight = 22;
|
|
591
616
|
const qtyWidth = qtyText ? Math.ceil(ctx.measureText(qtyText).width) + segmentPaddingX * 2 : 0;
|
|
@@ -709,7 +734,7 @@ function createChart(element, options = {}) {
|
|
|
709
734
|
}
|
|
710
735
|
const priceText = formatPrice(renderPrice);
|
|
711
736
|
const pricePaddingX = 8;
|
|
712
|
-
const measuredPriceWidth =
|
|
737
|
+
const measuredPriceWidth = getPriceLabelWidth(priceText, pricePaddingX);
|
|
713
738
|
const priceWidth = mergedLine.id === void 0 ? measuredPriceWidth : Math.max(measuredPriceWidth, orderPriceTagWidthById.get(mergedLine.id) ?? 0);
|
|
714
739
|
if (mergedLine.id) {
|
|
715
740
|
orderPriceTagWidthById.set(mergedLine.id, priceWidth);
|
|
@@ -1036,7 +1061,7 @@ function createChart(element, options = {}) {
|
|
|
1036
1061
|
ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
|
|
1037
1062
|
const labelPaddingX = 8;
|
|
1038
1063
|
const labelHeight = 20;
|
|
1039
|
-
const labelWidth =
|
|
1064
|
+
const labelWidth = getPriceLabelWidth(tickerLabel, labelPaddingX);
|
|
1040
1065
|
const labelX = chartRight + 4;
|
|
1041
1066
|
const labelY = clamp(lineY - labelHeight / 2, chartTop, chartBottom - labelHeight);
|
|
1042
1067
|
const labelRadius = Math.max(0, ticker.labelBorderRadius ?? 0);
|
|
@@ -1109,7 +1134,7 @@ function createChart(element, options = {}) {
|
|
|
1109
1134
|
if (crosshair.showPriceLabel) {
|
|
1110
1135
|
const hoverPrice = yMin + (chartBottom - cy) / chartHeight * yRange;
|
|
1111
1136
|
const priceText = formatPrice(hoverPrice);
|
|
1112
|
-
const priceWidth =
|
|
1137
|
+
const priceWidth = getPriceLabelWidth(priceText, labelPaddingX);
|
|
1113
1138
|
const priceX = chartRight + 4;
|
|
1114
1139
|
const priceY = clamp(cy - labelHeight / 2, chartTop, chartBottom - labelHeight);
|
|
1115
1140
|
ctx.fillStyle = labelBackground;
|
package/dist/index.d.cts
CHANGED
|
@@ -5,6 +5,8 @@ interface ChartOptions {
|
|
|
5
5
|
axisColor?: string;
|
|
6
6
|
axis?: AxisOptions;
|
|
7
7
|
priceDecimals?: number;
|
|
8
|
+
stabilizePriceLabels?: boolean;
|
|
9
|
+
priceLabelMinIntegerDigits?: number;
|
|
8
10
|
initialViewport?: "latest" | "center";
|
|
9
11
|
initialVisibleBars?: number;
|
|
10
12
|
minVisibleBars?: number;
|
|
@@ -128,6 +130,8 @@ interface OrderLineOptions {
|
|
|
128
130
|
actionButtonFontWeight?: number | string;
|
|
129
131
|
actionButtonBorderColor?: string;
|
|
130
132
|
actionButtonBorderStyle?: "solid" | "dotted" | "dashed";
|
|
133
|
+
actionButtonsInnerGap?: number;
|
|
134
|
+
actionButtonsGroupGap?: number;
|
|
131
135
|
actionButtons?: OrderActionButton[];
|
|
132
136
|
connectorToPrice?: number;
|
|
133
137
|
connectorColor?: string;
|
package/dist/index.d.ts
CHANGED
|
@@ -5,6 +5,8 @@ interface ChartOptions {
|
|
|
5
5
|
axisColor?: string;
|
|
6
6
|
axis?: AxisOptions;
|
|
7
7
|
priceDecimals?: number;
|
|
8
|
+
stabilizePriceLabels?: boolean;
|
|
9
|
+
priceLabelMinIntegerDigits?: number;
|
|
8
10
|
initialViewport?: "latest" | "center";
|
|
9
11
|
initialVisibleBars?: number;
|
|
10
12
|
minVisibleBars?: number;
|
|
@@ -128,6 +130,8 @@ interface OrderLineOptions {
|
|
|
128
130
|
actionButtonFontWeight?: number | string;
|
|
129
131
|
actionButtonBorderColor?: string;
|
|
130
132
|
actionButtonBorderStyle?: "solid" | "dotted" | "dashed";
|
|
133
|
+
actionButtonsInnerGap?: number;
|
|
134
|
+
actionButtonsGroupGap?: number;
|
|
131
135
|
actionButtons?: OrderActionButton[];
|
|
132
136
|
connectorToPrice?: number;
|
|
133
137
|
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,8 @@ var DEFAULT_OPTIONS = {
|
|
|
100
102
|
axisColor: "#7f8289",
|
|
101
103
|
axis: DEFAULT_AXIS_OPTIONS,
|
|
102
104
|
priceDecimals: 2,
|
|
105
|
+
stabilizePriceLabels: true,
|
|
106
|
+
priceLabelMinIntegerDigits: 3,
|
|
103
107
|
initialViewport: "latest",
|
|
104
108
|
initialVisibleBars: 60,
|
|
105
109
|
minVisibleBars: 5,
|
|
@@ -339,6 +343,27 @@ function createChart(element, options = {}) {
|
|
|
339
343
|
const decimals = clamp(Math.round(mergedOptions.priceDecimals), 0, 8);
|
|
340
344
|
return price.toFixed(decimals);
|
|
341
345
|
};
|
|
346
|
+
const getStabilizedPriceTemplate = () => {
|
|
347
|
+
const decimals = clamp(Math.round(mergedOptions.priceDecimals), 0, 8);
|
|
348
|
+
const configuredDigits = Math.max(1, Math.floor(mergedOptions.priceLabelMinIntegerDigits));
|
|
349
|
+
let maxAbsPrice = 0;
|
|
350
|
+
for (const point of data) {
|
|
351
|
+
maxAbsPrice = Math.max(maxAbsPrice, Math.abs(point.o), Math.abs(point.h), Math.abs(point.l), Math.abs(point.c));
|
|
352
|
+
}
|
|
353
|
+
const observedDigits = maxAbsPrice >= 1 ? Math.floor(Math.log10(maxAbsPrice)) + 1 : 1;
|
|
354
|
+
const integerDigits = Math.max(configuredDigits, observedDigits);
|
|
355
|
+
const integerPart = "8".repeat(integerDigits);
|
|
356
|
+
const decimalPart = decimals > 0 ? `.${"8".repeat(decimals)}` : "";
|
|
357
|
+
return `${integerPart}${decimalPart}`;
|
|
358
|
+
};
|
|
359
|
+
const getPriceLabelWidth = (priceText, paddingX) => {
|
|
360
|
+
const measured = Math.ceil(ctx.measureText(priceText).width) + paddingX * 2;
|
|
361
|
+
if (!mergedOptions.stabilizePriceLabels) {
|
|
362
|
+
return measured;
|
|
363
|
+
}
|
|
364
|
+
const templateWidth = Math.ceil(ctx.measureText(getStabilizedPriceTemplate()).width) + paddingX * 2;
|
|
365
|
+
return Math.max(measured, templateWidth);
|
|
366
|
+
};
|
|
342
367
|
const parseData = (nextData) => {
|
|
343
368
|
return nextData.map((point) => ({
|
|
344
369
|
time: new Date(point.t),
|
|
@@ -457,7 +482,7 @@ function createChart(element, options = {}) {
|
|
|
457
482
|
ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
|
|
458
483
|
const labelPaddingX = 8;
|
|
459
484
|
const labelHeight = 20;
|
|
460
|
-
const labelWidth = Math.ceil(ctx.measureText(labelText).width) + labelPaddingX * 2;
|
|
485
|
+
const labelWidth = mergedLine.label === void 0 ? getPriceLabelWidth(labelText, labelPaddingX) : Math.ceil(ctx.measureText(labelText).width) + labelPaddingX * 2;
|
|
461
486
|
const labelX = chartRight + 4;
|
|
462
487
|
const labelY = clamp(lineY - labelHeight / 2, chartTop, chartBottom - labelHeight);
|
|
463
488
|
ctx.fillStyle = mergedLine.labelBackgroundColor;
|
|
@@ -559,9 +584,9 @@ function createChart(element, options = {}) {
|
|
|
559
584
|
const width2 = Math.max(minWidth, Math.ceil(ctx.measureText(button.text).width) + paddingX * 2);
|
|
560
585
|
return { button, width: width2 };
|
|
561
586
|
});
|
|
562
|
-
const actionButtonInnerGap = actionButtonMetrics.length > 1 ?
|
|
587
|
+
const actionButtonInnerGap = actionButtonMetrics.length > 1 ? Math.max(0, mergedLine.actionButtonsInnerGap) : 0;
|
|
563
588
|
const actionButtonsTotalWidth = actionButtonMetrics.reduce((sum, metric) => sum + metric.width, 0) + Math.max(0, actionButtonMetrics.length - 1) * actionButtonInnerGap;
|
|
564
|
-
const actionButtonsGap = actionButtonMetrics.length > 0 ?
|
|
589
|
+
const actionButtonsGap = actionButtonMetrics.length > 0 ? Math.max(0, mergedLine.actionButtonsGroupGap) : 0;
|
|
565
590
|
const segmentPaddingX = 8;
|
|
566
591
|
const labelHeight = 22;
|
|
567
592
|
const qtyWidth = qtyText ? Math.ceil(ctx.measureText(qtyText).width) + segmentPaddingX * 2 : 0;
|
|
@@ -685,7 +710,7 @@ function createChart(element, options = {}) {
|
|
|
685
710
|
}
|
|
686
711
|
const priceText = formatPrice(renderPrice);
|
|
687
712
|
const pricePaddingX = 8;
|
|
688
|
-
const measuredPriceWidth =
|
|
713
|
+
const measuredPriceWidth = getPriceLabelWidth(priceText, pricePaddingX);
|
|
689
714
|
const priceWidth = mergedLine.id === void 0 ? measuredPriceWidth : Math.max(measuredPriceWidth, orderPriceTagWidthById.get(mergedLine.id) ?? 0);
|
|
690
715
|
if (mergedLine.id) {
|
|
691
716
|
orderPriceTagWidthById.set(mergedLine.id, priceWidth);
|
|
@@ -1012,7 +1037,7 @@ function createChart(element, options = {}) {
|
|
|
1012
1037
|
ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
|
|
1013
1038
|
const labelPaddingX = 8;
|
|
1014
1039
|
const labelHeight = 20;
|
|
1015
|
-
const labelWidth =
|
|
1040
|
+
const labelWidth = getPriceLabelWidth(tickerLabel, labelPaddingX);
|
|
1016
1041
|
const labelX = chartRight + 4;
|
|
1017
1042
|
const labelY = clamp(lineY - labelHeight / 2, chartTop, chartBottom - labelHeight);
|
|
1018
1043
|
const labelRadius = Math.max(0, ticker.labelBorderRadius ?? 0);
|
|
@@ -1085,7 +1110,7 @@ function createChart(element, options = {}) {
|
|
|
1085
1110
|
if (crosshair.showPriceLabel) {
|
|
1086
1111
|
const hoverPrice = yMin + (chartBottom - cy) / chartHeight * yRange;
|
|
1087
1112
|
const priceText = formatPrice(hoverPrice);
|
|
1088
|
-
const priceWidth =
|
|
1113
|
+
const priceWidth = getPriceLabelWidth(priceText, labelPaddingX);
|
|
1089
1114
|
const priceX = chartRight + 4;
|
|
1090
1115
|
const priceY = clamp(cy - labelHeight / 2, chartTop, chartBottom - labelHeight);
|
|
1091
1116
|
ctx.fillStyle = labelBackground;
|
package/docs/API.md
CHANGED
|
@@ -33,6 +33,8 @@ 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)
|
|
36
38
|
- `initialViewport` (`"latest"` | `"center"`, default `"latest"`)
|
|
37
39
|
- `initialVisibleBars` (default `60`)
|
|
38
40
|
- `minVisibleBars` (default `5`, lower clamp for x zoom)
|
|
@@ -208,6 +210,8 @@ Legacy single action button:
|
|
|
208
210
|
- `actionButtonFontWeight?: number | string`
|
|
209
211
|
- `actionButtonBorderColor?: string`
|
|
210
212
|
- `actionButtonBorderStyle?: "solid" | "dotted" | "dashed"`
|
|
213
|
+
- `actionButtonsInnerGap?: number` (default `6`, spacing between action buttons)
|
|
214
|
+
- `actionButtonsGroupGap?: number` (default `8`, spacing between action-button group and main order widget)
|
|
211
215
|
|
|
212
216
|
Multi-button actions:
|
|
213
217
|
|
package/docs/RECIPES.md
CHANGED
|
@@ -51,6 +51,16 @@ 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
|
+
});
|
|
62
|
+
```
|
|
63
|
+
|
|
54
64
|
## Add a draggable pending limit line
|
|
55
65
|
|
|
56
66
|
```ts
|