hyperprop-charting-library 0.1.44 → 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.
@@ -61,6 +61,35 @@ var DEFAULT_DASH_PATTERNS = {
61
61
  borderDotted: [2, 2],
62
62
  borderDashed: [6, 4]
63
63
  };
64
+ var DEFAULT_LABELS_OPTIONS = {
65
+ visible: true,
66
+ symbolName: "",
67
+ showSymbolName: false,
68
+ showLastPrice: true,
69
+ showPreviousClose: false,
70
+ previousClosePrice: Number.NaN,
71
+ showHighLow: false,
72
+ showBidAsk: false,
73
+ bidPrice: Number.NaN,
74
+ askPrice: Number.NaN,
75
+ showIndicatorNames: false,
76
+ showIndicatorValues: false,
77
+ showCountdownToBarClose: false,
78
+ noOverlapping: true,
79
+ backgroundColor: "#0b1220",
80
+ textColor: "#cbd5e1",
81
+ mutedTextColor: "#94a3b8",
82
+ symbolNameBackgroundColor: "#1f2937",
83
+ symbolNameTextColor: "#e5e7eb",
84
+ previousCloseColor: "#94a3b8",
85
+ highLowColor: "#a78bfa",
86
+ bidColor: "#ef4444",
87
+ askColor: "#22c55e",
88
+ indicatorTextColor: "#cbd5e1",
89
+ borderRadius: 3,
90
+ labelHeight: 20,
91
+ labelPaddingX: 8
92
+ };
64
93
  var DEFAULT_PRICE_LINE_OPTIONS = {
65
94
  visible: true,
66
95
  style: "solid",
@@ -148,8 +177,13 @@ var DEFAULT_OPTIONS = {
148
177
  style: "dotted",
149
178
  thickness: 1,
150
179
  labelTextColor: "#0b1220",
180
+ labelSubtext: "",
181
+ labelSubtextColor: "#0b1220",
182
+ labelSubtextFontSize: 0,
183
+ showCountdownInLabel: false,
151
184
  labelBorderRadius: 3
152
185
  },
186
+ labels: DEFAULT_LABELS_OPTIONS,
153
187
  dashPatterns: DEFAULT_DASH_PATTERNS,
154
188
  indicators: []
155
189
  };
@@ -189,6 +223,10 @@ var mergeChartOptions = (baseOptions, options = {}) => ({
189
223
  ...baseOptions.tickerLine,
190
224
  ...options.tickerLine ?? {}
191
225
  },
226
+ labels: {
227
+ ...baseOptions.labels,
228
+ ...options.labels ?? {}
229
+ },
192
230
  dashPatterns: {
193
231
  ...baseOptions.dashPatterns,
194
232
  ...options.dashPatterns ?? {}
@@ -1255,6 +1293,14 @@ function createChart(element, options = {}) {
1255
1293
  ctx.textBaseline = baseline;
1256
1294
  ctx.fillText(text, x, y);
1257
1295
  };
1296
+ const formatDuration = (ms) => {
1297
+ const totalSeconds = Math.max(0, Math.floor(ms / 1e3));
1298
+ const hours = Math.floor(totalSeconds / 3600);
1299
+ const minutes = Math.floor(totalSeconds % 3600 / 60);
1300
+ const seconds = totalSeconds % 60;
1301
+ const pad = (value) => String(value).padStart(2, "0");
1302
+ return hours > 0 ? `${hours}:${pad(minutes)}:${pad(seconds)}` : `${pad(minutes)}:${pad(seconds)}`;
1303
+ };
1258
1304
  const drawPriceLine = (line, yFromPrice, chartLeft, chartTop, chartRight, chartBottom) => {
1259
1305
  const mergedLine = { ...DEFAULT_PRICE_LINE_OPTIONS, ...line };
1260
1306
  if (!mergedLine.visible || !Number.isFinite(mergedLine.price)) {
@@ -2014,14 +2060,49 @@ function createChart(element, options = {}) {
2014
2060
  drawText(formatPrice(price), chartRight + 6, y, "left", "middle", yAxis.textColor);
2015
2061
  ctx.font = prevFont;
2016
2062
  }
2063
+ const labels = { ...DEFAULT_LABELS_OPTIONS, ...mergedOptions.labels ?? {} };
2064
+ const priceAxisLabels = [];
2065
+ const addPriceAxisLabel = (label) => {
2066
+ if (!labels.visible || !Number.isFinite(label.price) || label.text.length === 0) {
2067
+ return;
2068
+ }
2069
+ priceAxisLabels.push(label);
2070
+ };
2071
+ const drawReferenceLine = (price, color, style = "dotted") => {
2072
+ if (!Number.isFinite(price)) {
2073
+ return;
2074
+ }
2075
+ const y = clamp(yFromPrice(price), chartTop + 1, chartBottom - 1);
2076
+ ctx.save();
2077
+ ctx.strokeStyle = color;
2078
+ ctx.lineWidth = 1;
2079
+ applyDashPattern(style, dashPatterns.dotted, dashPatterns.dashed);
2080
+ ctx.beginPath();
2081
+ ctx.moveTo(crisp(chartLeft), crisp(y));
2082
+ ctx.lineTo(crisp(chartRight), crisp(y));
2083
+ ctx.stroke();
2084
+ ctx.restore();
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
+ };
2017
2096
  const ticker = mergedOptions.tickerLine ?? DEFAULT_OPTIONS.tickerLine;
2018
2097
  const lastPoint = data[data.length - 1];
2098
+ let tickerPrice = null;
2099
+ let tickerColor = null;
2019
2100
  if ((ticker.visible ?? true) && lastPoint) {
2020
- const tickerPrice = ticker.smoothing && smoothedTickerPrice !== null ? smoothedTickerPrice : lastPoint.c;
2101
+ tickerPrice = ticker.smoothing && smoothedTickerPrice !== null ? smoothedTickerPrice : lastPoint.c;
2021
2102
  const tickerY = yFromPrice(tickerPrice);
2022
2103
  const lineY = clamp(tickerY, chartTop + 1, chartBottom - 1);
2023
2104
  const lastDirection = ticker.smoothing && smoothedTickerPrice !== null ? smoothedTickerPrice >= lastPoint.o ? "up" : "down" : getCandleDirectionByIndex(data.length - 1);
2024
- const tickerColor = ticker.color ?? (lastDirection === "up" ? mergedOptions.upColor : mergedOptions.downColor);
2105
+ tickerColor = ticker.color ?? (lastDirection === "up" ? mergedOptions.upColor : mergedOptions.downColor);
2025
2106
  const tickerThickness = Math.max(1, ticker.thickness ?? 1);
2026
2107
  const tickerStyle = ticker.style ?? "solid";
2027
2108
  ctx.save();
@@ -2034,24 +2115,155 @@ function createChart(element, options = {}) {
2034
2115
  ctx.stroke();
2035
2116
  ctx.setLineDash([]);
2036
2117
  ctx.restore();
2037
- const tickerLabel = formatPrice(tickerPrice);
2038
- ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
2039
- const labelPaddingX = 8;
2040
- const labelHeight = 20;
2041
- const labelWidth = getPriceLabelWidth(tickerLabel, labelPaddingX);
2118
+ }
2119
+ if ((ticker.visible ?? true) && labels.showLastPrice && tickerPrice !== null && tickerColor !== null) {
2120
+ const tickerSubtext = ticker.showCountdownInLabel ? getCountdownText() : ticker.labelSubtext?.trim();
2121
+ addPriceAxisLabel({
2122
+ text: formatPrice(tickerPrice),
2123
+ ...tickerSubtext ? { subtext: tickerSubtext } : {},
2124
+ subtextColor: ticker.labelSubtextColor ?? ticker.labelTextColor ?? "#0b1220",
2125
+ ...ticker.labelSubtextFontSize === void 0 ? {} : { subtextFontSize: ticker.labelSubtextFontSize },
2126
+ price: tickerPrice,
2127
+ backgroundColor: ticker.labelBackgroundColor ?? tickerColor,
2128
+ textColor: ticker.labelTextColor ?? "#0b1220",
2129
+ color: tickerColor,
2130
+ priority: 100
2131
+ });
2132
+ }
2133
+ if (labels.showSymbolName && labels.symbolName.trim().length > 0 && tickerPrice !== null) {
2134
+ addPriceAxisLabel({
2135
+ text: labels.symbolName.trim(),
2136
+ price: tickerPrice,
2137
+ backgroundColor: labels.symbolNameBackgroundColor,
2138
+ textColor: labels.symbolNameTextColor,
2139
+ color: labels.symbolNameBackgroundColor,
2140
+ priority: 95
2141
+ });
2142
+ }
2143
+ if (labels.showPreviousClose) {
2144
+ const previousCloseCandidate = Number.isFinite(labels.previousClosePrice) ? labels.previousClosePrice : data.length > 1 ? data[data.length - 2]?.c : Number.NaN;
2145
+ if (previousCloseCandidate !== void 0 && Number.isFinite(previousCloseCandidate)) {
2146
+ const previousClose = previousCloseCandidate;
2147
+ drawReferenceLine(previousClose, labels.previousCloseColor, "dashed");
2148
+ addPriceAxisLabel({
2149
+ text: `PDC ${formatPrice(previousClose)}`,
2150
+ price: previousClose,
2151
+ backgroundColor: labels.backgroundColor,
2152
+ textColor: labels.mutedTextColor,
2153
+ color: labels.previousCloseColor,
2154
+ priority: 50
2155
+ });
2156
+ }
2157
+ }
2158
+ if (labels.showHighLow && visibleData.length > 0) {
2159
+ const visibleHigh = Math.max(...visibleData.map((point) => point.h));
2160
+ const visibleLow = Math.min(...visibleData.map((point) => point.l));
2161
+ addPriceAxisLabel({
2162
+ text: `H ${formatPrice(visibleHigh)}`,
2163
+ price: visibleHigh,
2164
+ backgroundColor: labels.backgroundColor,
2165
+ textColor: labels.textColor,
2166
+ color: labels.highLowColor,
2167
+ priority: 40
2168
+ });
2169
+ addPriceAxisLabel({
2170
+ text: `L ${formatPrice(visibleLow)}`,
2171
+ price: visibleLow,
2172
+ backgroundColor: labels.backgroundColor,
2173
+ textColor: labels.textColor,
2174
+ color: labels.highLowColor,
2175
+ priority: 40
2176
+ });
2177
+ }
2178
+ if (labels.showBidAsk) {
2179
+ if (Number.isFinite(labels.bidPrice)) {
2180
+ drawReferenceLine(labels.bidPrice, labels.bidColor, "dotted");
2181
+ addPriceAxisLabel({
2182
+ text: `B ${formatPrice(labels.bidPrice)}`,
2183
+ price: labels.bidPrice,
2184
+ backgroundColor: labels.bidColor,
2185
+ textColor: "#0b1220",
2186
+ color: labels.bidColor,
2187
+ priority: 80
2188
+ });
2189
+ }
2190
+ if (Number.isFinite(labels.askPrice)) {
2191
+ drawReferenceLine(labels.askPrice, labels.askColor, "dotted");
2192
+ addPriceAxisLabel({
2193
+ text: `A ${formatPrice(labels.askPrice)}`,
2194
+ price: labels.askPrice,
2195
+ backgroundColor: labels.askColor,
2196
+ textColor: "#0b1220",
2197
+ color: labels.askColor,
2198
+ priority: 80
2199
+ });
2200
+ }
2201
+ }
2202
+ if (priceAxisLabels.length > 0) {
2203
+ const labelPaddingX = Math.max(4, labels.labelPaddingX);
2204
+ const baseLabelHeight = Math.max(14, labels.labelHeight);
2205
+ const labelRadius = Math.max(0, labels.borderRadius);
2206
+ const priceLabelFontSize = Math.max(8, axis.fontSize);
2207
+ ctx.font = `${priceLabelFontSize}px ${mergedOptions.fontFamily}`;
2208
+ const positionedLabels = priceAxisLabels.map((label) => {
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);
2221
+ return {
2222
+ ...label,
2223
+ subtext,
2224
+ subtextFontSize,
2225
+ height: labelHeight,
2226
+ width: labelTextWidth,
2227
+ targetY: clamp(yFromPrice(label.price), chartTop + 1, chartBottom - 1) - labelHeight / 2,
2228
+ y: 0
2229
+ };
2230
+ }).sort((a, b) => a.targetY - b.targetY || b.priority - a.priority);
2231
+ const minY = chartTop;
2232
+ let cursorY = minY;
2233
+ for (const label of positionedLabels) {
2234
+ const maxY = chartBottom - label.height;
2235
+ label.y = labels.noOverlapping ? Math.max(clamp(label.targetY, minY, maxY), cursorY) : clamp(label.targetY, minY, maxY);
2236
+ cursorY = label.y + label.height + 2;
2237
+ }
2238
+ if (labels.noOverlapping && positionedLabels.length > 0) {
2239
+ const lastLabel = positionedLabels[positionedLabels.length - 1];
2240
+ const overflow = lastLabel ? lastLabel.y + lastLabel.height - chartBottom : 0;
2241
+ if (overflow > 0) {
2242
+ for (const label of positionedLabels) {
2243
+ label.y = Math.max(minY, label.y - overflow);
2244
+ }
2245
+ }
2246
+ }
2042
2247
  const labelX = chartRight + 4;
2043
- const labelY = clamp(lineY - labelHeight / 2, chartTop, chartBottom - labelHeight);
2044
- const labelRadius = Math.max(0, ticker.labelBorderRadius ?? 0);
2045
- ctx.fillStyle = ticker.labelBackgroundColor ?? tickerColor;
2046
- fillRoundedRect(Math.round(labelX), Math.round(labelY), labelWidth, labelHeight, labelRadius);
2047
- drawText(
2048
- tickerLabel,
2049
- labelX + labelPaddingX,
2050
- labelY + labelHeight / 2,
2051
- "left",
2052
- "middle",
2053
- ticker.labelTextColor ?? "#0b1220"
2054
- );
2248
+ for (const label of positionedLabels) {
2249
+ ctx.fillStyle = label.backgroundColor;
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
+ }
2266
+ }
2055
2267
  }
2056
2268
  for (const priceLine of priceLines) {
2057
2269
  drawPriceLine(priceLine, yFromPrice, chartLeft, chartTop, chartRight, chartBottom);
@@ -2059,6 +2271,25 @@ function createChart(element, options = {}) {
2059
2271
  for (const orderLine of orderLines) {
2060
2272
  drawOrderLine(orderLine, yFromPrice, chartLeft, chartTop, chartRight, chartBottom);
2061
2273
  }
2274
+ if (labels.visible && (labels.showIndicatorNames || labels.showIndicatorValues)) {
2275
+ const labelEntries = [...activeOverlayIndicators, ...activeSeparateIndicators].map(({ indicator, plugin }) => {
2276
+ const inputValues = Object.entries(indicator.inputs).filter(([, value]) => typeof value === "number" || typeof value === "string" || typeof value === "boolean").slice(0, 2).map(([, value]) => String(value));
2277
+ if (labels.showIndicatorNames && labels.showIndicatorValues && inputValues.length > 0) {
2278
+ return `${plugin.name} ${inputValues.join(" ")}`;
2279
+ }
2280
+ if (labels.showIndicatorNames) {
2281
+ return plugin.name;
2282
+ }
2283
+ return inputValues.join(" ");
2284
+ }).filter((entry) => entry.length > 0);
2285
+ if (labelEntries.length > 0) {
2286
+ const prevFont = ctx.font;
2287
+ ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
2288
+ const legendText = labelEntries.join(" ");
2289
+ drawText(legendText, chartLeft + 10, chartTop + 10, "left", "top", labels.indicatorTextColor);
2290
+ ctx.font = prevFont;
2291
+ }
2292
+ }
2062
2293
  for (let index = tickStartIndex; index <= visibleTickEnd; index += xStep) {
2063
2294
  const tickTime = getTimeForIndex(index);
2064
2295
  if (!tickTime) {
@@ -2074,6 +2305,16 @@ function createChart(element, options = {}) {
2074
2305
  drawText(timeLabel, x, fullChartBottom + 8, "center", "top", xAxis.textColor);
2075
2306
  ctx.font = prevFont;
2076
2307
  }
2308
+ if (labels.visible && labels.showCountdownToBarClose && lastPoint) {
2309
+ const stepMs = getTimeStepMs();
2310
+ const rawRemainingMs = lastPoint.time.getTime() + stepMs - Date.now();
2311
+ const countdownMs = rawRemainingMs >= 0 && rawRemainingMs <= stepMs * 2 ? rawRemainingMs : stepMs - Date.now() % stepMs;
2312
+ const countdownText = formatDuration(countdownMs);
2313
+ const prevFont = ctx.font;
2314
+ ctx.font = `${xAxisFontSize}px ${mergedOptions.fontFamily}`;
2315
+ drawText(countdownText, chartRight + 6, fullChartBottom + 8, "left", "top", xAxis.textColor);
2316
+ ctx.font = prevFont;
2317
+ }
2077
2318
  if (crosshair.visible && crosshairPoint) {
2078
2319
  const cx = clamp(crosshairPoint.x, chartLeft, chartRight);
2079
2320
  const cy = clamp(crosshairPoint.y, chartTop, chartBottom);
package/dist/index.cjs CHANGED
@@ -85,6 +85,35 @@ var DEFAULT_DASH_PATTERNS = {
85
85
  borderDotted: [2, 2],
86
86
  borderDashed: [6, 4]
87
87
  };
88
+ var DEFAULT_LABELS_OPTIONS = {
89
+ visible: true,
90
+ symbolName: "",
91
+ showSymbolName: false,
92
+ showLastPrice: true,
93
+ showPreviousClose: false,
94
+ previousClosePrice: Number.NaN,
95
+ showHighLow: false,
96
+ showBidAsk: false,
97
+ bidPrice: Number.NaN,
98
+ askPrice: Number.NaN,
99
+ showIndicatorNames: false,
100
+ showIndicatorValues: false,
101
+ showCountdownToBarClose: false,
102
+ noOverlapping: true,
103
+ backgroundColor: "#0b1220",
104
+ textColor: "#cbd5e1",
105
+ mutedTextColor: "#94a3b8",
106
+ symbolNameBackgroundColor: "#1f2937",
107
+ symbolNameTextColor: "#e5e7eb",
108
+ previousCloseColor: "#94a3b8",
109
+ highLowColor: "#a78bfa",
110
+ bidColor: "#ef4444",
111
+ askColor: "#22c55e",
112
+ indicatorTextColor: "#cbd5e1",
113
+ borderRadius: 3,
114
+ labelHeight: 20,
115
+ labelPaddingX: 8
116
+ };
88
117
  var DEFAULT_PRICE_LINE_OPTIONS = {
89
118
  visible: true,
90
119
  style: "solid",
@@ -172,8 +201,13 @@ var DEFAULT_OPTIONS = {
172
201
  style: "dotted",
173
202
  thickness: 1,
174
203
  labelTextColor: "#0b1220",
204
+ labelSubtext: "",
205
+ labelSubtextColor: "#0b1220",
206
+ labelSubtextFontSize: 0,
207
+ showCountdownInLabel: false,
175
208
  labelBorderRadius: 3
176
209
  },
210
+ labels: DEFAULT_LABELS_OPTIONS,
177
211
  dashPatterns: DEFAULT_DASH_PATTERNS,
178
212
  indicators: []
179
213
  };
@@ -213,6 +247,10 @@ var mergeChartOptions = (baseOptions, options = {}) => ({
213
247
  ...baseOptions.tickerLine,
214
248
  ...options.tickerLine ?? {}
215
249
  },
250
+ labels: {
251
+ ...baseOptions.labels,
252
+ ...options.labels ?? {}
253
+ },
216
254
  dashPatterns: {
217
255
  ...baseOptions.dashPatterns,
218
256
  ...options.dashPatterns ?? {}
@@ -1279,6 +1317,14 @@ function createChart(element, options = {}) {
1279
1317
  ctx.textBaseline = baseline;
1280
1318
  ctx.fillText(text, x, y);
1281
1319
  };
1320
+ const formatDuration = (ms) => {
1321
+ const totalSeconds = Math.max(0, Math.floor(ms / 1e3));
1322
+ const hours = Math.floor(totalSeconds / 3600);
1323
+ const minutes = Math.floor(totalSeconds % 3600 / 60);
1324
+ const seconds = totalSeconds % 60;
1325
+ const pad = (value) => String(value).padStart(2, "0");
1326
+ return hours > 0 ? `${hours}:${pad(minutes)}:${pad(seconds)}` : `${pad(minutes)}:${pad(seconds)}`;
1327
+ };
1282
1328
  const drawPriceLine = (line, yFromPrice, chartLeft, chartTop, chartRight, chartBottom) => {
1283
1329
  const mergedLine = { ...DEFAULT_PRICE_LINE_OPTIONS, ...line };
1284
1330
  if (!mergedLine.visible || !Number.isFinite(mergedLine.price)) {
@@ -2038,14 +2084,49 @@ function createChart(element, options = {}) {
2038
2084
  drawText(formatPrice(price), chartRight + 6, y, "left", "middle", yAxis.textColor);
2039
2085
  ctx.font = prevFont;
2040
2086
  }
2087
+ const labels = { ...DEFAULT_LABELS_OPTIONS, ...mergedOptions.labels ?? {} };
2088
+ const priceAxisLabels = [];
2089
+ const addPriceAxisLabel = (label) => {
2090
+ if (!labels.visible || !Number.isFinite(label.price) || label.text.length === 0) {
2091
+ return;
2092
+ }
2093
+ priceAxisLabels.push(label);
2094
+ };
2095
+ const drawReferenceLine = (price, color, style = "dotted") => {
2096
+ if (!Number.isFinite(price)) {
2097
+ return;
2098
+ }
2099
+ const y = clamp(yFromPrice(price), chartTop + 1, chartBottom - 1);
2100
+ ctx.save();
2101
+ ctx.strokeStyle = color;
2102
+ ctx.lineWidth = 1;
2103
+ applyDashPattern(style, dashPatterns.dotted, dashPatterns.dashed);
2104
+ ctx.beginPath();
2105
+ ctx.moveTo(crisp(chartLeft), crisp(y));
2106
+ ctx.lineTo(crisp(chartRight), crisp(y));
2107
+ ctx.stroke();
2108
+ ctx.restore();
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
+ };
2041
2120
  const ticker = mergedOptions.tickerLine ?? DEFAULT_OPTIONS.tickerLine;
2042
2121
  const lastPoint = data[data.length - 1];
2122
+ let tickerPrice = null;
2123
+ let tickerColor = null;
2043
2124
  if ((ticker.visible ?? true) && lastPoint) {
2044
- const tickerPrice = ticker.smoothing && smoothedTickerPrice !== null ? smoothedTickerPrice : lastPoint.c;
2125
+ tickerPrice = ticker.smoothing && smoothedTickerPrice !== null ? smoothedTickerPrice : lastPoint.c;
2045
2126
  const tickerY = yFromPrice(tickerPrice);
2046
2127
  const lineY = clamp(tickerY, chartTop + 1, chartBottom - 1);
2047
2128
  const lastDirection = ticker.smoothing && smoothedTickerPrice !== null ? smoothedTickerPrice >= lastPoint.o ? "up" : "down" : getCandleDirectionByIndex(data.length - 1);
2048
- const tickerColor = ticker.color ?? (lastDirection === "up" ? mergedOptions.upColor : mergedOptions.downColor);
2129
+ tickerColor = ticker.color ?? (lastDirection === "up" ? mergedOptions.upColor : mergedOptions.downColor);
2049
2130
  const tickerThickness = Math.max(1, ticker.thickness ?? 1);
2050
2131
  const tickerStyle = ticker.style ?? "solid";
2051
2132
  ctx.save();
@@ -2058,24 +2139,155 @@ function createChart(element, options = {}) {
2058
2139
  ctx.stroke();
2059
2140
  ctx.setLineDash([]);
2060
2141
  ctx.restore();
2061
- const tickerLabel = formatPrice(tickerPrice);
2062
- ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
2063
- const labelPaddingX = 8;
2064
- const labelHeight = 20;
2065
- const labelWidth = getPriceLabelWidth(tickerLabel, labelPaddingX);
2142
+ }
2143
+ if ((ticker.visible ?? true) && labels.showLastPrice && tickerPrice !== null && tickerColor !== null) {
2144
+ const tickerSubtext = ticker.showCountdownInLabel ? getCountdownText() : ticker.labelSubtext?.trim();
2145
+ addPriceAxisLabel({
2146
+ text: formatPrice(tickerPrice),
2147
+ ...tickerSubtext ? { subtext: tickerSubtext } : {},
2148
+ subtextColor: ticker.labelSubtextColor ?? ticker.labelTextColor ?? "#0b1220",
2149
+ ...ticker.labelSubtextFontSize === void 0 ? {} : { subtextFontSize: ticker.labelSubtextFontSize },
2150
+ price: tickerPrice,
2151
+ backgroundColor: ticker.labelBackgroundColor ?? tickerColor,
2152
+ textColor: ticker.labelTextColor ?? "#0b1220",
2153
+ color: tickerColor,
2154
+ priority: 100
2155
+ });
2156
+ }
2157
+ if (labels.showSymbolName && labels.symbolName.trim().length > 0 && tickerPrice !== null) {
2158
+ addPriceAxisLabel({
2159
+ text: labels.symbolName.trim(),
2160
+ price: tickerPrice,
2161
+ backgroundColor: labels.symbolNameBackgroundColor,
2162
+ textColor: labels.symbolNameTextColor,
2163
+ color: labels.symbolNameBackgroundColor,
2164
+ priority: 95
2165
+ });
2166
+ }
2167
+ if (labels.showPreviousClose) {
2168
+ const previousCloseCandidate = Number.isFinite(labels.previousClosePrice) ? labels.previousClosePrice : data.length > 1 ? data[data.length - 2]?.c : Number.NaN;
2169
+ if (previousCloseCandidate !== void 0 && Number.isFinite(previousCloseCandidate)) {
2170
+ const previousClose = previousCloseCandidate;
2171
+ drawReferenceLine(previousClose, labels.previousCloseColor, "dashed");
2172
+ addPriceAxisLabel({
2173
+ text: `PDC ${formatPrice(previousClose)}`,
2174
+ price: previousClose,
2175
+ backgroundColor: labels.backgroundColor,
2176
+ textColor: labels.mutedTextColor,
2177
+ color: labels.previousCloseColor,
2178
+ priority: 50
2179
+ });
2180
+ }
2181
+ }
2182
+ if (labels.showHighLow && visibleData.length > 0) {
2183
+ const visibleHigh = Math.max(...visibleData.map((point) => point.h));
2184
+ const visibleLow = Math.min(...visibleData.map((point) => point.l));
2185
+ addPriceAxisLabel({
2186
+ text: `H ${formatPrice(visibleHigh)}`,
2187
+ price: visibleHigh,
2188
+ backgroundColor: labels.backgroundColor,
2189
+ textColor: labels.textColor,
2190
+ color: labels.highLowColor,
2191
+ priority: 40
2192
+ });
2193
+ addPriceAxisLabel({
2194
+ text: `L ${formatPrice(visibleLow)}`,
2195
+ price: visibleLow,
2196
+ backgroundColor: labels.backgroundColor,
2197
+ textColor: labels.textColor,
2198
+ color: labels.highLowColor,
2199
+ priority: 40
2200
+ });
2201
+ }
2202
+ if (labels.showBidAsk) {
2203
+ if (Number.isFinite(labels.bidPrice)) {
2204
+ drawReferenceLine(labels.bidPrice, labels.bidColor, "dotted");
2205
+ addPriceAxisLabel({
2206
+ text: `B ${formatPrice(labels.bidPrice)}`,
2207
+ price: labels.bidPrice,
2208
+ backgroundColor: labels.bidColor,
2209
+ textColor: "#0b1220",
2210
+ color: labels.bidColor,
2211
+ priority: 80
2212
+ });
2213
+ }
2214
+ if (Number.isFinite(labels.askPrice)) {
2215
+ drawReferenceLine(labels.askPrice, labels.askColor, "dotted");
2216
+ addPriceAxisLabel({
2217
+ text: `A ${formatPrice(labels.askPrice)}`,
2218
+ price: labels.askPrice,
2219
+ backgroundColor: labels.askColor,
2220
+ textColor: "#0b1220",
2221
+ color: labels.askColor,
2222
+ priority: 80
2223
+ });
2224
+ }
2225
+ }
2226
+ if (priceAxisLabels.length > 0) {
2227
+ const labelPaddingX = Math.max(4, labels.labelPaddingX);
2228
+ const baseLabelHeight = Math.max(14, labels.labelHeight);
2229
+ const labelRadius = Math.max(0, labels.borderRadius);
2230
+ const priceLabelFontSize = Math.max(8, axis.fontSize);
2231
+ ctx.font = `${priceLabelFontSize}px ${mergedOptions.fontFamily}`;
2232
+ const positionedLabels = priceAxisLabels.map((label) => {
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);
2245
+ return {
2246
+ ...label,
2247
+ subtext,
2248
+ subtextFontSize,
2249
+ height: labelHeight,
2250
+ width: labelTextWidth,
2251
+ targetY: clamp(yFromPrice(label.price), chartTop + 1, chartBottom - 1) - labelHeight / 2,
2252
+ y: 0
2253
+ };
2254
+ }).sort((a, b) => a.targetY - b.targetY || b.priority - a.priority);
2255
+ const minY = chartTop;
2256
+ let cursorY = minY;
2257
+ for (const label of positionedLabels) {
2258
+ const maxY = chartBottom - label.height;
2259
+ label.y = labels.noOverlapping ? Math.max(clamp(label.targetY, minY, maxY), cursorY) : clamp(label.targetY, minY, maxY);
2260
+ cursorY = label.y + label.height + 2;
2261
+ }
2262
+ if (labels.noOverlapping && positionedLabels.length > 0) {
2263
+ const lastLabel = positionedLabels[positionedLabels.length - 1];
2264
+ const overflow = lastLabel ? lastLabel.y + lastLabel.height - chartBottom : 0;
2265
+ if (overflow > 0) {
2266
+ for (const label of positionedLabels) {
2267
+ label.y = Math.max(minY, label.y - overflow);
2268
+ }
2269
+ }
2270
+ }
2066
2271
  const labelX = chartRight + 4;
2067
- const labelY = clamp(lineY - labelHeight / 2, chartTop, chartBottom - labelHeight);
2068
- const labelRadius = Math.max(0, ticker.labelBorderRadius ?? 0);
2069
- ctx.fillStyle = ticker.labelBackgroundColor ?? tickerColor;
2070
- fillRoundedRect(Math.round(labelX), Math.round(labelY), labelWidth, labelHeight, labelRadius);
2071
- drawText(
2072
- tickerLabel,
2073
- labelX + labelPaddingX,
2074
- labelY + labelHeight / 2,
2075
- "left",
2076
- "middle",
2077
- ticker.labelTextColor ?? "#0b1220"
2078
- );
2272
+ for (const label of positionedLabels) {
2273
+ ctx.fillStyle = label.backgroundColor;
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
+ }
2290
+ }
2079
2291
  }
2080
2292
  for (const priceLine of priceLines) {
2081
2293
  drawPriceLine(priceLine, yFromPrice, chartLeft, chartTop, chartRight, chartBottom);
@@ -2083,6 +2295,25 @@ function createChart(element, options = {}) {
2083
2295
  for (const orderLine of orderLines) {
2084
2296
  drawOrderLine(orderLine, yFromPrice, chartLeft, chartTop, chartRight, chartBottom);
2085
2297
  }
2298
+ if (labels.visible && (labels.showIndicatorNames || labels.showIndicatorValues)) {
2299
+ const labelEntries = [...activeOverlayIndicators, ...activeSeparateIndicators].map(({ indicator, plugin }) => {
2300
+ const inputValues = Object.entries(indicator.inputs).filter(([, value]) => typeof value === "number" || typeof value === "string" || typeof value === "boolean").slice(0, 2).map(([, value]) => String(value));
2301
+ if (labels.showIndicatorNames && labels.showIndicatorValues && inputValues.length > 0) {
2302
+ return `${plugin.name} ${inputValues.join(" ")}`;
2303
+ }
2304
+ if (labels.showIndicatorNames) {
2305
+ return plugin.name;
2306
+ }
2307
+ return inputValues.join(" ");
2308
+ }).filter((entry) => entry.length > 0);
2309
+ if (labelEntries.length > 0) {
2310
+ const prevFont = ctx.font;
2311
+ ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
2312
+ const legendText = labelEntries.join(" ");
2313
+ drawText(legendText, chartLeft + 10, chartTop + 10, "left", "top", labels.indicatorTextColor);
2314
+ ctx.font = prevFont;
2315
+ }
2316
+ }
2086
2317
  for (let index = tickStartIndex; index <= visibleTickEnd; index += xStep) {
2087
2318
  const tickTime = getTimeForIndex(index);
2088
2319
  if (!tickTime) {
@@ -2098,6 +2329,16 @@ function createChart(element, options = {}) {
2098
2329
  drawText(timeLabel, x, fullChartBottom + 8, "center", "top", xAxis.textColor);
2099
2330
  ctx.font = prevFont;
2100
2331
  }
2332
+ if (labels.visible && labels.showCountdownToBarClose && lastPoint) {
2333
+ const stepMs = getTimeStepMs();
2334
+ const rawRemainingMs = lastPoint.time.getTime() + stepMs - Date.now();
2335
+ const countdownMs = rawRemainingMs >= 0 && rawRemainingMs <= stepMs * 2 ? rawRemainingMs : stepMs - Date.now() % stepMs;
2336
+ const countdownText = formatDuration(countdownMs);
2337
+ const prevFont = ctx.font;
2338
+ ctx.font = `${xAxisFontSize}px ${mergedOptions.fontFamily}`;
2339
+ drawText(countdownText, chartRight + 6, fullChartBottom + 8, "left", "top", xAxis.textColor);
2340
+ ctx.font = prevFont;
2341
+ }
2101
2342
  if (crosshair.visible && crosshairPoint) {
2102
2343
  const cx = clamp(crosshairPoint.x, chartLeft, chartRight);
2103
2344
  const cy = clamp(crosshairPoint.y, chartTop, chartBottom);