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 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 ? 4 : 0;
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 ? 6 : 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 = Math.ceil(ctx.measureText(priceText).width) + pricePaddingX * 2;
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 = Math.ceil(ctx.measureText(tickerLabel).width) + labelPaddingX * 2;
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 = Math.ceil(ctx.measureText(priceText).width) + labelPaddingX * 2;
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 ? 4 : 0;
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 ? 6 : 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 = Math.ceil(ctx.measureText(priceText).width) + pricePaddingX * 2;
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 = Math.ceil(ctx.measureText(tickerLabel).width) + labelPaddingX * 2;
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 = Math.ceil(ctx.measureText(priceText).width) + labelPaddingX * 2;
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 ? 4 : 0;
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 ? 6 : 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 = Math.ceil(ctx.measureText(priceText).width) + pricePaddingX * 2;
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 = Math.ceil(ctx.measureText(tickerLabel).width) + labelPaddingX * 2;
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 = Math.ceil(ctx.measureText(priceText).width) + labelPaddingX * 2;
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 ? 4 : 0;
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 ? 6 : 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 = Math.ceil(ctx.measureText(priceText).width) + pricePaddingX * 2;
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 = Math.ceil(ctx.measureText(tickerLabel).width) + labelPaddingX * 2;
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 = Math.ceil(ctx.measureText(priceText).width) + labelPaddingX * 2;
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
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.15",
4
4
  "description": "Lightweight TypeScript charting core",
5
5
  "type": "module",
6
6
  "main": "./dist/hyperprop-charting-library.cjs",