hyperprop-charting-library 0.1.12 → 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 +28 -1
- package/dist/hyperprop-charting-library.cjs +81 -56
- package/dist/hyperprop-charting-library.d.ts +14 -1
- package/dist/hyperprop-charting-library.js +81 -56
- package/dist/index.cjs +81 -56
- package/dist/index.d.cts +14 -1
- package/dist/index.d.ts +14 -1
- package/dist/index.js +81 -56
- package/docs/API.md +14 -0
- package/docs/RECIPES.md +23 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -32,6 +32,32 @@ const data: OhlcDataPoint[] = [
|
|
|
32
32
|
chart.setData(data);
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
+
## Dash Pattern Styling
|
|
36
|
+
|
|
37
|
+
You can control dotted/dashed spacing globally with `dashPatterns`:
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
const chart = createChart(root, {
|
|
41
|
+
dashPatterns: {
|
|
42
|
+
dotted: [2, 1],
|
|
43
|
+
connectorDotted: [2, 2],
|
|
44
|
+
borderDotted: [2, 1]
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
```
|
|
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
|
+
|
|
35
61
|
## Full Documentation
|
|
36
62
|
|
|
37
63
|
- API reference: `docs/API.md`
|
|
@@ -46,8 +72,9 @@ chart.setData(data);
|
|
|
46
72
|
- `chart.setData(data)`
|
|
47
73
|
- `chart.setPriceLines(lines)` / `chart.addPriceLine(line)` / `chart.removePriceLine(id)`
|
|
48
74
|
- `chart.setOrderLines(lines)` / `chart.addOrderLine(line)` / `chart.updateOrderLine(id, patch)` / `chart.removeOrderLine(id)`
|
|
49
|
-
- `chart.onOrderAction(handler)` / `chart.onChartClick(handler)`
|
|
75
|
+
- `chart.onOrderAction(handler)` / `chart.onChartClick(handler)` / `chart.onCrosshairMove(handler)`
|
|
50
76
|
- `chart.setDoubleClickEnabled(enabled)` / `chart.setDoubleClickAction(action)`
|
|
77
|
+
- `chart.zoomInX()` / `chart.zoomOutX()` / `chart.panX(bars)` / `chart.resetViewport()`
|
|
51
78
|
- `chart.resize(width, height)` / `chart.destroy()`
|
|
52
79
|
|
|
53
80
|
## Scope
|
|
@@ -68,6 +68,14 @@ var DEFAULT_WATERMARK_OPTIONS = {
|
|
|
68
68
|
imageTintColor: "",
|
|
69
69
|
imageTintOpacity: 1
|
|
70
70
|
};
|
|
71
|
+
var DEFAULT_DASH_PATTERNS = {
|
|
72
|
+
dotted: [2, 2],
|
|
73
|
+
dashed: [8, 6],
|
|
74
|
+
connectorDotted: [2, 3],
|
|
75
|
+
connectorDashed: [6, 5],
|
|
76
|
+
borderDotted: [2, 2],
|
|
77
|
+
borderDashed: [6, 4]
|
|
78
|
+
};
|
|
71
79
|
var DEFAULT_PRICE_LINE_OPTIONS = {
|
|
72
80
|
visible: true,
|
|
73
81
|
style: "solid",
|
|
@@ -100,6 +108,8 @@ var DEFAULT_ORDER_LINE_OPTIONS = {
|
|
|
100
108
|
actionButtonFontWeight: 500,
|
|
101
109
|
actionButtonBorderColor: "#2563eb",
|
|
102
110
|
actionButtonBorderStyle: "solid",
|
|
111
|
+
actionButtonsInnerGap: 6,
|
|
112
|
+
actionButtonsGroupGap: 8,
|
|
103
113
|
actionButtons: [],
|
|
104
114
|
connectorToPrice: Number.NaN,
|
|
105
115
|
connectorColor: "#2563eb",
|
|
@@ -116,6 +126,8 @@ var DEFAULT_OPTIONS = {
|
|
|
116
126
|
axisColor: "#7f8289",
|
|
117
127
|
axis: DEFAULT_AXIS_OPTIONS,
|
|
118
128
|
priceDecimals: 2,
|
|
129
|
+
stabilizePriceLabels: true,
|
|
130
|
+
priceLabelMinIntegerDigits: 3,
|
|
119
131
|
initialViewport: "latest",
|
|
120
132
|
initialVisibleBars: 60,
|
|
121
133
|
minVisibleBars: 5,
|
|
@@ -147,7 +159,8 @@ var DEFAULT_OPTIONS = {
|
|
|
147
159
|
labelBackgroundColor: "#38bdf8",
|
|
148
160
|
labelTextColor: "#0b1220",
|
|
149
161
|
labelBorderRadius: 3
|
|
150
|
-
}
|
|
162
|
+
},
|
|
163
|
+
dashPatterns: DEFAULT_DASH_PATTERNS
|
|
151
164
|
};
|
|
152
165
|
var BRAND_LOGO_VIEWBOX_WIDTH = 190;
|
|
153
166
|
var BRAND_LOGO_VIEWBOX_HEIGHT = 186;
|
|
@@ -177,6 +190,10 @@ function createChart(element, options = {}) {
|
|
|
177
190
|
tickerLine: {
|
|
178
191
|
...DEFAULT_OPTIONS.tickerLine,
|
|
179
192
|
...options.tickerLine
|
|
193
|
+
},
|
|
194
|
+
dashPatterns: {
|
|
195
|
+
...DEFAULT_DASH_PATTERNS,
|
|
196
|
+
...options.dashPatterns ?? {}
|
|
180
197
|
}
|
|
181
198
|
};
|
|
182
199
|
let width = mergedOptions.width;
|
|
@@ -264,6 +281,23 @@ function createChart(element, options = {}) {
|
|
|
264
281
|
const clamp = (value, min, max) => {
|
|
265
282
|
return Math.min(max, Math.max(min, value));
|
|
266
283
|
};
|
|
284
|
+
const dashPatterns = {
|
|
285
|
+
dotted: mergedOptions.dashPatterns.dotted ?? DEFAULT_DASH_PATTERNS.dotted,
|
|
286
|
+
dashed: mergedOptions.dashPatterns.dashed ?? DEFAULT_DASH_PATTERNS.dashed,
|
|
287
|
+
connectorDotted: mergedOptions.dashPatterns.connectorDotted ?? DEFAULT_DASH_PATTERNS.connectorDotted,
|
|
288
|
+
connectorDashed: mergedOptions.dashPatterns.connectorDashed ?? DEFAULT_DASH_PATTERNS.connectorDashed,
|
|
289
|
+
borderDotted: mergedOptions.dashPatterns.borderDotted ?? DEFAULT_DASH_PATTERNS.borderDotted,
|
|
290
|
+
borderDashed: mergedOptions.dashPatterns.borderDashed ?? DEFAULT_DASH_PATTERNS.borderDashed
|
|
291
|
+
};
|
|
292
|
+
const applyDashPattern = (style, dotted, dashed) => {
|
|
293
|
+
if (style === "dotted") {
|
|
294
|
+
ctx.setLineDash(dotted);
|
|
295
|
+
} else if (style === "dashed") {
|
|
296
|
+
ctx.setLineDash(dashed);
|
|
297
|
+
} else {
|
|
298
|
+
ctx.setLineDash([]);
|
|
299
|
+
}
|
|
300
|
+
};
|
|
267
301
|
const clampXViewport = () => {
|
|
268
302
|
const count = data.length;
|
|
269
303
|
if (count === 0) {
|
|
@@ -333,6 +367,27 @@ function createChart(element, options = {}) {
|
|
|
333
367
|
const decimals = clamp(Math.round(mergedOptions.priceDecimals), 0, 8);
|
|
334
368
|
return price.toFixed(decimals);
|
|
335
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
|
+
};
|
|
336
391
|
const parseData = (nextData) => {
|
|
337
392
|
return nextData.map((point) => ({
|
|
338
393
|
time: new Date(point.t),
|
|
@@ -437,13 +492,7 @@ function createChart(element, options = {}) {
|
|
|
437
492
|
ctx.save();
|
|
438
493
|
ctx.strokeStyle = color;
|
|
439
494
|
ctx.lineWidth = Math.max(1, mergedLine.thickness);
|
|
440
|
-
|
|
441
|
-
ctx.setLineDash([2, 4]);
|
|
442
|
-
} else if (mergedLine.style === "dashed") {
|
|
443
|
-
ctx.setLineDash([8, 6]);
|
|
444
|
-
} else {
|
|
445
|
-
ctx.setLineDash([]);
|
|
446
|
-
}
|
|
495
|
+
applyDashPattern(mergedLine.style, dashPatterns.dotted, dashPatterns.dashed);
|
|
447
496
|
ctx.beginPath();
|
|
448
497
|
ctx.moveTo(crisp(chartLeft), crisp(lineY));
|
|
449
498
|
ctx.lineTo(crisp(chartRight), crisp(lineY));
|
|
@@ -457,7 +506,7 @@ function createChart(element, options = {}) {
|
|
|
457
506
|
ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
|
|
458
507
|
const labelPaddingX = 8;
|
|
459
508
|
const labelHeight = 20;
|
|
460
|
-
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;
|
|
461
510
|
const labelX = chartRight + 4;
|
|
462
511
|
const labelY = clamp(lineY - labelHeight / 2, chartTop, chartBottom - labelHeight);
|
|
463
512
|
ctx.fillStyle = mergedLine.labelBackgroundColor;
|
|
@@ -497,13 +546,7 @@ function createChart(element, options = {}) {
|
|
|
497
546
|
ctx.save();
|
|
498
547
|
ctx.strokeStyle = color;
|
|
499
548
|
ctx.lineWidth = Math.max(1, mergedLine.thickness);
|
|
500
|
-
|
|
501
|
-
ctx.setLineDash([2, 4]);
|
|
502
|
-
} else if (mergedLine.style === "dashed") {
|
|
503
|
-
ctx.setLineDash([8, 6]);
|
|
504
|
-
} else {
|
|
505
|
-
ctx.setLineDash([]);
|
|
506
|
-
}
|
|
549
|
+
applyDashPattern(mergedLine.style, dashPatterns.dotted, dashPatterns.dashed);
|
|
507
550
|
ctx.beginPath();
|
|
508
551
|
ctx.moveTo(crisp(chartLeft), crisp(lineY));
|
|
509
552
|
ctx.lineTo(crisp(chartRight), crisp(lineY));
|
|
@@ -515,13 +558,11 @@ function createChart(element, options = {}) {
|
|
|
515
558
|
ctx.save();
|
|
516
559
|
ctx.strokeStyle = mergedLine.connectorColor ?? color;
|
|
517
560
|
ctx.lineWidth = Math.max(1, mergedLine.connectorThickness);
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
ctx.setLineDash([]);
|
|
524
|
-
}
|
|
561
|
+
applyDashPattern(
|
|
562
|
+
mergedLine.connectorStyle,
|
|
563
|
+
dashPatterns.connectorDotted,
|
|
564
|
+
dashPatterns.connectorDashed
|
|
565
|
+
);
|
|
525
566
|
ctx.beginPath();
|
|
526
567
|
ctx.moveTo(crisp(connectorX), crisp(lineY));
|
|
527
568
|
ctx.lineTo(crisp(connectorX), crisp(connectorY));
|
|
@@ -567,9 +608,9 @@ function createChart(element, options = {}) {
|
|
|
567
608
|
const width2 = Math.max(minWidth, Math.ceil(ctx.measureText(button.text).width) + paddingX * 2);
|
|
568
609
|
return { button, width: width2 };
|
|
569
610
|
});
|
|
570
|
-
const actionButtonInnerGap = actionButtonMetrics.length > 1 ?
|
|
611
|
+
const actionButtonInnerGap = actionButtonMetrics.length > 1 ? Math.max(0, mergedLine.actionButtonsInnerGap) : 0;
|
|
571
612
|
const actionButtonsTotalWidth = actionButtonMetrics.reduce((sum, metric) => sum + metric.width, 0) + Math.max(0, actionButtonMetrics.length - 1) * actionButtonInnerGap;
|
|
572
|
-
const actionButtonsGap = actionButtonMetrics.length > 0 ?
|
|
613
|
+
const actionButtonsGap = actionButtonMetrics.length > 0 ? Math.max(0, mergedLine.actionButtonsGroupGap) : 0;
|
|
573
614
|
const segmentPaddingX = 8;
|
|
574
615
|
const labelHeight = 22;
|
|
575
616
|
const qtyWidth = qtyText ? Math.ceil(ctx.measureText(qtyText).width) + segmentPaddingX * 2 : 0;
|
|
@@ -624,13 +665,11 @@ function createChart(element, options = {}) {
|
|
|
624
665
|
ctx.save();
|
|
625
666
|
ctx.strokeStyle = actionBorderColor;
|
|
626
667
|
ctx.lineWidth = 1;
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
ctx.setLineDash([]);
|
|
633
|
-
}
|
|
668
|
+
applyDashPattern(
|
|
669
|
+
actionBorderStyle,
|
|
670
|
+
dashPatterns.borderDotted,
|
|
671
|
+
dashPatterns.borderDashed
|
|
672
|
+
);
|
|
634
673
|
strokeRoundedRect(Math.round(actionX), Math.round(actionY), actionW, actionH, actionRadius);
|
|
635
674
|
ctx.restore();
|
|
636
675
|
const baseFont = ctx.font;
|
|
@@ -695,7 +734,7 @@ function createChart(element, options = {}) {
|
|
|
695
734
|
}
|
|
696
735
|
const priceText = formatPrice(renderPrice);
|
|
697
736
|
const pricePaddingX = 8;
|
|
698
|
-
const measuredPriceWidth =
|
|
737
|
+
const measuredPriceWidth = getPriceLabelWidth(priceText, pricePaddingX);
|
|
699
738
|
const priceWidth = mergedLine.id === void 0 ? measuredPriceWidth : Math.max(measuredPriceWidth, orderPriceTagWidthById.get(mergedLine.id) ?? 0);
|
|
700
739
|
if (mergedLine.id) {
|
|
701
740
|
orderPriceTagWidthById.set(mergedLine.id, priceWidth);
|
|
@@ -969,13 +1008,7 @@ function createChart(element, options = {}) {
|
|
|
969
1008
|
ctx.save();
|
|
970
1009
|
ctx.strokeStyle = crosshair.color;
|
|
971
1010
|
ctx.lineWidth = Math.max(1, crosshair.width);
|
|
972
|
-
|
|
973
|
-
ctx.setLineDash([2, 4]);
|
|
974
|
-
} else if (crosshair.style === "dashed") {
|
|
975
|
-
ctx.setLineDash([8, 6]);
|
|
976
|
-
} else {
|
|
977
|
-
ctx.setLineDash([]);
|
|
978
|
-
}
|
|
1011
|
+
applyDashPattern(crosshair.style, dashPatterns.dotted, dashPatterns.dashed);
|
|
979
1012
|
if (crosshair.showVertical) {
|
|
980
1013
|
ctx.beginPath();
|
|
981
1014
|
ctx.moveTo(crisp(cx), crisp(chartTop));
|
|
@@ -1017,13 +1050,7 @@ function createChart(element, options = {}) {
|
|
|
1017
1050
|
ctx.save();
|
|
1018
1051
|
ctx.strokeStyle = tickerColor;
|
|
1019
1052
|
ctx.lineWidth = tickerThickness;
|
|
1020
|
-
|
|
1021
|
-
ctx.setLineDash([2, 4]);
|
|
1022
|
-
} else if (tickerStyle === "dashed") {
|
|
1023
|
-
ctx.setLineDash([8, 6]);
|
|
1024
|
-
} else {
|
|
1025
|
-
ctx.setLineDash([]);
|
|
1026
|
-
}
|
|
1053
|
+
applyDashPattern(tickerStyle, dashPatterns.dotted, dashPatterns.dashed);
|
|
1027
1054
|
ctx.beginPath();
|
|
1028
1055
|
ctx.moveTo(crisp(chartLeft), crisp(lineY));
|
|
1029
1056
|
ctx.lineTo(crisp(chartRight), crisp(lineY));
|
|
@@ -1034,7 +1061,7 @@ function createChart(element, options = {}) {
|
|
|
1034
1061
|
ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
|
|
1035
1062
|
const labelPaddingX = 8;
|
|
1036
1063
|
const labelHeight = 20;
|
|
1037
|
-
const labelWidth =
|
|
1064
|
+
const labelWidth = getPriceLabelWidth(tickerLabel, labelPaddingX);
|
|
1038
1065
|
const labelX = chartRight + 4;
|
|
1039
1066
|
const labelY = clamp(lineY - labelHeight / 2, chartTop, chartBottom - labelHeight);
|
|
1040
1067
|
const labelRadius = Math.max(0, ticker.labelBorderRadius ?? 0);
|
|
@@ -1096,20 +1123,18 @@ function createChart(element, options = {}) {
|
|
|
1096
1123
|
ctx.save();
|
|
1097
1124
|
ctx.strokeStyle = labelBorderColor;
|
|
1098
1125
|
ctx.lineWidth = labelBorderWidth;
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
ctx.setLineDash([]);
|
|
1105
|
-
}
|
|
1126
|
+
applyDashPattern(
|
|
1127
|
+
labelBorderStyle,
|
|
1128
|
+
dashPatterns.borderDotted,
|
|
1129
|
+
dashPatterns.borderDashed
|
|
1130
|
+
);
|
|
1106
1131
|
strokeRoundedRect(Math.round(x), Math.round(y), widthValue, labelHeight, labelRadius);
|
|
1107
1132
|
ctx.restore();
|
|
1108
1133
|
};
|
|
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;
|
|
@@ -29,6 +31,15 @@ interface ChartOptions {
|
|
|
29
31
|
priceLines?: PriceLineOptions[];
|
|
30
32
|
orderLines?: OrderLineOptions[];
|
|
31
33
|
tickerLine?: TickerLineOptions;
|
|
34
|
+
dashPatterns?: Partial<DashPatternOptions>;
|
|
35
|
+
}
|
|
36
|
+
interface DashPatternOptions {
|
|
37
|
+
dotted: [number, number];
|
|
38
|
+
dashed: [number, number];
|
|
39
|
+
connectorDotted: [number, number];
|
|
40
|
+
connectorDashed: [number, number];
|
|
41
|
+
borderDotted: [number, number];
|
|
42
|
+
borderDashed: [number, number];
|
|
32
43
|
}
|
|
33
44
|
interface AxisOptions {
|
|
34
45
|
lineColor?: string;
|
|
@@ -119,6 +130,8 @@ interface OrderLineOptions {
|
|
|
119
130
|
actionButtonFontWeight?: number | string;
|
|
120
131
|
actionButtonBorderColor?: string;
|
|
121
132
|
actionButtonBorderStyle?: "solid" | "dotted" | "dashed";
|
|
133
|
+
actionButtonsInnerGap?: number;
|
|
134
|
+
actionButtonsGroupGap?: number;
|
|
122
135
|
actionButtons?: OrderActionButton[];
|
|
123
136
|
connectorToPrice?: number;
|
|
124
137
|
connectorColor?: string;
|
|
@@ -208,4 +221,4 @@ interface OhlcDataPoint {
|
|
|
208
221
|
}
|
|
209
222
|
declare function createChart(element: HTMLElement, options?: ChartOptions): ChartInstance;
|
|
210
223
|
|
|
211
|
-
export { type AxisOptions, type ChartClickEvent, type ChartInstance, type ChartOptions, type CrosshairMoveEvent, type CrosshairOptions, type GridOptions, type OhlcDataPoint, type OrderActionButton, type OrderActionEvent, type OrderLineOptions, type PriceLineOptions, type TickerLineOptions, type WatermarkOptions, createChart };
|
|
224
|
+
export { type AxisOptions, type ChartClickEvent, type ChartInstance, type ChartOptions, type CrosshairMoveEvent, type CrosshairOptions, type DashPatternOptions, type GridOptions, type OhlcDataPoint, type OrderActionButton, type OrderActionEvent, type OrderLineOptions, type PriceLineOptions, type TickerLineOptions, type WatermarkOptions, createChart };
|
|
@@ -44,6 +44,14 @@ var DEFAULT_WATERMARK_OPTIONS = {
|
|
|
44
44
|
imageTintColor: "",
|
|
45
45
|
imageTintOpacity: 1
|
|
46
46
|
};
|
|
47
|
+
var DEFAULT_DASH_PATTERNS = {
|
|
48
|
+
dotted: [2, 2],
|
|
49
|
+
dashed: [8, 6],
|
|
50
|
+
connectorDotted: [2, 3],
|
|
51
|
+
connectorDashed: [6, 5],
|
|
52
|
+
borderDotted: [2, 2],
|
|
53
|
+
borderDashed: [6, 4]
|
|
54
|
+
};
|
|
47
55
|
var DEFAULT_PRICE_LINE_OPTIONS = {
|
|
48
56
|
visible: true,
|
|
49
57
|
style: "solid",
|
|
@@ -76,6 +84,8 @@ var DEFAULT_ORDER_LINE_OPTIONS = {
|
|
|
76
84
|
actionButtonFontWeight: 500,
|
|
77
85
|
actionButtonBorderColor: "#2563eb",
|
|
78
86
|
actionButtonBorderStyle: "solid",
|
|
87
|
+
actionButtonsInnerGap: 6,
|
|
88
|
+
actionButtonsGroupGap: 8,
|
|
79
89
|
actionButtons: [],
|
|
80
90
|
connectorToPrice: Number.NaN,
|
|
81
91
|
connectorColor: "#2563eb",
|
|
@@ -92,6 +102,8 @@ var DEFAULT_OPTIONS = {
|
|
|
92
102
|
axisColor: "#7f8289",
|
|
93
103
|
axis: DEFAULT_AXIS_OPTIONS,
|
|
94
104
|
priceDecimals: 2,
|
|
105
|
+
stabilizePriceLabels: true,
|
|
106
|
+
priceLabelMinIntegerDigits: 3,
|
|
95
107
|
initialViewport: "latest",
|
|
96
108
|
initialVisibleBars: 60,
|
|
97
109
|
minVisibleBars: 5,
|
|
@@ -123,7 +135,8 @@ var DEFAULT_OPTIONS = {
|
|
|
123
135
|
labelBackgroundColor: "#38bdf8",
|
|
124
136
|
labelTextColor: "#0b1220",
|
|
125
137
|
labelBorderRadius: 3
|
|
126
|
-
}
|
|
138
|
+
},
|
|
139
|
+
dashPatterns: DEFAULT_DASH_PATTERNS
|
|
127
140
|
};
|
|
128
141
|
var BRAND_LOGO_VIEWBOX_WIDTH = 190;
|
|
129
142
|
var BRAND_LOGO_VIEWBOX_HEIGHT = 186;
|
|
@@ -153,6 +166,10 @@ function createChart(element, options = {}) {
|
|
|
153
166
|
tickerLine: {
|
|
154
167
|
...DEFAULT_OPTIONS.tickerLine,
|
|
155
168
|
...options.tickerLine
|
|
169
|
+
},
|
|
170
|
+
dashPatterns: {
|
|
171
|
+
...DEFAULT_DASH_PATTERNS,
|
|
172
|
+
...options.dashPatterns ?? {}
|
|
156
173
|
}
|
|
157
174
|
};
|
|
158
175
|
let width = mergedOptions.width;
|
|
@@ -240,6 +257,23 @@ function createChart(element, options = {}) {
|
|
|
240
257
|
const clamp = (value, min, max) => {
|
|
241
258
|
return Math.min(max, Math.max(min, value));
|
|
242
259
|
};
|
|
260
|
+
const dashPatterns = {
|
|
261
|
+
dotted: mergedOptions.dashPatterns.dotted ?? DEFAULT_DASH_PATTERNS.dotted,
|
|
262
|
+
dashed: mergedOptions.dashPatterns.dashed ?? DEFAULT_DASH_PATTERNS.dashed,
|
|
263
|
+
connectorDotted: mergedOptions.dashPatterns.connectorDotted ?? DEFAULT_DASH_PATTERNS.connectorDotted,
|
|
264
|
+
connectorDashed: mergedOptions.dashPatterns.connectorDashed ?? DEFAULT_DASH_PATTERNS.connectorDashed,
|
|
265
|
+
borderDotted: mergedOptions.dashPatterns.borderDotted ?? DEFAULT_DASH_PATTERNS.borderDotted,
|
|
266
|
+
borderDashed: mergedOptions.dashPatterns.borderDashed ?? DEFAULT_DASH_PATTERNS.borderDashed
|
|
267
|
+
};
|
|
268
|
+
const applyDashPattern = (style, dotted, dashed) => {
|
|
269
|
+
if (style === "dotted") {
|
|
270
|
+
ctx.setLineDash(dotted);
|
|
271
|
+
} else if (style === "dashed") {
|
|
272
|
+
ctx.setLineDash(dashed);
|
|
273
|
+
} else {
|
|
274
|
+
ctx.setLineDash([]);
|
|
275
|
+
}
|
|
276
|
+
};
|
|
243
277
|
const clampXViewport = () => {
|
|
244
278
|
const count = data.length;
|
|
245
279
|
if (count === 0) {
|
|
@@ -309,6 +343,27 @@ function createChart(element, options = {}) {
|
|
|
309
343
|
const decimals = clamp(Math.round(mergedOptions.priceDecimals), 0, 8);
|
|
310
344
|
return price.toFixed(decimals);
|
|
311
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
|
+
};
|
|
312
367
|
const parseData = (nextData) => {
|
|
313
368
|
return nextData.map((point) => ({
|
|
314
369
|
time: new Date(point.t),
|
|
@@ -413,13 +468,7 @@ function createChart(element, options = {}) {
|
|
|
413
468
|
ctx.save();
|
|
414
469
|
ctx.strokeStyle = color;
|
|
415
470
|
ctx.lineWidth = Math.max(1, mergedLine.thickness);
|
|
416
|
-
|
|
417
|
-
ctx.setLineDash([2, 4]);
|
|
418
|
-
} else if (mergedLine.style === "dashed") {
|
|
419
|
-
ctx.setLineDash([8, 6]);
|
|
420
|
-
} else {
|
|
421
|
-
ctx.setLineDash([]);
|
|
422
|
-
}
|
|
471
|
+
applyDashPattern(mergedLine.style, dashPatterns.dotted, dashPatterns.dashed);
|
|
423
472
|
ctx.beginPath();
|
|
424
473
|
ctx.moveTo(crisp(chartLeft), crisp(lineY));
|
|
425
474
|
ctx.lineTo(crisp(chartRight), crisp(lineY));
|
|
@@ -433,7 +482,7 @@ function createChart(element, options = {}) {
|
|
|
433
482
|
ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
|
|
434
483
|
const labelPaddingX = 8;
|
|
435
484
|
const labelHeight = 20;
|
|
436
|
-
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;
|
|
437
486
|
const labelX = chartRight + 4;
|
|
438
487
|
const labelY = clamp(lineY - labelHeight / 2, chartTop, chartBottom - labelHeight);
|
|
439
488
|
ctx.fillStyle = mergedLine.labelBackgroundColor;
|
|
@@ -473,13 +522,7 @@ function createChart(element, options = {}) {
|
|
|
473
522
|
ctx.save();
|
|
474
523
|
ctx.strokeStyle = color;
|
|
475
524
|
ctx.lineWidth = Math.max(1, mergedLine.thickness);
|
|
476
|
-
|
|
477
|
-
ctx.setLineDash([2, 4]);
|
|
478
|
-
} else if (mergedLine.style === "dashed") {
|
|
479
|
-
ctx.setLineDash([8, 6]);
|
|
480
|
-
} else {
|
|
481
|
-
ctx.setLineDash([]);
|
|
482
|
-
}
|
|
525
|
+
applyDashPattern(mergedLine.style, dashPatterns.dotted, dashPatterns.dashed);
|
|
483
526
|
ctx.beginPath();
|
|
484
527
|
ctx.moveTo(crisp(chartLeft), crisp(lineY));
|
|
485
528
|
ctx.lineTo(crisp(chartRight), crisp(lineY));
|
|
@@ -491,13 +534,11 @@ function createChart(element, options = {}) {
|
|
|
491
534
|
ctx.save();
|
|
492
535
|
ctx.strokeStyle = mergedLine.connectorColor ?? color;
|
|
493
536
|
ctx.lineWidth = Math.max(1, mergedLine.connectorThickness);
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
ctx.setLineDash([]);
|
|
500
|
-
}
|
|
537
|
+
applyDashPattern(
|
|
538
|
+
mergedLine.connectorStyle,
|
|
539
|
+
dashPatterns.connectorDotted,
|
|
540
|
+
dashPatterns.connectorDashed
|
|
541
|
+
);
|
|
501
542
|
ctx.beginPath();
|
|
502
543
|
ctx.moveTo(crisp(connectorX), crisp(lineY));
|
|
503
544
|
ctx.lineTo(crisp(connectorX), crisp(connectorY));
|
|
@@ -543,9 +584,9 @@ function createChart(element, options = {}) {
|
|
|
543
584
|
const width2 = Math.max(minWidth, Math.ceil(ctx.measureText(button.text).width) + paddingX * 2);
|
|
544
585
|
return { button, width: width2 };
|
|
545
586
|
});
|
|
546
|
-
const actionButtonInnerGap = actionButtonMetrics.length > 1 ?
|
|
587
|
+
const actionButtonInnerGap = actionButtonMetrics.length > 1 ? Math.max(0, mergedLine.actionButtonsInnerGap) : 0;
|
|
547
588
|
const actionButtonsTotalWidth = actionButtonMetrics.reduce((sum, metric) => sum + metric.width, 0) + Math.max(0, actionButtonMetrics.length - 1) * actionButtonInnerGap;
|
|
548
|
-
const actionButtonsGap = actionButtonMetrics.length > 0 ?
|
|
589
|
+
const actionButtonsGap = actionButtonMetrics.length > 0 ? Math.max(0, mergedLine.actionButtonsGroupGap) : 0;
|
|
549
590
|
const segmentPaddingX = 8;
|
|
550
591
|
const labelHeight = 22;
|
|
551
592
|
const qtyWidth = qtyText ? Math.ceil(ctx.measureText(qtyText).width) + segmentPaddingX * 2 : 0;
|
|
@@ -600,13 +641,11 @@ function createChart(element, options = {}) {
|
|
|
600
641
|
ctx.save();
|
|
601
642
|
ctx.strokeStyle = actionBorderColor;
|
|
602
643
|
ctx.lineWidth = 1;
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
ctx.setLineDash([]);
|
|
609
|
-
}
|
|
644
|
+
applyDashPattern(
|
|
645
|
+
actionBorderStyle,
|
|
646
|
+
dashPatterns.borderDotted,
|
|
647
|
+
dashPatterns.borderDashed
|
|
648
|
+
);
|
|
610
649
|
strokeRoundedRect(Math.round(actionX), Math.round(actionY), actionW, actionH, actionRadius);
|
|
611
650
|
ctx.restore();
|
|
612
651
|
const baseFont = ctx.font;
|
|
@@ -671,7 +710,7 @@ function createChart(element, options = {}) {
|
|
|
671
710
|
}
|
|
672
711
|
const priceText = formatPrice(renderPrice);
|
|
673
712
|
const pricePaddingX = 8;
|
|
674
|
-
const measuredPriceWidth =
|
|
713
|
+
const measuredPriceWidth = getPriceLabelWidth(priceText, pricePaddingX);
|
|
675
714
|
const priceWidth = mergedLine.id === void 0 ? measuredPriceWidth : Math.max(measuredPriceWidth, orderPriceTagWidthById.get(mergedLine.id) ?? 0);
|
|
676
715
|
if (mergedLine.id) {
|
|
677
716
|
orderPriceTagWidthById.set(mergedLine.id, priceWidth);
|
|
@@ -945,13 +984,7 @@ function createChart(element, options = {}) {
|
|
|
945
984
|
ctx.save();
|
|
946
985
|
ctx.strokeStyle = crosshair.color;
|
|
947
986
|
ctx.lineWidth = Math.max(1, crosshair.width);
|
|
948
|
-
|
|
949
|
-
ctx.setLineDash([2, 4]);
|
|
950
|
-
} else if (crosshair.style === "dashed") {
|
|
951
|
-
ctx.setLineDash([8, 6]);
|
|
952
|
-
} else {
|
|
953
|
-
ctx.setLineDash([]);
|
|
954
|
-
}
|
|
987
|
+
applyDashPattern(crosshair.style, dashPatterns.dotted, dashPatterns.dashed);
|
|
955
988
|
if (crosshair.showVertical) {
|
|
956
989
|
ctx.beginPath();
|
|
957
990
|
ctx.moveTo(crisp(cx), crisp(chartTop));
|
|
@@ -993,13 +1026,7 @@ function createChart(element, options = {}) {
|
|
|
993
1026
|
ctx.save();
|
|
994
1027
|
ctx.strokeStyle = tickerColor;
|
|
995
1028
|
ctx.lineWidth = tickerThickness;
|
|
996
|
-
|
|
997
|
-
ctx.setLineDash([2, 4]);
|
|
998
|
-
} else if (tickerStyle === "dashed") {
|
|
999
|
-
ctx.setLineDash([8, 6]);
|
|
1000
|
-
} else {
|
|
1001
|
-
ctx.setLineDash([]);
|
|
1002
|
-
}
|
|
1029
|
+
applyDashPattern(tickerStyle, dashPatterns.dotted, dashPatterns.dashed);
|
|
1003
1030
|
ctx.beginPath();
|
|
1004
1031
|
ctx.moveTo(crisp(chartLeft), crisp(lineY));
|
|
1005
1032
|
ctx.lineTo(crisp(chartRight), crisp(lineY));
|
|
@@ -1010,7 +1037,7 @@ function createChart(element, options = {}) {
|
|
|
1010
1037
|
ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
|
|
1011
1038
|
const labelPaddingX = 8;
|
|
1012
1039
|
const labelHeight = 20;
|
|
1013
|
-
const labelWidth =
|
|
1040
|
+
const labelWidth = getPriceLabelWidth(tickerLabel, labelPaddingX);
|
|
1014
1041
|
const labelX = chartRight + 4;
|
|
1015
1042
|
const labelY = clamp(lineY - labelHeight / 2, chartTop, chartBottom - labelHeight);
|
|
1016
1043
|
const labelRadius = Math.max(0, ticker.labelBorderRadius ?? 0);
|
|
@@ -1072,20 +1099,18 @@ function createChart(element, options = {}) {
|
|
|
1072
1099
|
ctx.save();
|
|
1073
1100
|
ctx.strokeStyle = labelBorderColor;
|
|
1074
1101
|
ctx.lineWidth = labelBorderWidth;
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
ctx.setLineDash([]);
|
|
1081
|
-
}
|
|
1102
|
+
applyDashPattern(
|
|
1103
|
+
labelBorderStyle,
|
|
1104
|
+
dashPatterns.borderDotted,
|
|
1105
|
+
dashPatterns.borderDashed
|
|
1106
|
+
);
|
|
1082
1107
|
strokeRoundedRect(Math.round(x), Math.round(y), widthValue, labelHeight, labelRadius);
|
|
1083
1108
|
ctx.restore();
|
|
1084
1109
|
};
|
|
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
|
@@ -68,6 +68,14 @@ var DEFAULT_WATERMARK_OPTIONS = {
|
|
|
68
68
|
imageTintColor: "",
|
|
69
69
|
imageTintOpacity: 1
|
|
70
70
|
};
|
|
71
|
+
var DEFAULT_DASH_PATTERNS = {
|
|
72
|
+
dotted: [2, 2],
|
|
73
|
+
dashed: [8, 6],
|
|
74
|
+
connectorDotted: [2, 3],
|
|
75
|
+
connectorDashed: [6, 5],
|
|
76
|
+
borderDotted: [2, 2],
|
|
77
|
+
borderDashed: [6, 4]
|
|
78
|
+
};
|
|
71
79
|
var DEFAULT_PRICE_LINE_OPTIONS = {
|
|
72
80
|
visible: true,
|
|
73
81
|
style: "solid",
|
|
@@ -100,6 +108,8 @@ var DEFAULT_ORDER_LINE_OPTIONS = {
|
|
|
100
108
|
actionButtonFontWeight: 500,
|
|
101
109
|
actionButtonBorderColor: "#2563eb",
|
|
102
110
|
actionButtonBorderStyle: "solid",
|
|
111
|
+
actionButtonsInnerGap: 6,
|
|
112
|
+
actionButtonsGroupGap: 8,
|
|
103
113
|
actionButtons: [],
|
|
104
114
|
connectorToPrice: Number.NaN,
|
|
105
115
|
connectorColor: "#2563eb",
|
|
@@ -116,6 +126,8 @@ var DEFAULT_OPTIONS = {
|
|
|
116
126
|
axisColor: "#7f8289",
|
|
117
127
|
axis: DEFAULT_AXIS_OPTIONS,
|
|
118
128
|
priceDecimals: 2,
|
|
129
|
+
stabilizePriceLabels: true,
|
|
130
|
+
priceLabelMinIntegerDigits: 3,
|
|
119
131
|
initialViewport: "latest",
|
|
120
132
|
initialVisibleBars: 60,
|
|
121
133
|
minVisibleBars: 5,
|
|
@@ -147,7 +159,8 @@ var DEFAULT_OPTIONS = {
|
|
|
147
159
|
labelBackgroundColor: "#38bdf8",
|
|
148
160
|
labelTextColor: "#0b1220",
|
|
149
161
|
labelBorderRadius: 3
|
|
150
|
-
}
|
|
162
|
+
},
|
|
163
|
+
dashPatterns: DEFAULT_DASH_PATTERNS
|
|
151
164
|
};
|
|
152
165
|
var BRAND_LOGO_VIEWBOX_WIDTH = 190;
|
|
153
166
|
var BRAND_LOGO_VIEWBOX_HEIGHT = 186;
|
|
@@ -177,6 +190,10 @@ function createChart(element, options = {}) {
|
|
|
177
190
|
tickerLine: {
|
|
178
191
|
...DEFAULT_OPTIONS.tickerLine,
|
|
179
192
|
...options.tickerLine
|
|
193
|
+
},
|
|
194
|
+
dashPatterns: {
|
|
195
|
+
...DEFAULT_DASH_PATTERNS,
|
|
196
|
+
...options.dashPatterns ?? {}
|
|
180
197
|
}
|
|
181
198
|
};
|
|
182
199
|
let width = mergedOptions.width;
|
|
@@ -264,6 +281,23 @@ function createChart(element, options = {}) {
|
|
|
264
281
|
const clamp = (value, min, max) => {
|
|
265
282
|
return Math.min(max, Math.max(min, value));
|
|
266
283
|
};
|
|
284
|
+
const dashPatterns = {
|
|
285
|
+
dotted: mergedOptions.dashPatterns.dotted ?? DEFAULT_DASH_PATTERNS.dotted,
|
|
286
|
+
dashed: mergedOptions.dashPatterns.dashed ?? DEFAULT_DASH_PATTERNS.dashed,
|
|
287
|
+
connectorDotted: mergedOptions.dashPatterns.connectorDotted ?? DEFAULT_DASH_PATTERNS.connectorDotted,
|
|
288
|
+
connectorDashed: mergedOptions.dashPatterns.connectorDashed ?? DEFAULT_DASH_PATTERNS.connectorDashed,
|
|
289
|
+
borderDotted: mergedOptions.dashPatterns.borderDotted ?? DEFAULT_DASH_PATTERNS.borderDotted,
|
|
290
|
+
borderDashed: mergedOptions.dashPatterns.borderDashed ?? DEFAULT_DASH_PATTERNS.borderDashed
|
|
291
|
+
};
|
|
292
|
+
const applyDashPattern = (style, dotted, dashed) => {
|
|
293
|
+
if (style === "dotted") {
|
|
294
|
+
ctx.setLineDash(dotted);
|
|
295
|
+
} else if (style === "dashed") {
|
|
296
|
+
ctx.setLineDash(dashed);
|
|
297
|
+
} else {
|
|
298
|
+
ctx.setLineDash([]);
|
|
299
|
+
}
|
|
300
|
+
};
|
|
267
301
|
const clampXViewport = () => {
|
|
268
302
|
const count = data.length;
|
|
269
303
|
if (count === 0) {
|
|
@@ -333,6 +367,27 @@ function createChart(element, options = {}) {
|
|
|
333
367
|
const decimals = clamp(Math.round(mergedOptions.priceDecimals), 0, 8);
|
|
334
368
|
return price.toFixed(decimals);
|
|
335
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
|
+
};
|
|
336
391
|
const parseData = (nextData) => {
|
|
337
392
|
return nextData.map((point) => ({
|
|
338
393
|
time: new Date(point.t),
|
|
@@ -437,13 +492,7 @@ function createChart(element, options = {}) {
|
|
|
437
492
|
ctx.save();
|
|
438
493
|
ctx.strokeStyle = color;
|
|
439
494
|
ctx.lineWidth = Math.max(1, mergedLine.thickness);
|
|
440
|
-
|
|
441
|
-
ctx.setLineDash([2, 4]);
|
|
442
|
-
} else if (mergedLine.style === "dashed") {
|
|
443
|
-
ctx.setLineDash([8, 6]);
|
|
444
|
-
} else {
|
|
445
|
-
ctx.setLineDash([]);
|
|
446
|
-
}
|
|
495
|
+
applyDashPattern(mergedLine.style, dashPatterns.dotted, dashPatterns.dashed);
|
|
447
496
|
ctx.beginPath();
|
|
448
497
|
ctx.moveTo(crisp(chartLeft), crisp(lineY));
|
|
449
498
|
ctx.lineTo(crisp(chartRight), crisp(lineY));
|
|
@@ -457,7 +506,7 @@ function createChart(element, options = {}) {
|
|
|
457
506
|
ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
|
|
458
507
|
const labelPaddingX = 8;
|
|
459
508
|
const labelHeight = 20;
|
|
460
|
-
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;
|
|
461
510
|
const labelX = chartRight + 4;
|
|
462
511
|
const labelY = clamp(lineY - labelHeight / 2, chartTop, chartBottom - labelHeight);
|
|
463
512
|
ctx.fillStyle = mergedLine.labelBackgroundColor;
|
|
@@ -497,13 +546,7 @@ function createChart(element, options = {}) {
|
|
|
497
546
|
ctx.save();
|
|
498
547
|
ctx.strokeStyle = color;
|
|
499
548
|
ctx.lineWidth = Math.max(1, mergedLine.thickness);
|
|
500
|
-
|
|
501
|
-
ctx.setLineDash([2, 4]);
|
|
502
|
-
} else if (mergedLine.style === "dashed") {
|
|
503
|
-
ctx.setLineDash([8, 6]);
|
|
504
|
-
} else {
|
|
505
|
-
ctx.setLineDash([]);
|
|
506
|
-
}
|
|
549
|
+
applyDashPattern(mergedLine.style, dashPatterns.dotted, dashPatterns.dashed);
|
|
507
550
|
ctx.beginPath();
|
|
508
551
|
ctx.moveTo(crisp(chartLeft), crisp(lineY));
|
|
509
552
|
ctx.lineTo(crisp(chartRight), crisp(lineY));
|
|
@@ -515,13 +558,11 @@ function createChart(element, options = {}) {
|
|
|
515
558
|
ctx.save();
|
|
516
559
|
ctx.strokeStyle = mergedLine.connectorColor ?? color;
|
|
517
560
|
ctx.lineWidth = Math.max(1, mergedLine.connectorThickness);
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
ctx.setLineDash([]);
|
|
524
|
-
}
|
|
561
|
+
applyDashPattern(
|
|
562
|
+
mergedLine.connectorStyle,
|
|
563
|
+
dashPatterns.connectorDotted,
|
|
564
|
+
dashPatterns.connectorDashed
|
|
565
|
+
);
|
|
525
566
|
ctx.beginPath();
|
|
526
567
|
ctx.moveTo(crisp(connectorX), crisp(lineY));
|
|
527
568
|
ctx.lineTo(crisp(connectorX), crisp(connectorY));
|
|
@@ -567,9 +608,9 @@ function createChart(element, options = {}) {
|
|
|
567
608
|
const width2 = Math.max(minWidth, Math.ceil(ctx.measureText(button.text).width) + paddingX * 2);
|
|
568
609
|
return { button, width: width2 };
|
|
569
610
|
});
|
|
570
|
-
const actionButtonInnerGap = actionButtonMetrics.length > 1 ?
|
|
611
|
+
const actionButtonInnerGap = actionButtonMetrics.length > 1 ? Math.max(0, mergedLine.actionButtonsInnerGap) : 0;
|
|
571
612
|
const actionButtonsTotalWidth = actionButtonMetrics.reduce((sum, metric) => sum + metric.width, 0) + Math.max(0, actionButtonMetrics.length - 1) * actionButtonInnerGap;
|
|
572
|
-
const actionButtonsGap = actionButtonMetrics.length > 0 ?
|
|
613
|
+
const actionButtonsGap = actionButtonMetrics.length > 0 ? Math.max(0, mergedLine.actionButtonsGroupGap) : 0;
|
|
573
614
|
const segmentPaddingX = 8;
|
|
574
615
|
const labelHeight = 22;
|
|
575
616
|
const qtyWidth = qtyText ? Math.ceil(ctx.measureText(qtyText).width) + segmentPaddingX * 2 : 0;
|
|
@@ -624,13 +665,11 @@ function createChart(element, options = {}) {
|
|
|
624
665
|
ctx.save();
|
|
625
666
|
ctx.strokeStyle = actionBorderColor;
|
|
626
667
|
ctx.lineWidth = 1;
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
ctx.setLineDash([]);
|
|
633
|
-
}
|
|
668
|
+
applyDashPattern(
|
|
669
|
+
actionBorderStyle,
|
|
670
|
+
dashPatterns.borderDotted,
|
|
671
|
+
dashPatterns.borderDashed
|
|
672
|
+
);
|
|
634
673
|
strokeRoundedRect(Math.round(actionX), Math.round(actionY), actionW, actionH, actionRadius);
|
|
635
674
|
ctx.restore();
|
|
636
675
|
const baseFont = ctx.font;
|
|
@@ -695,7 +734,7 @@ function createChart(element, options = {}) {
|
|
|
695
734
|
}
|
|
696
735
|
const priceText = formatPrice(renderPrice);
|
|
697
736
|
const pricePaddingX = 8;
|
|
698
|
-
const measuredPriceWidth =
|
|
737
|
+
const measuredPriceWidth = getPriceLabelWidth(priceText, pricePaddingX);
|
|
699
738
|
const priceWidth = mergedLine.id === void 0 ? measuredPriceWidth : Math.max(measuredPriceWidth, orderPriceTagWidthById.get(mergedLine.id) ?? 0);
|
|
700
739
|
if (mergedLine.id) {
|
|
701
740
|
orderPriceTagWidthById.set(mergedLine.id, priceWidth);
|
|
@@ -969,13 +1008,7 @@ function createChart(element, options = {}) {
|
|
|
969
1008
|
ctx.save();
|
|
970
1009
|
ctx.strokeStyle = crosshair.color;
|
|
971
1010
|
ctx.lineWidth = Math.max(1, crosshair.width);
|
|
972
|
-
|
|
973
|
-
ctx.setLineDash([2, 4]);
|
|
974
|
-
} else if (crosshair.style === "dashed") {
|
|
975
|
-
ctx.setLineDash([8, 6]);
|
|
976
|
-
} else {
|
|
977
|
-
ctx.setLineDash([]);
|
|
978
|
-
}
|
|
1011
|
+
applyDashPattern(crosshair.style, dashPatterns.dotted, dashPatterns.dashed);
|
|
979
1012
|
if (crosshair.showVertical) {
|
|
980
1013
|
ctx.beginPath();
|
|
981
1014
|
ctx.moveTo(crisp(cx), crisp(chartTop));
|
|
@@ -1017,13 +1050,7 @@ function createChart(element, options = {}) {
|
|
|
1017
1050
|
ctx.save();
|
|
1018
1051
|
ctx.strokeStyle = tickerColor;
|
|
1019
1052
|
ctx.lineWidth = tickerThickness;
|
|
1020
|
-
|
|
1021
|
-
ctx.setLineDash([2, 4]);
|
|
1022
|
-
} else if (tickerStyle === "dashed") {
|
|
1023
|
-
ctx.setLineDash([8, 6]);
|
|
1024
|
-
} else {
|
|
1025
|
-
ctx.setLineDash([]);
|
|
1026
|
-
}
|
|
1053
|
+
applyDashPattern(tickerStyle, dashPatterns.dotted, dashPatterns.dashed);
|
|
1027
1054
|
ctx.beginPath();
|
|
1028
1055
|
ctx.moveTo(crisp(chartLeft), crisp(lineY));
|
|
1029
1056
|
ctx.lineTo(crisp(chartRight), crisp(lineY));
|
|
@@ -1034,7 +1061,7 @@ function createChart(element, options = {}) {
|
|
|
1034
1061
|
ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
|
|
1035
1062
|
const labelPaddingX = 8;
|
|
1036
1063
|
const labelHeight = 20;
|
|
1037
|
-
const labelWidth =
|
|
1064
|
+
const labelWidth = getPriceLabelWidth(tickerLabel, labelPaddingX);
|
|
1038
1065
|
const labelX = chartRight + 4;
|
|
1039
1066
|
const labelY = clamp(lineY - labelHeight / 2, chartTop, chartBottom - labelHeight);
|
|
1040
1067
|
const labelRadius = Math.max(0, ticker.labelBorderRadius ?? 0);
|
|
@@ -1096,20 +1123,18 @@ function createChart(element, options = {}) {
|
|
|
1096
1123
|
ctx.save();
|
|
1097
1124
|
ctx.strokeStyle = labelBorderColor;
|
|
1098
1125
|
ctx.lineWidth = labelBorderWidth;
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
ctx.setLineDash([]);
|
|
1105
|
-
}
|
|
1126
|
+
applyDashPattern(
|
|
1127
|
+
labelBorderStyle,
|
|
1128
|
+
dashPatterns.borderDotted,
|
|
1129
|
+
dashPatterns.borderDashed
|
|
1130
|
+
);
|
|
1106
1131
|
strokeRoundedRect(Math.round(x), Math.round(y), widthValue, labelHeight, labelRadius);
|
|
1107
1132
|
ctx.restore();
|
|
1108
1133
|
};
|
|
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;
|
|
@@ -29,6 +31,15 @@ interface ChartOptions {
|
|
|
29
31
|
priceLines?: PriceLineOptions[];
|
|
30
32
|
orderLines?: OrderLineOptions[];
|
|
31
33
|
tickerLine?: TickerLineOptions;
|
|
34
|
+
dashPatterns?: Partial<DashPatternOptions>;
|
|
35
|
+
}
|
|
36
|
+
interface DashPatternOptions {
|
|
37
|
+
dotted: [number, number];
|
|
38
|
+
dashed: [number, number];
|
|
39
|
+
connectorDotted: [number, number];
|
|
40
|
+
connectorDashed: [number, number];
|
|
41
|
+
borderDotted: [number, number];
|
|
42
|
+
borderDashed: [number, number];
|
|
32
43
|
}
|
|
33
44
|
interface AxisOptions {
|
|
34
45
|
lineColor?: string;
|
|
@@ -119,6 +130,8 @@ interface OrderLineOptions {
|
|
|
119
130
|
actionButtonFontWeight?: number | string;
|
|
120
131
|
actionButtonBorderColor?: string;
|
|
121
132
|
actionButtonBorderStyle?: "solid" | "dotted" | "dashed";
|
|
133
|
+
actionButtonsInnerGap?: number;
|
|
134
|
+
actionButtonsGroupGap?: number;
|
|
122
135
|
actionButtons?: OrderActionButton[];
|
|
123
136
|
connectorToPrice?: number;
|
|
124
137
|
connectorColor?: string;
|
|
@@ -208,4 +221,4 @@ interface OhlcDataPoint {
|
|
|
208
221
|
}
|
|
209
222
|
declare function createChart(element: HTMLElement, options?: ChartOptions): ChartInstance;
|
|
210
223
|
|
|
211
|
-
export { type AxisOptions, type ChartClickEvent, type ChartInstance, type ChartOptions, type CrosshairMoveEvent, type CrosshairOptions, type GridOptions, type OhlcDataPoint, type OrderActionButton, type OrderActionEvent, type OrderLineOptions, type PriceLineOptions, type TickerLineOptions, type WatermarkOptions, createChart };
|
|
224
|
+
export { type AxisOptions, type ChartClickEvent, type ChartInstance, type ChartOptions, type CrosshairMoveEvent, type CrosshairOptions, type DashPatternOptions, type GridOptions, type OhlcDataPoint, type OrderActionButton, type OrderActionEvent, type OrderLineOptions, type PriceLineOptions, type TickerLineOptions, type WatermarkOptions, createChart };
|
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;
|
|
@@ -29,6 +31,15 @@ interface ChartOptions {
|
|
|
29
31
|
priceLines?: PriceLineOptions[];
|
|
30
32
|
orderLines?: OrderLineOptions[];
|
|
31
33
|
tickerLine?: TickerLineOptions;
|
|
34
|
+
dashPatterns?: Partial<DashPatternOptions>;
|
|
35
|
+
}
|
|
36
|
+
interface DashPatternOptions {
|
|
37
|
+
dotted: [number, number];
|
|
38
|
+
dashed: [number, number];
|
|
39
|
+
connectorDotted: [number, number];
|
|
40
|
+
connectorDashed: [number, number];
|
|
41
|
+
borderDotted: [number, number];
|
|
42
|
+
borderDashed: [number, number];
|
|
32
43
|
}
|
|
33
44
|
interface AxisOptions {
|
|
34
45
|
lineColor?: string;
|
|
@@ -119,6 +130,8 @@ interface OrderLineOptions {
|
|
|
119
130
|
actionButtonFontWeight?: number | string;
|
|
120
131
|
actionButtonBorderColor?: string;
|
|
121
132
|
actionButtonBorderStyle?: "solid" | "dotted" | "dashed";
|
|
133
|
+
actionButtonsInnerGap?: number;
|
|
134
|
+
actionButtonsGroupGap?: number;
|
|
122
135
|
actionButtons?: OrderActionButton[];
|
|
123
136
|
connectorToPrice?: number;
|
|
124
137
|
connectorColor?: string;
|
|
@@ -208,4 +221,4 @@ interface OhlcDataPoint {
|
|
|
208
221
|
}
|
|
209
222
|
declare function createChart(element: HTMLElement, options?: ChartOptions): ChartInstance;
|
|
210
223
|
|
|
211
|
-
export { type AxisOptions, type ChartClickEvent, type ChartInstance, type ChartOptions, type CrosshairMoveEvent, type CrosshairOptions, type GridOptions, type OhlcDataPoint, type OrderActionButton, type OrderActionEvent, type OrderLineOptions, type PriceLineOptions, type TickerLineOptions, type WatermarkOptions, createChart };
|
|
224
|
+
export { type AxisOptions, type ChartClickEvent, type ChartInstance, type ChartOptions, type CrosshairMoveEvent, type CrosshairOptions, type DashPatternOptions, type GridOptions, type OhlcDataPoint, type OrderActionButton, type OrderActionEvent, type OrderLineOptions, type PriceLineOptions, type TickerLineOptions, type WatermarkOptions, createChart };
|
package/dist/index.js
CHANGED
|
@@ -44,6 +44,14 @@ var DEFAULT_WATERMARK_OPTIONS = {
|
|
|
44
44
|
imageTintColor: "",
|
|
45
45
|
imageTintOpacity: 1
|
|
46
46
|
};
|
|
47
|
+
var DEFAULT_DASH_PATTERNS = {
|
|
48
|
+
dotted: [2, 2],
|
|
49
|
+
dashed: [8, 6],
|
|
50
|
+
connectorDotted: [2, 3],
|
|
51
|
+
connectorDashed: [6, 5],
|
|
52
|
+
borderDotted: [2, 2],
|
|
53
|
+
borderDashed: [6, 4]
|
|
54
|
+
};
|
|
47
55
|
var DEFAULT_PRICE_LINE_OPTIONS = {
|
|
48
56
|
visible: true,
|
|
49
57
|
style: "solid",
|
|
@@ -76,6 +84,8 @@ var DEFAULT_ORDER_LINE_OPTIONS = {
|
|
|
76
84
|
actionButtonFontWeight: 500,
|
|
77
85
|
actionButtonBorderColor: "#2563eb",
|
|
78
86
|
actionButtonBorderStyle: "solid",
|
|
87
|
+
actionButtonsInnerGap: 6,
|
|
88
|
+
actionButtonsGroupGap: 8,
|
|
79
89
|
actionButtons: [],
|
|
80
90
|
connectorToPrice: Number.NaN,
|
|
81
91
|
connectorColor: "#2563eb",
|
|
@@ -92,6 +102,8 @@ var DEFAULT_OPTIONS = {
|
|
|
92
102
|
axisColor: "#7f8289",
|
|
93
103
|
axis: DEFAULT_AXIS_OPTIONS,
|
|
94
104
|
priceDecimals: 2,
|
|
105
|
+
stabilizePriceLabels: true,
|
|
106
|
+
priceLabelMinIntegerDigits: 3,
|
|
95
107
|
initialViewport: "latest",
|
|
96
108
|
initialVisibleBars: 60,
|
|
97
109
|
minVisibleBars: 5,
|
|
@@ -123,7 +135,8 @@ var DEFAULT_OPTIONS = {
|
|
|
123
135
|
labelBackgroundColor: "#38bdf8",
|
|
124
136
|
labelTextColor: "#0b1220",
|
|
125
137
|
labelBorderRadius: 3
|
|
126
|
-
}
|
|
138
|
+
},
|
|
139
|
+
dashPatterns: DEFAULT_DASH_PATTERNS
|
|
127
140
|
};
|
|
128
141
|
var BRAND_LOGO_VIEWBOX_WIDTH = 190;
|
|
129
142
|
var BRAND_LOGO_VIEWBOX_HEIGHT = 186;
|
|
@@ -153,6 +166,10 @@ function createChart(element, options = {}) {
|
|
|
153
166
|
tickerLine: {
|
|
154
167
|
...DEFAULT_OPTIONS.tickerLine,
|
|
155
168
|
...options.tickerLine
|
|
169
|
+
},
|
|
170
|
+
dashPatterns: {
|
|
171
|
+
...DEFAULT_DASH_PATTERNS,
|
|
172
|
+
...options.dashPatterns ?? {}
|
|
156
173
|
}
|
|
157
174
|
};
|
|
158
175
|
let width = mergedOptions.width;
|
|
@@ -240,6 +257,23 @@ function createChart(element, options = {}) {
|
|
|
240
257
|
const clamp = (value, min, max) => {
|
|
241
258
|
return Math.min(max, Math.max(min, value));
|
|
242
259
|
};
|
|
260
|
+
const dashPatterns = {
|
|
261
|
+
dotted: mergedOptions.dashPatterns.dotted ?? DEFAULT_DASH_PATTERNS.dotted,
|
|
262
|
+
dashed: mergedOptions.dashPatterns.dashed ?? DEFAULT_DASH_PATTERNS.dashed,
|
|
263
|
+
connectorDotted: mergedOptions.dashPatterns.connectorDotted ?? DEFAULT_DASH_PATTERNS.connectorDotted,
|
|
264
|
+
connectorDashed: mergedOptions.dashPatterns.connectorDashed ?? DEFAULT_DASH_PATTERNS.connectorDashed,
|
|
265
|
+
borderDotted: mergedOptions.dashPatterns.borderDotted ?? DEFAULT_DASH_PATTERNS.borderDotted,
|
|
266
|
+
borderDashed: mergedOptions.dashPatterns.borderDashed ?? DEFAULT_DASH_PATTERNS.borderDashed
|
|
267
|
+
};
|
|
268
|
+
const applyDashPattern = (style, dotted, dashed) => {
|
|
269
|
+
if (style === "dotted") {
|
|
270
|
+
ctx.setLineDash(dotted);
|
|
271
|
+
} else if (style === "dashed") {
|
|
272
|
+
ctx.setLineDash(dashed);
|
|
273
|
+
} else {
|
|
274
|
+
ctx.setLineDash([]);
|
|
275
|
+
}
|
|
276
|
+
};
|
|
243
277
|
const clampXViewport = () => {
|
|
244
278
|
const count = data.length;
|
|
245
279
|
if (count === 0) {
|
|
@@ -309,6 +343,27 @@ function createChart(element, options = {}) {
|
|
|
309
343
|
const decimals = clamp(Math.round(mergedOptions.priceDecimals), 0, 8);
|
|
310
344
|
return price.toFixed(decimals);
|
|
311
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
|
+
};
|
|
312
367
|
const parseData = (nextData) => {
|
|
313
368
|
return nextData.map((point) => ({
|
|
314
369
|
time: new Date(point.t),
|
|
@@ -413,13 +468,7 @@ function createChart(element, options = {}) {
|
|
|
413
468
|
ctx.save();
|
|
414
469
|
ctx.strokeStyle = color;
|
|
415
470
|
ctx.lineWidth = Math.max(1, mergedLine.thickness);
|
|
416
|
-
|
|
417
|
-
ctx.setLineDash([2, 4]);
|
|
418
|
-
} else if (mergedLine.style === "dashed") {
|
|
419
|
-
ctx.setLineDash([8, 6]);
|
|
420
|
-
} else {
|
|
421
|
-
ctx.setLineDash([]);
|
|
422
|
-
}
|
|
471
|
+
applyDashPattern(mergedLine.style, dashPatterns.dotted, dashPatterns.dashed);
|
|
423
472
|
ctx.beginPath();
|
|
424
473
|
ctx.moveTo(crisp(chartLeft), crisp(lineY));
|
|
425
474
|
ctx.lineTo(crisp(chartRight), crisp(lineY));
|
|
@@ -433,7 +482,7 @@ function createChart(element, options = {}) {
|
|
|
433
482
|
ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
|
|
434
483
|
const labelPaddingX = 8;
|
|
435
484
|
const labelHeight = 20;
|
|
436
|
-
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;
|
|
437
486
|
const labelX = chartRight + 4;
|
|
438
487
|
const labelY = clamp(lineY - labelHeight / 2, chartTop, chartBottom - labelHeight);
|
|
439
488
|
ctx.fillStyle = mergedLine.labelBackgroundColor;
|
|
@@ -473,13 +522,7 @@ function createChart(element, options = {}) {
|
|
|
473
522
|
ctx.save();
|
|
474
523
|
ctx.strokeStyle = color;
|
|
475
524
|
ctx.lineWidth = Math.max(1, mergedLine.thickness);
|
|
476
|
-
|
|
477
|
-
ctx.setLineDash([2, 4]);
|
|
478
|
-
} else if (mergedLine.style === "dashed") {
|
|
479
|
-
ctx.setLineDash([8, 6]);
|
|
480
|
-
} else {
|
|
481
|
-
ctx.setLineDash([]);
|
|
482
|
-
}
|
|
525
|
+
applyDashPattern(mergedLine.style, dashPatterns.dotted, dashPatterns.dashed);
|
|
483
526
|
ctx.beginPath();
|
|
484
527
|
ctx.moveTo(crisp(chartLeft), crisp(lineY));
|
|
485
528
|
ctx.lineTo(crisp(chartRight), crisp(lineY));
|
|
@@ -491,13 +534,11 @@ function createChart(element, options = {}) {
|
|
|
491
534
|
ctx.save();
|
|
492
535
|
ctx.strokeStyle = mergedLine.connectorColor ?? color;
|
|
493
536
|
ctx.lineWidth = Math.max(1, mergedLine.connectorThickness);
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
ctx.setLineDash([]);
|
|
500
|
-
}
|
|
537
|
+
applyDashPattern(
|
|
538
|
+
mergedLine.connectorStyle,
|
|
539
|
+
dashPatterns.connectorDotted,
|
|
540
|
+
dashPatterns.connectorDashed
|
|
541
|
+
);
|
|
501
542
|
ctx.beginPath();
|
|
502
543
|
ctx.moveTo(crisp(connectorX), crisp(lineY));
|
|
503
544
|
ctx.lineTo(crisp(connectorX), crisp(connectorY));
|
|
@@ -543,9 +584,9 @@ function createChart(element, options = {}) {
|
|
|
543
584
|
const width2 = Math.max(minWidth, Math.ceil(ctx.measureText(button.text).width) + paddingX * 2);
|
|
544
585
|
return { button, width: width2 };
|
|
545
586
|
});
|
|
546
|
-
const actionButtonInnerGap = actionButtonMetrics.length > 1 ?
|
|
587
|
+
const actionButtonInnerGap = actionButtonMetrics.length > 1 ? Math.max(0, mergedLine.actionButtonsInnerGap) : 0;
|
|
547
588
|
const actionButtonsTotalWidth = actionButtonMetrics.reduce((sum, metric) => sum + metric.width, 0) + Math.max(0, actionButtonMetrics.length - 1) * actionButtonInnerGap;
|
|
548
|
-
const actionButtonsGap = actionButtonMetrics.length > 0 ?
|
|
589
|
+
const actionButtonsGap = actionButtonMetrics.length > 0 ? Math.max(0, mergedLine.actionButtonsGroupGap) : 0;
|
|
549
590
|
const segmentPaddingX = 8;
|
|
550
591
|
const labelHeight = 22;
|
|
551
592
|
const qtyWidth = qtyText ? Math.ceil(ctx.measureText(qtyText).width) + segmentPaddingX * 2 : 0;
|
|
@@ -600,13 +641,11 @@ function createChart(element, options = {}) {
|
|
|
600
641
|
ctx.save();
|
|
601
642
|
ctx.strokeStyle = actionBorderColor;
|
|
602
643
|
ctx.lineWidth = 1;
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
ctx.setLineDash([]);
|
|
609
|
-
}
|
|
644
|
+
applyDashPattern(
|
|
645
|
+
actionBorderStyle,
|
|
646
|
+
dashPatterns.borderDotted,
|
|
647
|
+
dashPatterns.borderDashed
|
|
648
|
+
);
|
|
610
649
|
strokeRoundedRect(Math.round(actionX), Math.round(actionY), actionW, actionH, actionRadius);
|
|
611
650
|
ctx.restore();
|
|
612
651
|
const baseFont = ctx.font;
|
|
@@ -671,7 +710,7 @@ function createChart(element, options = {}) {
|
|
|
671
710
|
}
|
|
672
711
|
const priceText = formatPrice(renderPrice);
|
|
673
712
|
const pricePaddingX = 8;
|
|
674
|
-
const measuredPriceWidth =
|
|
713
|
+
const measuredPriceWidth = getPriceLabelWidth(priceText, pricePaddingX);
|
|
675
714
|
const priceWidth = mergedLine.id === void 0 ? measuredPriceWidth : Math.max(measuredPriceWidth, orderPriceTagWidthById.get(mergedLine.id) ?? 0);
|
|
676
715
|
if (mergedLine.id) {
|
|
677
716
|
orderPriceTagWidthById.set(mergedLine.id, priceWidth);
|
|
@@ -945,13 +984,7 @@ function createChart(element, options = {}) {
|
|
|
945
984
|
ctx.save();
|
|
946
985
|
ctx.strokeStyle = crosshair.color;
|
|
947
986
|
ctx.lineWidth = Math.max(1, crosshair.width);
|
|
948
|
-
|
|
949
|
-
ctx.setLineDash([2, 4]);
|
|
950
|
-
} else if (crosshair.style === "dashed") {
|
|
951
|
-
ctx.setLineDash([8, 6]);
|
|
952
|
-
} else {
|
|
953
|
-
ctx.setLineDash([]);
|
|
954
|
-
}
|
|
987
|
+
applyDashPattern(crosshair.style, dashPatterns.dotted, dashPatterns.dashed);
|
|
955
988
|
if (crosshair.showVertical) {
|
|
956
989
|
ctx.beginPath();
|
|
957
990
|
ctx.moveTo(crisp(cx), crisp(chartTop));
|
|
@@ -993,13 +1026,7 @@ function createChart(element, options = {}) {
|
|
|
993
1026
|
ctx.save();
|
|
994
1027
|
ctx.strokeStyle = tickerColor;
|
|
995
1028
|
ctx.lineWidth = tickerThickness;
|
|
996
|
-
|
|
997
|
-
ctx.setLineDash([2, 4]);
|
|
998
|
-
} else if (tickerStyle === "dashed") {
|
|
999
|
-
ctx.setLineDash([8, 6]);
|
|
1000
|
-
} else {
|
|
1001
|
-
ctx.setLineDash([]);
|
|
1002
|
-
}
|
|
1029
|
+
applyDashPattern(tickerStyle, dashPatterns.dotted, dashPatterns.dashed);
|
|
1003
1030
|
ctx.beginPath();
|
|
1004
1031
|
ctx.moveTo(crisp(chartLeft), crisp(lineY));
|
|
1005
1032
|
ctx.lineTo(crisp(chartRight), crisp(lineY));
|
|
@@ -1010,7 +1037,7 @@ function createChart(element, options = {}) {
|
|
|
1010
1037
|
ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
|
|
1011
1038
|
const labelPaddingX = 8;
|
|
1012
1039
|
const labelHeight = 20;
|
|
1013
|
-
const labelWidth =
|
|
1040
|
+
const labelWidth = getPriceLabelWidth(tickerLabel, labelPaddingX);
|
|
1014
1041
|
const labelX = chartRight + 4;
|
|
1015
1042
|
const labelY = clamp(lineY - labelHeight / 2, chartTop, chartBottom - labelHeight);
|
|
1016
1043
|
const labelRadius = Math.max(0, ticker.labelBorderRadius ?? 0);
|
|
@@ -1072,20 +1099,18 @@ function createChart(element, options = {}) {
|
|
|
1072
1099
|
ctx.save();
|
|
1073
1100
|
ctx.strokeStyle = labelBorderColor;
|
|
1074
1101
|
ctx.lineWidth = labelBorderWidth;
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
ctx.setLineDash([]);
|
|
1081
|
-
}
|
|
1102
|
+
applyDashPattern(
|
|
1103
|
+
labelBorderStyle,
|
|
1104
|
+
dashPatterns.borderDotted,
|
|
1105
|
+
dashPatterns.borderDashed
|
|
1106
|
+
);
|
|
1082
1107
|
strokeRoundedRect(Math.round(x), Math.round(y), widthValue, labelHeight, labelRadius);
|
|
1083
1108
|
ctx.restore();
|
|
1084
1109
|
};
|
|
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)
|
|
@@ -57,6 +59,7 @@ Top-level options:
|
|
|
57
59
|
- `priceLines?: PriceLineOptions[]`
|
|
58
60
|
- `orderLines?: OrderLineOptions[]`
|
|
59
61
|
- `tickerLine?: TickerLineOptions`
|
|
62
|
+
- `dashPatterns?: Partial<DashPatternOptions>` (controls dotted/dashed spacing)
|
|
60
63
|
|
|
61
64
|
### `AxisOptions`
|
|
62
65
|
|
|
@@ -129,6 +132,15 @@ watermark: {
|
|
|
129
132
|
- `labelTextColor` (default `#0b1220`)
|
|
130
133
|
- `labelBorderRadius` (default `3`)
|
|
131
134
|
|
|
135
|
+
### `DashPatternOptions`
|
|
136
|
+
|
|
137
|
+
- `dotted` (default `[2, 2]`)
|
|
138
|
+
- `dashed` (default `[8, 6]`)
|
|
139
|
+
- `connectorDotted` (default `[2, 3]`)
|
|
140
|
+
- `connectorDashed` (default `[6, 5]`)
|
|
141
|
+
- `borderDotted` (default `[2, 2]`)
|
|
142
|
+
- `borderDashed` (default `[6, 4]`)
|
|
143
|
+
|
|
132
144
|
### `PriceLineOptions`
|
|
133
145
|
|
|
134
146
|
- `id?: string`
|
|
@@ -198,6 +210,8 @@ Legacy single action button:
|
|
|
198
210
|
- `actionButtonFontWeight?: number | string`
|
|
199
211
|
- `actionButtonBorderColor?: string`
|
|
200
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)
|
|
201
215
|
|
|
202
216
|
Multi-button actions:
|
|
203
217
|
|
package/docs/RECIPES.md
CHANGED
|
@@ -38,6 +38,29 @@ chart.addPriceLine({
|
|
|
38
38
|
});
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
+
## Tighten dotted spacing globally
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
const chart = createChart(root, {
|
|
45
|
+
dashPatterns: {
|
|
46
|
+
dotted: [2, 1],
|
|
47
|
+
dashed: [8, 5],
|
|
48
|
+
connectorDotted: [2, 2],
|
|
49
|
+
borderDotted: [2, 1]
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
```
|
|
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
|
+
|
|
41
64
|
## Add a draggable pending limit line
|
|
42
65
|
|
|
43
66
|
```ts
|