hyperprop-charting-library 0.1.45 → 0.1.47
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 +13 -0
- package/dist/hyperprop-charting-library.cjs +64 -8
- package/dist/hyperprop-charting-library.d.ts +5 -0
- package/dist/hyperprop-charting-library.js +64 -8
- package/dist/index.cjs +64 -8
- package/dist/index.d.cts +5 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +64 -8
- package/docs/API.md +5 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -60,6 +60,19 @@ const chart = createChart(root, {
|
|
|
60
60
|
});
|
|
61
61
|
```
|
|
62
62
|
|
|
63
|
+
## Current Price Label Subtext
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
const chart = createChart(root, {
|
|
67
|
+
tickerLine: {
|
|
68
|
+
labelSubtexts: ["MNQ", "RTH"],
|
|
69
|
+
showCountdownInLabel: true
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
The price label grows to fit all extra lines. `labelSubtext: "MNQ"` still works for the simple one-line case.
|
|
75
|
+
|
|
63
76
|
## Candle Color Behavior
|
|
64
77
|
|
|
65
78
|
You can control how up/down candle color is decided:
|
|
@@ -201,6 +201,11 @@ var DEFAULT_OPTIONS = {
|
|
|
201
201
|
style: "dotted",
|
|
202
202
|
thickness: 1,
|
|
203
203
|
labelTextColor: "#0b1220",
|
|
204
|
+
labelSubtext: "",
|
|
205
|
+
labelSubtexts: [],
|
|
206
|
+
labelSubtextColor: "#0b1220",
|
|
207
|
+
labelSubtextFontSize: 0,
|
|
208
|
+
showCountdownInLabel: false,
|
|
204
209
|
labelBorderRadius: 3
|
|
205
210
|
},
|
|
206
211
|
labels: DEFAULT_LABELS_OPTIONS,
|
|
@@ -2103,6 +2108,16 @@ function createChart(element, options = {}) {
|
|
|
2103
2108
|
ctx.stroke();
|
|
2104
2109
|
ctx.restore();
|
|
2105
2110
|
};
|
|
2111
|
+
const getCountdownText = () => {
|
|
2112
|
+
const last = data[data.length - 1];
|
|
2113
|
+
if (!last) {
|
|
2114
|
+
return null;
|
|
2115
|
+
}
|
|
2116
|
+
const stepMs = getTimeStepMs();
|
|
2117
|
+
const rawRemainingMs = last.time.getTime() + stepMs - Date.now();
|
|
2118
|
+
const countdownMs = rawRemainingMs >= 0 && rawRemainingMs <= stepMs * 2 ? rawRemainingMs : stepMs - Date.now() % stepMs;
|
|
2119
|
+
return formatDuration(countdownMs);
|
|
2120
|
+
};
|
|
2106
2121
|
const ticker = mergedOptions.tickerLine ?? DEFAULT_OPTIONS.tickerLine;
|
|
2107
2122
|
const lastPoint = data[data.length - 1];
|
|
2108
2123
|
let tickerPrice = null;
|
|
@@ -2127,8 +2142,16 @@ function createChart(element, options = {}) {
|
|
|
2127
2142
|
ctx.restore();
|
|
2128
2143
|
}
|
|
2129
2144
|
if ((ticker.visible ?? true) && labels.showLastPrice && tickerPrice !== null && tickerColor !== null) {
|
|
2145
|
+
const tickerSubtexts = [
|
|
2146
|
+
...ticker.labelSubtext ? [ticker.labelSubtext] : [],
|
|
2147
|
+
...ticker.labelSubtexts ?? [],
|
|
2148
|
+
...ticker.showCountdownInLabel ? [getCountdownText()] : []
|
|
2149
|
+
].map((value) => value === null || value === void 0 ? "" : String(value).trim()).filter((value) => value.length > 0);
|
|
2130
2150
|
addPriceAxisLabel({
|
|
2131
2151
|
text: formatPrice(tickerPrice),
|
|
2152
|
+
...tickerSubtexts.length > 0 ? { subtexts: tickerSubtexts } : {},
|
|
2153
|
+
subtextColor: ticker.labelSubtextColor ?? ticker.labelTextColor ?? "#0b1220",
|
|
2154
|
+
...ticker.labelSubtextFontSize === void 0 ? {} : { subtextFontSize: ticker.labelSubtextFontSize },
|
|
2132
2155
|
price: tickerPrice,
|
|
2133
2156
|
backgroundColor: ticker.labelBackgroundColor ?? tickerColor,
|
|
2134
2157
|
textColor: ticker.labelTextColor ?? "#0b1220",
|
|
@@ -2207,28 +2230,44 @@ function createChart(element, options = {}) {
|
|
|
2207
2230
|
}
|
|
2208
2231
|
if (priceAxisLabels.length > 0) {
|
|
2209
2232
|
const labelPaddingX = Math.max(4, labels.labelPaddingX);
|
|
2210
|
-
const
|
|
2233
|
+
const baseLabelHeight = Math.max(14, labels.labelHeight);
|
|
2211
2234
|
const labelRadius = Math.max(0, labels.borderRadius);
|
|
2212
|
-
|
|
2235
|
+
const priceLabelFontSize = Math.max(8, axis.fontSize);
|
|
2236
|
+
ctx.font = `${priceLabelFontSize}px ${mergedOptions.fontFamily}`;
|
|
2213
2237
|
const positionedLabels = priceAxisLabels.map((label) => {
|
|
2214
|
-
const
|
|
2238
|
+
const subtexts = (label.subtexts ?? []).map((value) => value.trim()).filter((value) => value.length > 0);
|
|
2239
|
+
const subtextFontSize = label.subtextFontSize !== void 0 && label.subtextFontSize > 0 ? Math.max(8, Math.round(label.subtextFontSize)) : priceLabelFontSize;
|
|
2240
|
+
const subtextLineGap = 5;
|
|
2241
|
+
const labelHeight = baseLabelHeight + (subtexts.length > 0 ? subtexts.length * subtextFontSize + subtexts.length * subtextLineGap : 0);
|
|
2242
|
+
const primaryWidth = label.text === formatPrice(label.price) ? getPriceLabelWidth(label.text, labelPaddingX) : Math.ceil(ctx.measureText(label.text).width) + labelPaddingX * 2;
|
|
2243
|
+
let subtextWidth = 0;
|
|
2244
|
+
if (subtexts.length > 0) {
|
|
2245
|
+
const baseFont = ctx.font;
|
|
2246
|
+
ctx.font = `${subtextFontSize}px ${mergedOptions.fontFamily}`;
|
|
2247
|
+
subtextWidth = Math.max(...subtexts.map((subtext) => Math.ceil(ctx.measureText(subtext).width) + labelPaddingX * 2));
|
|
2248
|
+
ctx.font = baseFont;
|
|
2249
|
+
}
|
|
2250
|
+
const labelTextWidth = Math.max(primaryWidth, subtextWidth);
|
|
2215
2251
|
return {
|
|
2216
2252
|
...label,
|
|
2253
|
+
subtexts,
|
|
2254
|
+
subtextFontSize,
|
|
2255
|
+
height: labelHeight,
|
|
2217
2256
|
width: labelTextWidth,
|
|
2218
2257
|
targetY: clamp(yFromPrice(label.price), chartTop + 1, chartBottom - 1) - labelHeight / 2,
|
|
2219
2258
|
y: 0
|
|
2220
2259
|
};
|
|
2221
2260
|
}).sort((a, b) => a.targetY - b.targetY || b.priority - a.priority);
|
|
2222
2261
|
const minY = chartTop;
|
|
2223
|
-
const maxY = chartBottom - labelHeight;
|
|
2224
2262
|
let cursorY = minY;
|
|
2225
2263
|
for (const label of positionedLabels) {
|
|
2264
|
+
const maxY = Math.max(minY, chartBottom - label.height);
|
|
2226
2265
|
label.y = labels.noOverlapping ? Math.max(clamp(label.targetY, minY, maxY), cursorY) : clamp(label.targetY, minY, maxY);
|
|
2227
|
-
cursorY = label.y +
|
|
2266
|
+
cursorY = label.y + label.height + 2;
|
|
2228
2267
|
}
|
|
2229
2268
|
if (labels.noOverlapping && positionedLabels.length > 0) {
|
|
2230
2269
|
const lastLabel = positionedLabels[positionedLabels.length - 1];
|
|
2231
|
-
const overflow = lastLabel ? lastLabel.y +
|
|
2270
|
+
const overflow = lastLabel ? lastLabel.y + lastLabel.height - chartBottom : 0;
|
|
2232
2271
|
if (overflow > 0) {
|
|
2233
2272
|
for (const label of positionedLabels) {
|
|
2234
2273
|
label.y = Math.max(minY, label.y - overflow);
|
|
@@ -2238,8 +2277,25 @@ function createChart(element, options = {}) {
|
|
|
2238
2277
|
const labelX = chartRight + 4;
|
|
2239
2278
|
for (const label of positionedLabels) {
|
|
2240
2279
|
ctx.fillStyle = label.backgroundColor;
|
|
2241
|
-
fillRoundedRect(Math.round(labelX), Math.round(label.y), label.width,
|
|
2242
|
-
|
|
2280
|
+
fillRoundedRect(Math.round(labelX), Math.round(label.y), label.width, label.height, labelRadius);
|
|
2281
|
+
const hasSubtexts = label.subtexts.length > 0;
|
|
2282
|
+
const primaryY = hasSubtexts ? label.y + baseLabelHeight / 2 - 1 : label.y + label.height / 2;
|
|
2283
|
+
drawText(label.text, labelX + labelPaddingX, primaryY, "left", "middle", label.textColor);
|
|
2284
|
+
if (hasSubtexts) {
|
|
2285
|
+
const baseFont = ctx.font;
|
|
2286
|
+
ctx.font = `${label.subtextFontSize}px ${mergedOptions.fontFamily}`;
|
|
2287
|
+
label.subtexts.forEach((subtext, index) => {
|
|
2288
|
+
drawText(
|
|
2289
|
+
subtext,
|
|
2290
|
+
labelX + labelPaddingX,
|
|
2291
|
+
label.y + baseLabelHeight + index * (label.subtextFontSize + 5) + label.subtextFontSize / 2,
|
|
2292
|
+
"left",
|
|
2293
|
+
"middle",
|
|
2294
|
+
label.subtextColor ?? label.textColor
|
|
2295
|
+
);
|
|
2296
|
+
});
|
|
2297
|
+
ctx.font = baseFont;
|
|
2298
|
+
}
|
|
2243
2299
|
}
|
|
2244
2300
|
}
|
|
2245
2301
|
for (const priceLine of priceLines) {
|
|
@@ -260,6 +260,11 @@ interface TickerLineOptions {
|
|
|
260
260
|
color?: string;
|
|
261
261
|
labelBackgroundColor?: string;
|
|
262
262
|
labelTextColor?: string;
|
|
263
|
+
labelSubtext?: string;
|
|
264
|
+
labelSubtexts?: Array<string | number>;
|
|
265
|
+
labelSubtextColor?: string;
|
|
266
|
+
labelSubtextFontSize?: number;
|
|
267
|
+
showCountdownInLabel?: boolean;
|
|
263
268
|
labelBorderRadius?: number;
|
|
264
269
|
smoothing?: boolean;
|
|
265
270
|
smoothingSpeed?: number;
|
|
@@ -177,6 +177,11 @@ var DEFAULT_OPTIONS = {
|
|
|
177
177
|
style: "dotted",
|
|
178
178
|
thickness: 1,
|
|
179
179
|
labelTextColor: "#0b1220",
|
|
180
|
+
labelSubtext: "",
|
|
181
|
+
labelSubtexts: [],
|
|
182
|
+
labelSubtextColor: "#0b1220",
|
|
183
|
+
labelSubtextFontSize: 0,
|
|
184
|
+
showCountdownInLabel: false,
|
|
180
185
|
labelBorderRadius: 3
|
|
181
186
|
},
|
|
182
187
|
labels: DEFAULT_LABELS_OPTIONS,
|
|
@@ -2079,6 +2084,16 @@ function createChart(element, options = {}) {
|
|
|
2079
2084
|
ctx.stroke();
|
|
2080
2085
|
ctx.restore();
|
|
2081
2086
|
};
|
|
2087
|
+
const getCountdownText = () => {
|
|
2088
|
+
const last = data[data.length - 1];
|
|
2089
|
+
if (!last) {
|
|
2090
|
+
return null;
|
|
2091
|
+
}
|
|
2092
|
+
const stepMs = getTimeStepMs();
|
|
2093
|
+
const rawRemainingMs = last.time.getTime() + stepMs - Date.now();
|
|
2094
|
+
const countdownMs = rawRemainingMs >= 0 && rawRemainingMs <= stepMs * 2 ? rawRemainingMs : stepMs - Date.now() % stepMs;
|
|
2095
|
+
return formatDuration(countdownMs);
|
|
2096
|
+
};
|
|
2082
2097
|
const ticker = mergedOptions.tickerLine ?? DEFAULT_OPTIONS.tickerLine;
|
|
2083
2098
|
const lastPoint = data[data.length - 1];
|
|
2084
2099
|
let tickerPrice = null;
|
|
@@ -2103,8 +2118,16 @@ function createChart(element, options = {}) {
|
|
|
2103
2118
|
ctx.restore();
|
|
2104
2119
|
}
|
|
2105
2120
|
if ((ticker.visible ?? true) && labels.showLastPrice && tickerPrice !== null && tickerColor !== null) {
|
|
2121
|
+
const tickerSubtexts = [
|
|
2122
|
+
...ticker.labelSubtext ? [ticker.labelSubtext] : [],
|
|
2123
|
+
...ticker.labelSubtexts ?? [],
|
|
2124
|
+
...ticker.showCountdownInLabel ? [getCountdownText()] : []
|
|
2125
|
+
].map((value) => value === null || value === void 0 ? "" : String(value).trim()).filter((value) => value.length > 0);
|
|
2106
2126
|
addPriceAxisLabel({
|
|
2107
2127
|
text: formatPrice(tickerPrice),
|
|
2128
|
+
...tickerSubtexts.length > 0 ? { subtexts: tickerSubtexts } : {},
|
|
2129
|
+
subtextColor: ticker.labelSubtextColor ?? ticker.labelTextColor ?? "#0b1220",
|
|
2130
|
+
...ticker.labelSubtextFontSize === void 0 ? {} : { subtextFontSize: ticker.labelSubtextFontSize },
|
|
2108
2131
|
price: tickerPrice,
|
|
2109
2132
|
backgroundColor: ticker.labelBackgroundColor ?? tickerColor,
|
|
2110
2133
|
textColor: ticker.labelTextColor ?? "#0b1220",
|
|
@@ -2183,28 +2206,44 @@ function createChart(element, options = {}) {
|
|
|
2183
2206
|
}
|
|
2184
2207
|
if (priceAxisLabels.length > 0) {
|
|
2185
2208
|
const labelPaddingX = Math.max(4, labels.labelPaddingX);
|
|
2186
|
-
const
|
|
2209
|
+
const baseLabelHeight = Math.max(14, labels.labelHeight);
|
|
2187
2210
|
const labelRadius = Math.max(0, labels.borderRadius);
|
|
2188
|
-
|
|
2211
|
+
const priceLabelFontSize = Math.max(8, axis.fontSize);
|
|
2212
|
+
ctx.font = `${priceLabelFontSize}px ${mergedOptions.fontFamily}`;
|
|
2189
2213
|
const positionedLabels = priceAxisLabels.map((label) => {
|
|
2190
|
-
const
|
|
2214
|
+
const subtexts = (label.subtexts ?? []).map((value) => value.trim()).filter((value) => value.length > 0);
|
|
2215
|
+
const subtextFontSize = label.subtextFontSize !== void 0 && label.subtextFontSize > 0 ? Math.max(8, Math.round(label.subtextFontSize)) : priceLabelFontSize;
|
|
2216
|
+
const subtextLineGap = 5;
|
|
2217
|
+
const labelHeight = baseLabelHeight + (subtexts.length > 0 ? subtexts.length * subtextFontSize + subtexts.length * subtextLineGap : 0);
|
|
2218
|
+
const primaryWidth = label.text === formatPrice(label.price) ? getPriceLabelWidth(label.text, labelPaddingX) : Math.ceil(ctx.measureText(label.text).width) + labelPaddingX * 2;
|
|
2219
|
+
let subtextWidth = 0;
|
|
2220
|
+
if (subtexts.length > 0) {
|
|
2221
|
+
const baseFont = ctx.font;
|
|
2222
|
+
ctx.font = `${subtextFontSize}px ${mergedOptions.fontFamily}`;
|
|
2223
|
+
subtextWidth = Math.max(...subtexts.map((subtext) => Math.ceil(ctx.measureText(subtext).width) + labelPaddingX * 2));
|
|
2224
|
+
ctx.font = baseFont;
|
|
2225
|
+
}
|
|
2226
|
+
const labelTextWidth = Math.max(primaryWidth, subtextWidth);
|
|
2191
2227
|
return {
|
|
2192
2228
|
...label,
|
|
2229
|
+
subtexts,
|
|
2230
|
+
subtextFontSize,
|
|
2231
|
+
height: labelHeight,
|
|
2193
2232
|
width: labelTextWidth,
|
|
2194
2233
|
targetY: clamp(yFromPrice(label.price), chartTop + 1, chartBottom - 1) - labelHeight / 2,
|
|
2195
2234
|
y: 0
|
|
2196
2235
|
};
|
|
2197
2236
|
}).sort((a, b) => a.targetY - b.targetY || b.priority - a.priority);
|
|
2198
2237
|
const minY = chartTop;
|
|
2199
|
-
const maxY = chartBottom - labelHeight;
|
|
2200
2238
|
let cursorY = minY;
|
|
2201
2239
|
for (const label of positionedLabels) {
|
|
2240
|
+
const maxY = Math.max(minY, chartBottom - label.height);
|
|
2202
2241
|
label.y = labels.noOverlapping ? Math.max(clamp(label.targetY, minY, maxY), cursorY) : clamp(label.targetY, minY, maxY);
|
|
2203
|
-
cursorY = label.y +
|
|
2242
|
+
cursorY = label.y + label.height + 2;
|
|
2204
2243
|
}
|
|
2205
2244
|
if (labels.noOverlapping && positionedLabels.length > 0) {
|
|
2206
2245
|
const lastLabel = positionedLabels[positionedLabels.length - 1];
|
|
2207
|
-
const overflow = lastLabel ? lastLabel.y +
|
|
2246
|
+
const overflow = lastLabel ? lastLabel.y + lastLabel.height - chartBottom : 0;
|
|
2208
2247
|
if (overflow > 0) {
|
|
2209
2248
|
for (const label of positionedLabels) {
|
|
2210
2249
|
label.y = Math.max(minY, label.y - overflow);
|
|
@@ -2214,8 +2253,25 @@ function createChart(element, options = {}) {
|
|
|
2214
2253
|
const labelX = chartRight + 4;
|
|
2215
2254
|
for (const label of positionedLabels) {
|
|
2216
2255
|
ctx.fillStyle = label.backgroundColor;
|
|
2217
|
-
fillRoundedRect(Math.round(labelX), Math.round(label.y), label.width,
|
|
2218
|
-
|
|
2256
|
+
fillRoundedRect(Math.round(labelX), Math.round(label.y), label.width, label.height, labelRadius);
|
|
2257
|
+
const hasSubtexts = label.subtexts.length > 0;
|
|
2258
|
+
const primaryY = hasSubtexts ? label.y + baseLabelHeight / 2 - 1 : label.y + label.height / 2;
|
|
2259
|
+
drawText(label.text, labelX + labelPaddingX, primaryY, "left", "middle", label.textColor);
|
|
2260
|
+
if (hasSubtexts) {
|
|
2261
|
+
const baseFont = ctx.font;
|
|
2262
|
+
ctx.font = `${label.subtextFontSize}px ${mergedOptions.fontFamily}`;
|
|
2263
|
+
label.subtexts.forEach((subtext, index) => {
|
|
2264
|
+
drawText(
|
|
2265
|
+
subtext,
|
|
2266
|
+
labelX + labelPaddingX,
|
|
2267
|
+
label.y + baseLabelHeight + index * (label.subtextFontSize + 5) + label.subtextFontSize / 2,
|
|
2268
|
+
"left",
|
|
2269
|
+
"middle",
|
|
2270
|
+
label.subtextColor ?? label.textColor
|
|
2271
|
+
);
|
|
2272
|
+
});
|
|
2273
|
+
ctx.font = baseFont;
|
|
2274
|
+
}
|
|
2219
2275
|
}
|
|
2220
2276
|
}
|
|
2221
2277
|
for (const priceLine of priceLines) {
|
package/dist/index.cjs
CHANGED
|
@@ -201,6 +201,11 @@ var DEFAULT_OPTIONS = {
|
|
|
201
201
|
style: "dotted",
|
|
202
202
|
thickness: 1,
|
|
203
203
|
labelTextColor: "#0b1220",
|
|
204
|
+
labelSubtext: "",
|
|
205
|
+
labelSubtexts: [],
|
|
206
|
+
labelSubtextColor: "#0b1220",
|
|
207
|
+
labelSubtextFontSize: 0,
|
|
208
|
+
showCountdownInLabel: false,
|
|
204
209
|
labelBorderRadius: 3
|
|
205
210
|
},
|
|
206
211
|
labels: DEFAULT_LABELS_OPTIONS,
|
|
@@ -2103,6 +2108,16 @@ function createChart(element, options = {}) {
|
|
|
2103
2108
|
ctx.stroke();
|
|
2104
2109
|
ctx.restore();
|
|
2105
2110
|
};
|
|
2111
|
+
const getCountdownText = () => {
|
|
2112
|
+
const last = data[data.length - 1];
|
|
2113
|
+
if (!last) {
|
|
2114
|
+
return null;
|
|
2115
|
+
}
|
|
2116
|
+
const stepMs = getTimeStepMs();
|
|
2117
|
+
const rawRemainingMs = last.time.getTime() + stepMs - Date.now();
|
|
2118
|
+
const countdownMs = rawRemainingMs >= 0 && rawRemainingMs <= stepMs * 2 ? rawRemainingMs : stepMs - Date.now() % stepMs;
|
|
2119
|
+
return formatDuration(countdownMs);
|
|
2120
|
+
};
|
|
2106
2121
|
const ticker = mergedOptions.tickerLine ?? DEFAULT_OPTIONS.tickerLine;
|
|
2107
2122
|
const lastPoint = data[data.length - 1];
|
|
2108
2123
|
let tickerPrice = null;
|
|
@@ -2127,8 +2142,16 @@ function createChart(element, options = {}) {
|
|
|
2127
2142
|
ctx.restore();
|
|
2128
2143
|
}
|
|
2129
2144
|
if ((ticker.visible ?? true) && labels.showLastPrice && tickerPrice !== null && tickerColor !== null) {
|
|
2145
|
+
const tickerSubtexts = [
|
|
2146
|
+
...ticker.labelSubtext ? [ticker.labelSubtext] : [],
|
|
2147
|
+
...ticker.labelSubtexts ?? [],
|
|
2148
|
+
...ticker.showCountdownInLabel ? [getCountdownText()] : []
|
|
2149
|
+
].map((value) => value === null || value === void 0 ? "" : String(value).trim()).filter((value) => value.length > 0);
|
|
2130
2150
|
addPriceAxisLabel({
|
|
2131
2151
|
text: formatPrice(tickerPrice),
|
|
2152
|
+
...tickerSubtexts.length > 0 ? { subtexts: tickerSubtexts } : {},
|
|
2153
|
+
subtextColor: ticker.labelSubtextColor ?? ticker.labelTextColor ?? "#0b1220",
|
|
2154
|
+
...ticker.labelSubtextFontSize === void 0 ? {} : { subtextFontSize: ticker.labelSubtextFontSize },
|
|
2132
2155
|
price: tickerPrice,
|
|
2133
2156
|
backgroundColor: ticker.labelBackgroundColor ?? tickerColor,
|
|
2134
2157
|
textColor: ticker.labelTextColor ?? "#0b1220",
|
|
@@ -2207,28 +2230,44 @@ function createChart(element, options = {}) {
|
|
|
2207
2230
|
}
|
|
2208
2231
|
if (priceAxisLabels.length > 0) {
|
|
2209
2232
|
const labelPaddingX = Math.max(4, labels.labelPaddingX);
|
|
2210
|
-
const
|
|
2233
|
+
const baseLabelHeight = Math.max(14, labels.labelHeight);
|
|
2211
2234
|
const labelRadius = Math.max(0, labels.borderRadius);
|
|
2212
|
-
|
|
2235
|
+
const priceLabelFontSize = Math.max(8, axis.fontSize);
|
|
2236
|
+
ctx.font = `${priceLabelFontSize}px ${mergedOptions.fontFamily}`;
|
|
2213
2237
|
const positionedLabels = priceAxisLabels.map((label) => {
|
|
2214
|
-
const
|
|
2238
|
+
const subtexts = (label.subtexts ?? []).map((value) => value.trim()).filter((value) => value.length > 0);
|
|
2239
|
+
const subtextFontSize = label.subtextFontSize !== void 0 && label.subtextFontSize > 0 ? Math.max(8, Math.round(label.subtextFontSize)) : priceLabelFontSize;
|
|
2240
|
+
const subtextLineGap = 5;
|
|
2241
|
+
const labelHeight = baseLabelHeight + (subtexts.length > 0 ? subtexts.length * subtextFontSize + subtexts.length * subtextLineGap : 0);
|
|
2242
|
+
const primaryWidth = label.text === formatPrice(label.price) ? getPriceLabelWidth(label.text, labelPaddingX) : Math.ceil(ctx.measureText(label.text).width) + labelPaddingX * 2;
|
|
2243
|
+
let subtextWidth = 0;
|
|
2244
|
+
if (subtexts.length > 0) {
|
|
2245
|
+
const baseFont = ctx.font;
|
|
2246
|
+
ctx.font = `${subtextFontSize}px ${mergedOptions.fontFamily}`;
|
|
2247
|
+
subtextWidth = Math.max(...subtexts.map((subtext) => Math.ceil(ctx.measureText(subtext).width) + labelPaddingX * 2));
|
|
2248
|
+
ctx.font = baseFont;
|
|
2249
|
+
}
|
|
2250
|
+
const labelTextWidth = Math.max(primaryWidth, subtextWidth);
|
|
2215
2251
|
return {
|
|
2216
2252
|
...label,
|
|
2253
|
+
subtexts,
|
|
2254
|
+
subtextFontSize,
|
|
2255
|
+
height: labelHeight,
|
|
2217
2256
|
width: labelTextWidth,
|
|
2218
2257
|
targetY: clamp(yFromPrice(label.price), chartTop + 1, chartBottom - 1) - labelHeight / 2,
|
|
2219
2258
|
y: 0
|
|
2220
2259
|
};
|
|
2221
2260
|
}).sort((a, b) => a.targetY - b.targetY || b.priority - a.priority);
|
|
2222
2261
|
const minY = chartTop;
|
|
2223
|
-
const maxY = chartBottom - labelHeight;
|
|
2224
2262
|
let cursorY = minY;
|
|
2225
2263
|
for (const label of positionedLabels) {
|
|
2264
|
+
const maxY = Math.max(minY, chartBottom - label.height);
|
|
2226
2265
|
label.y = labels.noOverlapping ? Math.max(clamp(label.targetY, minY, maxY), cursorY) : clamp(label.targetY, minY, maxY);
|
|
2227
|
-
cursorY = label.y +
|
|
2266
|
+
cursorY = label.y + label.height + 2;
|
|
2228
2267
|
}
|
|
2229
2268
|
if (labels.noOverlapping && positionedLabels.length > 0) {
|
|
2230
2269
|
const lastLabel = positionedLabels[positionedLabels.length - 1];
|
|
2231
|
-
const overflow = lastLabel ? lastLabel.y +
|
|
2270
|
+
const overflow = lastLabel ? lastLabel.y + lastLabel.height - chartBottom : 0;
|
|
2232
2271
|
if (overflow > 0) {
|
|
2233
2272
|
for (const label of positionedLabels) {
|
|
2234
2273
|
label.y = Math.max(minY, label.y - overflow);
|
|
@@ -2238,8 +2277,25 @@ function createChart(element, options = {}) {
|
|
|
2238
2277
|
const labelX = chartRight + 4;
|
|
2239
2278
|
for (const label of positionedLabels) {
|
|
2240
2279
|
ctx.fillStyle = label.backgroundColor;
|
|
2241
|
-
fillRoundedRect(Math.round(labelX), Math.round(label.y), label.width,
|
|
2242
|
-
|
|
2280
|
+
fillRoundedRect(Math.round(labelX), Math.round(label.y), label.width, label.height, labelRadius);
|
|
2281
|
+
const hasSubtexts = label.subtexts.length > 0;
|
|
2282
|
+
const primaryY = hasSubtexts ? label.y + baseLabelHeight / 2 - 1 : label.y + label.height / 2;
|
|
2283
|
+
drawText(label.text, labelX + labelPaddingX, primaryY, "left", "middle", label.textColor);
|
|
2284
|
+
if (hasSubtexts) {
|
|
2285
|
+
const baseFont = ctx.font;
|
|
2286
|
+
ctx.font = `${label.subtextFontSize}px ${mergedOptions.fontFamily}`;
|
|
2287
|
+
label.subtexts.forEach((subtext, index) => {
|
|
2288
|
+
drawText(
|
|
2289
|
+
subtext,
|
|
2290
|
+
labelX + labelPaddingX,
|
|
2291
|
+
label.y + baseLabelHeight + index * (label.subtextFontSize + 5) + label.subtextFontSize / 2,
|
|
2292
|
+
"left",
|
|
2293
|
+
"middle",
|
|
2294
|
+
label.subtextColor ?? label.textColor
|
|
2295
|
+
);
|
|
2296
|
+
});
|
|
2297
|
+
ctx.font = baseFont;
|
|
2298
|
+
}
|
|
2243
2299
|
}
|
|
2244
2300
|
}
|
|
2245
2301
|
for (const priceLine of priceLines) {
|
package/dist/index.d.cts
CHANGED
|
@@ -260,6 +260,11 @@ interface TickerLineOptions {
|
|
|
260
260
|
color?: string;
|
|
261
261
|
labelBackgroundColor?: string;
|
|
262
262
|
labelTextColor?: string;
|
|
263
|
+
labelSubtext?: string;
|
|
264
|
+
labelSubtexts?: Array<string | number>;
|
|
265
|
+
labelSubtextColor?: string;
|
|
266
|
+
labelSubtextFontSize?: number;
|
|
267
|
+
showCountdownInLabel?: boolean;
|
|
263
268
|
labelBorderRadius?: number;
|
|
264
269
|
smoothing?: boolean;
|
|
265
270
|
smoothingSpeed?: number;
|
package/dist/index.d.ts
CHANGED
|
@@ -260,6 +260,11 @@ interface TickerLineOptions {
|
|
|
260
260
|
color?: string;
|
|
261
261
|
labelBackgroundColor?: string;
|
|
262
262
|
labelTextColor?: string;
|
|
263
|
+
labelSubtext?: string;
|
|
264
|
+
labelSubtexts?: Array<string | number>;
|
|
265
|
+
labelSubtextColor?: string;
|
|
266
|
+
labelSubtextFontSize?: number;
|
|
267
|
+
showCountdownInLabel?: boolean;
|
|
263
268
|
labelBorderRadius?: number;
|
|
264
269
|
smoothing?: boolean;
|
|
265
270
|
smoothingSpeed?: number;
|
package/dist/index.js
CHANGED
|
@@ -177,6 +177,11 @@ var DEFAULT_OPTIONS = {
|
|
|
177
177
|
style: "dotted",
|
|
178
178
|
thickness: 1,
|
|
179
179
|
labelTextColor: "#0b1220",
|
|
180
|
+
labelSubtext: "",
|
|
181
|
+
labelSubtexts: [],
|
|
182
|
+
labelSubtextColor: "#0b1220",
|
|
183
|
+
labelSubtextFontSize: 0,
|
|
184
|
+
showCountdownInLabel: false,
|
|
180
185
|
labelBorderRadius: 3
|
|
181
186
|
},
|
|
182
187
|
labels: DEFAULT_LABELS_OPTIONS,
|
|
@@ -2079,6 +2084,16 @@ function createChart(element, options = {}) {
|
|
|
2079
2084
|
ctx.stroke();
|
|
2080
2085
|
ctx.restore();
|
|
2081
2086
|
};
|
|
2087
|
+
const getCountdownText = () => {
|
|
2088
|
+
const last = data[data.length - 1];
|
|
2089
|
+
if (!last) {
|
|
2090
|
+
return null;
|
|
2091
|
+
}
|
|
2092
|
+
const stepMs = getTimeStepMs();
|
|
2093
|
+
const rawRemainingMs = last.time.getTime() + stepMs - Date.now();
|
|
2094
|
+
const countdownMs = rawRemainingMs >= 0 && rawRemainingMs <= stepMs * 2 ? rawRemainingMs : stepMs - Date.now() % stepMs;
|
|
2095
|
+
return formatDuration(countdownMs);
|
|
2096
|
+
};
|
|
2082
2097
|
const ticker = mergedOptions.tickerLine ?? DEFAULT_OPTIONS.tickerLine;
|
|
2083
2098
|
const lastPoint = data[data.length - 1];
|
|
2084
2099
|
let tickerPrice = null;
|
|
@@ -2103,8 +2118,16 @@ function createChart(element, options = {}) {
|
|
|
2103
2118
|
ctx.restore();
|
|
2104
2119
|
}
|
|
2105
2120
|
if ((ticker.visible ?? true) && labels.showLastPrice && tickerPrice !== null && tickerColor !== null) {
|
|
2121
|
+
const tickerSubtexts = [
|
|
2122
|
+
...ticker.labelSubtext ? [ticker.labelSubtext] : [],
|
|
2123
|
+
...ticker.labelSubtexts ?? [],
|
|
2124
|
+
...ticker.showCountdownInLabel ? [getCountdownText()] : []
|
|
2125
|
+
].map((value) => value === null || value === void 0 ? "" : String(value).trim()).filter((value) => value.length > 0);
|
|
2106
2126
|
addPriceAxisLabel({
|
|
2107
2127
|
text: formatPrice(tickerPrice),
|
|
2128
|
+
...tickerSubtexts.length > 0 ? { subtexts: tickerSubtexts } : {},
|
|
2129
|
+
subtextColor: ticker.labelSubtextColor ?? ticker.labelTextColor ?? "#0b1220",
|
|
2130
|
+
...ticker.labelSubtextFontSize === void 0 ? {} : { subtextFontSize: ticker.labelSubtextFontSize },
|
|
2108
2131
|
price: tickerPrice,
|
|
2109
2132
|
backgroundColor: ticker.labelBackgroundColor ?? tickerColor,
|
|
2110
2133
|
textColor: ticker.labelTextColor ?? "#0b1220",
|
|
@@ -2183,28 +2206,44 @@ function createChart(element, options = {}) {
|
|
|
2183
2206
|
}
|
|
2184
2207
|
if (priceAxisLabels.length > 0) {
|
|
2185
2208
|
const labelPaddingX = Math.max(4, labels.labelPaddingX);
|
|
2186
|
-
const
|
|
2209
|
+
const baseLabelHeight = Math.max(14, labels.labelHeight);
|
|
2187
2210
|
const labelRadius = Math.max(0, labels.borderRadius);
|
|
2188
|
-
|
|
2211
|
+
const priceLabelFontSize = Math.max(8, axis.fontSize);
|
|
2212
|
+
ctx.font = `${priceLabelFontSize}px ${mergedOptions.fontFamily}`;
|
|
2189
2213
|
const positionedLabels = priceAxisLabels.map((label) => {
|
|
2190
|
-
const
|
|
2214
|
+
const subtexts = (label.subtexts ?? []).map((value) => value.trim()).filter((value) => value.length > 0);
|
|
2215
|
+
const subtextFontSize = label.subtextFontSize !== void 0 && label.subtextFontSize > 0 ? Math.max(8, Math.round(label.subtextFontSize)) : priceLabelFontSize;
|
|
2216
|
+
const subtextLineGap = 5;
|
|
2217
|
+
const labelHeight = baseLabelHeight + (subtexts.length > 0 ? subtexts.length * subtextFontSize + subtexts.length * subtextLineGap : 0);
|
|
2218
|
+
const primaryWidth = label.text === formatPrice(label.price) ? getPriceLabelWidth(label.text, labelPaddingX) : Math.ceil(ctx.measureText(label.text).width) + labelPaddingX * 2;
|
|
2219
|
+
let subtextWidth = 0;
|
|
2220
|
+
if (subtexts.length > 0) {
|
|
2221
|
+
const baseFont = ctx.font;
|
|
2222
|
+
ctx.font = `${subtextFontSize}px ${mergedOptions.fontFamily}`;
|
|
2223
|
+
subtextWidth = Math.max(...subtexts.map((subtext) => Math.ceil(ctx.measureText(subtext).width) + labelPaddingX * 2));
|
|
2224
|
+
ctx.font = baseFont;
|
|
2225
|
+
}
|
|
2226
|
+
const labelTextWidth = Math.max(primaryWidth, subtextWidth);
|
|
2191
2227
|
return {
|
|
2192
2228
|
...label,
|
|
2229
|
+
subtexts,
|
|
2230
|
+
subtextFontSize,
|
|
2231
|
+
height: labelHeight,
|
|
2193
2232
|
width: labelTextWidth,
|
|
2194
2233
|
targetY: clamp(yFromPrice(label.price), chartTop + 1, chartBottom - 1) - labelHeight / 2,
|
|
2195
2234
|
y: 0
|
|
2196
2235
|
};
|
|
2197
2236
|
}).sort((a, b) => a.targetY - b.targetY || b.priority - a.priority);
|
|
2198
2237
|
const minY = chartTop;
|
|
2199
|
-
const maxY = chartBottom - labelHeight;
|
|
2200
2238
|
let cursorY = minY;
|
|
2201
2239
|
for (const label of positionedLabels) {
|
|
2240
|
+
const maxY = Math.max(minY, chartBottom - label.height);
|
|
2202
2241
|
label.y = labels.noOverlapping ? Math.max(clamp(label.targetY, minY, maxY), cursorY) : clamp(label.targetY, minY, maxY);
|
|
2203
|
-
cursorY = label.y +
|
|
2242
|
+
cursorY = label.y + label.height + 2;
|
|
2204
2243
|
}
|
|
2205
2244
|
if (labels.noOverlapping && positionedLabels.length > 0) {
|
|
2206
2245
|
const lastLabel = positionedLabels[positionedLabels.length - 1];
|
|
2207
|
-
const overflow = lastLabel ? lastLabel.y +
|
|
2246
|
+
const overflow = lastLabel ? lastLabel.y + lastLabel.height - chartBottom : 0;
|
|
2208
2247
|
if (overflow > 0) {
|
|
2209
2248
|
for (const label of positionedLabels) {
|
|
2210
2249
|
label.y = Math.max(minY, label.y - overflow);
|
|
@@ -2214,8 +2253,25 @@ function createChart(element, options = {}) {
|
|
|
2214
2253
|
const labelX = chartRight + 4;
|
|
2215
2254
|
for (const label of positionedLabels) {
|
|
2216
2255
|
ctx.fillStyle = label.backgroundColor;
|
|
2217
|
-
fillRoundedRect(Math.round(labelX), Math.round(label.y), label.width,
|
|
2218
|
-
|
|
2256
|
+
fillRoundedRect(Math.round(labelX), Math.round(label.y), label.width, label.height, labelRadius);
|
|
2257
|
+
const hasSubtexts = label.subtexts.length > 0;
|
|
2258
|
+
const primaryY = hasSubtexts ? label.y + baseLabelHeight / 2 - 1 : label.y + label.height / 2;
|
|
2259
|
+
drawText(label.text, labelX + labelPaddingX, primaryY, "left", "middle", label.textColor);
|
|
2260
|
+
if (hasSubtexts) {
|
|
2261
|
+
const baseFont = ctx.font;
|
|
2262
|
+
ctx.font = `${label.subtextFontSize}px ${mergedOptions.fontFamily}`;
|
|
2263
|
+
label.subtexts.forEach((subtext, index) => {
|
|
2264
|
+
drawText(
|
|
2265
|
+
subtext,
|
|
2266
|
+
labelX + labelPaddingX,
|
|
2267
|
+
label.y + baseLabelHeight + index * (label.subtextFontSize + 5) + label.subtextFontSize / 2,
|
|
2268
|
+
"left",
|
|
2269
|
+
"middle",
|
|
2270
|
+
label.subtextColor ?? label.textColor
|
|
2271
|
+
);
|
|
2272
|
+
});
|
|
2273
|
+
ctx.font = baseFont;
|
|
2274
|
+
}
|
|
2219
2275
|
}
|
|
2220
2276
|
}
|
|
2221
2277
|
for (const priceLine of priceLines) {
|
package/docs/API.md
CHANGED
|
@@ -149,6 +149,11 @@ watermark: {
|
|
|
149
149
|
- `color` (default `#38bdf8`)
|
|
150
150
|
- `labelBackgroundColor` (default `#38bdf8`)
|
|
151
151
|
- `labelTextColor` (default `#0b1220`)
|
|
152
|
+
- `labelSubtext` (optional single extra line inside the current-price label, for example `"MNQ"`)
|
|
153
|
+
- `labelSubtexts` (optional array of extra lines inside the current-price label, for example `["MNQ", "RTH"]`)
|
|
154
|
+
- `labelSubtextColor` (defaults to the label text color)
|
|
155
|
+
- `labelSubtextFontSize` (default `0`, meaning each extra line uses the same font size as the price)
|
|
156
|
+
- `showCountdownInLabel` (default `false`; appends bar-close countdown as another extra line)
|
|
152
157
|
- `labelBorderRadius` (default `3`)
|
|
153
158
|
|
|
154
159
|
### `LabelsOptions`
|