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 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 ? 4 : 0;
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 ? 6 : 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 = Math.ceil(ctx.measureText(priceText).width) + pricePaddingX * 2;
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 = Math.ceil(ctx.measureText(tickerLabel).width) + labelPaddingX * 2;
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 = Math.ceil(ctx.measureText(priceText).width) + labelPaddingX * 2;
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 ? 4 : 0;
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 ? 6 : 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 = Math.ceil(ctx.measureText(priceText).width) + pricePaddingX * 2;
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 = Math.ceil(ctx.measureText(tickerLabel).width) + labelPaddingX * 2;
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 = Math.ceil(ctx.measureText(priceText).width) + labelPaddingX * 2;
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 ? 4 : 0;
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 ? 6 : 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 = Math.ceil(ctx.measureText(priceText).width) + pricePaddingX * 2;
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 = Math.ceil(ctx.measureText(tickerLabel).width) + labelPaddingX * 2;
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 = Math.ceil(ctx.measureText(priceText).width) + labelPaddingX * 2;
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 ? 4 : 0;
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 ? 6 : 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 = Math.ceil(ctx.measureText(priceText).width) + pricePaddingX * 2;
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 = Math.ceil(ctx.measureText(tickerLabel).width) + labelPaddingX * 2;
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 = Math.ceil(ctx.measureText(priceText).width) + labelPaddingX * 2;
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hyperprop-charting-library",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
4
4
  "description": "Lightweight TypeScript charting core",
5
5
  "type": "module",
6
6
  "main": "./dist/hyperprop-charting-library.cjs",