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