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 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 labelHeight = Math.max(14, labels.labelHeight);
2233
+ const baseLabelHeight = Math.max(14, labels.labelHeight);
2211
2234
  const labelRadius = Math.max(0, labels.borderRadius);
2212
- ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
2235
+ const priceLabelFontSize = Math.max(8, axis.fontSize);
2236
+ ctx.font = `${priceLabelFontSize}px ${mergedOptions.fontFamily}`;
2213
2237
  const positionedLabels = priceAxisLabels.map((label) => {
2214
- const labelTextWidth = label.text === formatPrice(label.price) ? getPriceLabelWidth(label.text, labelPaddingX) : Math.ceil(ctx.measureText(label.text).width) + labelPaddingX * 2;
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 + labelHeight + 2;
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 + labelHeight - maxY : 0;
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, labelHeight, labelRadius);
2242
- drawText(label.text, labelX + labelPaddingX, label.y + labelHeight / 2, "left", "middle", label.textColor);
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 labelHeight = Math.max(14, labels.labelHeight);
2209
+ const baseLabelHeight = Math.max(14, labels.labelHeight);
2187
2210
  const labelRadius = Math.max(0, labels.borderRadius);
2188
- ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
2211
+ const priceLabelFontSize = Math.max(8, axis.fontSize);
2212
+ ctx.font = `${priceLabelFontSize}px ${mergedOptions.fontFamily}`;
2189
2213
  const positionedLabels = priceAxisLabels.map((label) => {
2190
- const labelTextWidth = label.text === formatPrice(label.price) ? getPriceLabelWidth(label.text, labelPaddingX) : Math.ceil(ctx.measureText(label.text).width) + labelPaddingX * 2;
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 + labelHeight + 2;
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 + labelHeight - maxY : 0;
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, labelHeight, labelRadius);
2218
- drawText(label.text, labelX + labelPaddingX, label.y + labelHeight / 2, "left", "middle", label.textColor);
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 labelHeight = Math.max(14, labels.labelHeight);
2233
+ const baseLabelHeight = Math.max(14, labels.labelHeight);
2211
2234
  const labelRadius = Math.max(0, labels.borderRadius);
2212
- ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
2235
+ const priceLabelFontSize = Math.max(8, axis.fontSize);
2236
+ ctx.font = `${priceLabelFontSize}px ${mergedOptions.fontFamily}`;
2213
2237
  const positionedLabels = priceAxisLabels.map((label) => {
2214
- const labelTextWidth = label.text === formatPrice(label.price) ? getPriceLabelWidth(label.text, labelPaddingX) : Math.ceil(ctx.measureText(label.text).width) + labelPaddingX * 2;
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 + labelHeight + 2;
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 + labelHeight - maxY : 0;
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, labelHeight, labelRadius);
2242
- drawText(label.text, labelX + labelPaddingX, label.y + labelHeight / 2, "left", "middle", label.textColor);
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 labelHeight = Math.max(14, labels.labelHeight);
2209
+ const baseLabelHeight = Math.max(14, labels.labelHeight);
2187
2210
  const labelRadius = Math.max(0, labels.borderRadius);
2188
- ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
2211
+ const priceLabelFontSize = Math.max(8, axis.fontSize);
2212
+ ctx.font = `${priceLabelFontSize}px ${mergedOptions.fontFamily}`;
2189
2213
  const positionedLabels = priceAxisLabels.map((label) => {
2190
- const labelTextWidth = label.text === formatPrice(label.price) ? getPriceLabelWidth(label.text, labelPaddingX) : Math.ceil(ctx.measureText(label.text).width) + labelPaddingX * 2;
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 + labelHeight + 2;
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 + labelHeight - maxY : 0;
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, labelHeight, labelRadius);
2218
- drawText(label.text, labelX + labelPaddingX, label.y + labelHeight / 2, "left", "middle", label.textColor);
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`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hyperprop-charting-library",
3
- "version": "0.1.45",
3
+ "version": "0.1.47",
4
4
  "description": "Lightweight TypeScript charting core",
5
5
  "type": "module",
6
6
  "main": "./dist/hyperprop-charting-library.cjs",