hyperprop-charting-library 0.1.44 → 0.1.45

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
@@ -94,6 +94,28 @@ const chart = createChart(root, {
94
94
  });
95
95
  ```
96
96
 
97
+ ## TradingView-Style Labels
98
+
99
+ ```ts
100
+ const chart = createChart(root, {
101
+ labels: {
102
+ symbolName: "ESH6",
103
+ showSymbolName: true,
104
+ showLastPrice: true,
105
+ showPreviousClose: true,
106
+ previousClosePrice: 5231.25,
107
+ showHighLow: true,
108
+ showBidAsk: true,
109
+ bidPrice: 5234.75,
110
+ askPrice: 5235.0,
111
+ showIndicatorNames: true,
112
+ showIndicatorValues: true,
113
+ showCountdownToBarClose: true,
114
+ noOverlapping: true
115
+ }
116
+ });
117
+ ```
118
+
97
119
  ## Axis Label Density
98
120
 
99
121
  ```ts
@@ -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",
@@ -174,6 +203,7 @@ var DEFAULT_OPTIONS = {
174
203
  labelTextColor: "#0b1220",
175
204
  labelBorderRadius: 3
176
205
  },
206
+ labels: DEFAULT_LABELS_OPTIONS,
177
207
  dashPatterns: DEFAULT_DASH_PATTERNS,
178
208
  indicators: []
179
209
  };
@@ -213,6 +243,10 @@ var mergeChartOptions = (baseOptions, options = {}) => ({
213
243
  ...baseOptions.tickerLine,
214
244
  ...options.tickerLine ?? {}
215
245
  },
246
+ labels: {
247
+ ...baseOptions.labels,
248
+ ...options.labels ?? {}
249
+ },
216
250
  dashPatterns: {
217
251
  ...baseOptions.dashPatterns,
218
252
  ...options.dashPatterns ?? {}
@@ -1279,6 +1313,14 @@ function createChart(element, options = {}) {
1279
1313
  ctx.textBaseline = baseline;
1280
1314
  ctx.fillText(text, x, y);
1281
1315
  };
1316
+ const formatDuration = (ms) => {
1317
+ const totalSeconds = Math.max(0, Math.floor(ms / 1e3));
1318
+ const hours = Math.floor(totalSeconds / 3600);
1319
+ const minutes = Math.floor(totalSeconds % 3600 / 60);
1320
+ const seconds = totalSeconds % 60;
1321
+ const pad = (value) => String(value).padStart(2, "0");
1322
+ return hours > 0 ? `${hours}:${pad(minutes)}:${pad(seconds)}` : `${pad(minutes)}:${pad(seconds)}`;
1323
+ };
1282
1324
  const drawPriceLine = (line, yFromPrice, chartLeft, chartTop, chartRight, chartBottom) => {
1283
1325
  const mergedLine = { ...DEFAULT_PRICE_LINE_OPTIONS, ...line };
1284
1326
  if (!mergedLine.visible || !Number.isFinite(mergedLine.price)) {
@@ -2038,14 +2080,39 @@ function createChart(element, options = {}) {
2038
2080
  drawText(formatPrice(price), chartRight + 6, y, "left", "middle", yAxis.textColor);
2039
2081
  ctx.font = prevFont;
2040
2082
  }
2083
+ const labels = { ...DEFAULT_LABELS_OPTIONS, ...mergedOptions.labels ?? {} };
2084
+ const priceAxisLabels = [];
2085
+ const addPriceAxisLabel = (label) => {
2086
+ if (!labels.visible || !Number.isFinite(label.price) || label.text.length === 0) {
2087
+ return;
2088
+ }
2089
+ priceAxisLabels.push(label);
2090
+ };
2091
+ const drawReferenceLine = (price, color, style = "dotted") => {
2092
+ if (!Number.isFinite(price)) {
2093
+ return;
2094
+ }
2095
+ const y = clamp(yFromPrice(price), chartTop + 1, chartBottom - 1);
2096
+ ctx.save();
2097
+ ctx.strokeStyle = color;
2098
+ ctx.lineWidth = 1;
2099
+ applyDashPattern(style, dashPatterns.dotted, dashPatterns.dashed);
2100
+ ctx.beginPath();
2101
+ ctx.moveTo(crisp(chartLeft), crisp(y));
2102
+ ctx.lineTo(crisp(chartRight), crisp(y));
2103
+ ctx.stroke();
2104
+ ctx.restore();
2105
+ };
2041
2106
  const ticker = mergedOptions.tickerLine ?? DEFAULT_OPTIONS.tickerLine;
2042
2107
  const lastPoint = data[data.length - 1];
2108
+ let tickerPrice = null;
2109
+ let tickerColor = null;
2043
2110
  if ((ticker.visible ?? true) && lastPoint) {
2044
- const tickerPrice = ticker.smoothing && smoothedTickerPrice !== null ? smoothedTickerPrice : lastPoint.c;
2111
+ tickerPrice = ticker.smoothing && smoothedTickerPrice !== null ? smoothedTickerPrice : lastPoint.c;
2045
2112
  const tickerY = yFromPrice(tickerPrice);
2046
2113
  const lineY = clamp(tickerY, chartTop + 1, chartBottom - 1);
2047
2114
  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);
2115
+ tickerColor = ticker.color ?? (lastDirection === "up" ? mergedOptions.upColor : mergedOptions.downColor);
2049
2116
  const tickerThickness = Math.max(1, ticker.thickness ?? 1);
2050
2117
  const tickerStyle = ticker.style ?? "solid";
2051
2118
  ctx.save();
@@ -2058,24 +2125,122 @@ function createChart(element, options = {}) {
2058
2125
  ctx.stroke();
2059
2126
  ctx.setLineDash([]);
2060
2127
  ctx.restore();
2061
- const tickerLabel = formatPrice(tickerPrice);
2128
+ }
2129
+ if ((ticker.visible ?? true) && labels.showLastPrice && tickerPrice !== null && tickerColor !== null) {
2130
+ addPriceAxisLabel({
2131
+ text: formatPrice(tickerPrice),
2132
+ price: tickerPrice,
2133
+ backgroundColor: ticker.labelBackgroundColor ?? tickerColor,
2134
+ textColor: ticker.labelTextColor ?? "#0b1220",
2135
+ color: tickerColor,
2136
+ priority: 100
2137
+ });
2138
+ }
2139
+ if (labels.showSymbolName && labels.symbolName.trim().length > 0 && tickerPrice !== null) {
2140
+ addPriceAxisLabel({
2141
+ text: labels.symbolName.trim(),
2142
+ price: tickerPrice,
2143
+ backgroundColor: labels.symbolNameBackgroundColor,
2144
+ textColor: labels.symbolNameTextColor,
2145
+ color: labels.symbolNameBackgroundColor,
2146
+ priority: 95
2147
+ });
2148
+ }
2149
+ if (labels.showPreviousClose) {
2150
+ const previousCloseCandidate = Number.isFinite(labels.previousClosePrice) ? labels.previousClosePrice : data.length > 1 ? data[data.length - 2]?.c : Number.NaN;
2151
+ if (previousCloseCandidate !== void 0 && Number.isFinite(previousCloseCandidate)) {
2152
+ const previousClose = previousCloseCandidate;
2153
+ drawReferenceLine(previousClose, labels.previousCloseColor, "dashed");
2154
+ addPriceAxisLabel({
2155
+ text: `PDC ${formatPrice(previousClose)}`,
2156
+ price: previousClose,
2157
+ backgroundColor: labels.backgroundColor,
2158
+ textColor: labels.mutedTextColor,
2159
+ color: labels.previousCloseColor,
2160
+ priority: 50
2161
+ });
2162
+ }
2163
+ }
2164
+ if (labels.showHighLow && visibleData.length > 0) {
2165
+ const visibleHigh = Math.max(...visibleData.map((point) => point.h));
2166
+ const visibleLow = Math.min(...visibleData.map((point) => point.l));
2167
+ addPriceAxisLabel({
2168
+ text: `H ${formatPrice(visibleHigh)}`,
2169
+ price: visibleHigh,
2170
+ backgroundColor: labels.backgroundColor,
2171
+ textColor: labels.textColor,
2172
+ color: labels.highLowColor,
2173
+ priority: 40
2174
+ });
2175
+ addPriceAxisLabel({
2176
+ text: `L ${formatPrice(visibleLow)}`,
2177
+ price: visibleLow,
2178
+ backgroundColor: labels.backgroundColor,
2179
+ textColor: labels.textColor,
2180
+ color: labels.highLowColor,
2181
+ priority: 40
2182
+ });
2183
+ }
2184
+ if (labels.showBidAsk) {
2185
+ if (Number.isFinite(labels.bidPrice)) {
2186
+ drawReferenceLine(labels.bidPrice, labels.bidColor, "dotted");
2187
+ addPriceAxisLabel({
2188
+ text: `B ${formatPrice(labels.bidPrice)}`,
2189
+ price: labels.bidPrice,
2190
+ backgroundColor: labels.bidColor,
2191
+ textColor: "#0b1220",
2192
+ color: labels.bidColor,
2193
+ priority: 80
2194
+ });
2195
+ }
2196
+ if (Number.isFinite(labels.askPrice)) {
2197
+ drawReferenceLine(labels.askPrice, labels.askColor, "dotted");
2198
+ addPriceAxisLabel({
2199
+ text: `A ${formatPrice(labels.askPrice)}`,
2200
+ price: labels.askPrice,
2201
+ backgroundColor: labels.askColor,
2202
+ textColor: "#0b1220",
2203
+ color: labels.askColor,
2204
+ priority: 80
2205
+ });
2206
+ }
2207
+ }
2208
+ if (priceAxisLabels.length > 0) {
2209
+ const labelPaddingX = Math.max(4, labels.labelPaddingX);
2210
+ const labelHeight = Math.max(14, labels.labelHeight);
2211
+ const labelRadius = Math.max(0, labels.borderRadius);
2062
2212
  ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
2063
- const labelPaddingX = 8;
2064
- const labelHeight = 20;
2065
- const labelWidth = getPriceLabelWidth(tickerLabel, labelPaddingX);
2213
+ 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;
2215
+ return {
2216
+ ...label,
2217
+ width: labelTextWidth,
2218
+ targetY: clamp(yFromPrice(label.price), chartTop + 1, chartBottom - 1) - labelHeight / 2,
2219
+ y: 0
2220
+ };
2221
+ }).sort((a, b) => a.targetY - b.targetY || b.priority - a.priority);
2222
+ const minY = chartTop;
2223
+ const maxY = chartBottom - labelHeight;
2224
+ let cursorY = minY;
2225
+ for (const label of positionedLabels) {
2226
+ label.y = labels.noOverlapping ? Math.max(clamp(label.targetY, minY, maxY), cursorY) : clamp(label.targetY, minY, maxY);
2227
+ cursorY = label.y + labelHeight + 2;
2228
+ }
2229
+ if (labels.noOverlapping && positionedLabels.length > 0) {
2230
+ const lastLabel = positionedLabels[positionedLabels.length - 1];
2231
+ const overflow = lastLabel ? lastLabel.y + labelHeight - maxY : 0;
2232
+ if (overflow > 0) {
2233
+ for (const label of positionedLabels) {
2234
+ label.y = Math.max(minY, label.y - overflow);
2235
+ }
2236
+ }
2237
+ }
2066
2238
  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
- );
2239
+ for (const label of positionedLabels) {
2240
+ 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);
2243
+ }
2079
2244
  }
2080
2245
  for (const priceLine of priceLines) {
2081
2246
  drawPriceLine(priceLine, yFromPrice, chartLeft, chartTop, chartRight, chartBottom);
@@ -2083,6 +2248,25 @@ function createChart(element, options = {}) {
2083
2248
  for (const orderLine of orderLines) {
2084
2249
  drawOrderLine(orderLine, yFromPrice, chartLeft, chartTop, chartRight, chartBottom);
2085
2250
  }
2251
+ if (labels.visible && (labels.showIndicatorNames || labels.showIndicatorValues)) {
2252
+ const labelEntries = [...activeOverlayIndicators, ...activeSeparateIndicators].map(({ indicator, plugin }) => {
2253
+ const inputValues = Object.entries(indicator.inputs).filter(([, value]) => typeof value === "number" || typeof value === "string" || typeof value === "boolean").slice(0, 2).map(([, value]) => String(value));
2254
+ if (labels.showIndicatorNames && labels.showIndicatorValues && inputValues.length > 0) {
2255
+ return `${plugin.name} ${inputValues.join(" ")}`;
2256
+ }
2257
+ if (labels.showIndicatorNames) {
2258
+ return plugin.name;
2259
+ }
2260
+ return inputValues.join(" ");
2261
+ }).filter((entry) => entry.length > 0);
2262
+ if (labelEntries.length > 0) {
2263
+ const prevFont = ctx.font;
2264
+ ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
2265
+ const legendText = labelEntries.join(" ");
2266
+ drawText(legendText, chartLeft + 10, chartTop + 10, "left", "top", labels.indicatorTextColor);
2267
+ ctx.font = prevFont;
2268
+ }
2269
+ }
2086
2270
  for (let index = tickStartIndex; index <= visibleTickEnd; index += xStep) {
2087
2271
  const tickTime = getTimeForIndex(index);
2088
2272
  if (!tickTime) {
@@ -2098,6 +2282,16 @@ function createChart(element, options = {}) {
2098
2282
  drawText(timeLabel, x, fullChartBottom + 8, "center", "top", xAxis.textColor);
2099
2283
  ctx.font = prevFont;
2100
2284
  }
2285
+ if (labels.visible && labels.showCountdownToBarClose && lastPoint) {
2286
+ const stepMs = getTimeStepMs();
2287
+ const rawRemainingMs = lastPoint.time.getTime() + stepMs - Date.now();
2288
+ const countdownMs = rawRemainingMs >= 0 && rawRemainingMs <= stepMs * 2 ? rawRemainingMs : stepMs - Date.now() % stepMs;
2289
+ const countdownText = formatDuration(countdownMs);
2290
+ const prevFont = ctx.font;
2291
+ ctx.font = `${xAxisFontSize}px ${mergedOptions.fontFamily}`;
2292
+ drawText(countdownText, chartRight + 6, fullChartBottom + 8, "left", "top", xAxis.textColor);
2293
+ ctx.font = prevFont;
2294
+ }
2101
2295
  if (crosshair.visible && crosshairPoint) {
2102
2296
  const cx = clamp(crosshairPoint.x, chartLeft, chartRight);
2103
2297
  const cy = clamp(crosshairPoint.y, chartTop, chartBottom);
@@ -37,6 +37,7 @@ interface ChartOptions {
37
37
  priceLines?: PriceLineOptions[];
38
38
  orderLines?: OrderLineOptions[];
39
39
  tickerLine?: TickerLineOptions;
40
+ labels?: LabelsOptions;
40
41
  dashPatterns?: Partial<DashPatternOptions>;
41
42
  indicators?: IndicatorInstanceOptions[];
42
43
  }
@@ -263,6 +264,35 @@ interface TickerLineOptions {
263
264
  smoothing?: boolean;
264
265
  smoothingSpeed?: number;
265
266
  }
267
+ interface LabelsOptions {
268
+ visible?: boolean;
269
+ symbolName?: string;
270
+ showSymbolName?: boolean;
271
+ showLastPrice?: boolean;
272
+ showPreviousClose?: boolean;
273
+ previousClosePrice?: number;
274
+ showHighLow?: boolean;
275
+ showBidAsk?: boolean;
276
+ bidPrice?: number;
277
+ askPrice?: number;
278
+ showIndicatorNames?: boolean;
279
+ showIndicatorValues?: boolean;
280
+ showCountdownToBarClose?: boolean;
281
+ noOverlapping?: boolean;
282
+ backgroundColor?: string;
283
+ textColor?: string;
284
+ mutedTextColor?: string;
285
+ symbolNameBackgroundColor?: string;
286
+ symbolNameTextColor?: string;
287
+ previousCloseColor?: string;
288
+ highLowColor?: string;
289
+ bidColor?: string;
290
+ askColor?: string;
291
+ indicatorTextColor?: string;
292
+ borderRadius?: number;
293
+ labelHeight?: number;
294
+ labelPaddingX?: number;
295
+ }
266
296
  interface ChartInstance {
267
297
  updateOptions: (options: ChartOptions) => void;
268
298
  setData: (data: OhlcDataPoint[]) => void;
@@ -326,4 +356,4 @@ interface ViewportState {
326
356
  }
327
357
  declare function createChart(element: HTMLElement, options?: ChartOptions): ChartInstance;
328
358
 
329
- export { type AxisOptions, type BuiltInIndicatorInfo, type ChartClickEvent, type ChartInstance, type ChartOptions, type CrosshairMoveEvent, type CrosshairOptions, type CrosshairPriceActionEvent, type DashPatternOptions, type GridOptions, type IndicatorInstanceOptions, type IndicatorPane, type IndicatorPlugin, type IndicatorRenderContext, type OhlcDataPoint, type OrderActionButton, type OrderActionEvent, type OrderLineOptions, type PriceLineOptions, type TickerLineOptions, type ViewportState, type WatermarkOptions, createChart };
359
+ export { type AxisOptions, type BuiltInIndicatorInfo, type ChartClickEvent, type ChartInstance, type ChartOptions, type CrosshairMoveEvent, type CrosshairOptions, type CrosshairPriceActionEvent, type DashPatternOptions, type GridOptions, type IndicatorInstanceOptions, type IndicatorPane, type IndicatorPlugin, type IndicatorRenderContext, type LabelsOptions, type OhlcDataPoint, type OrderActionButton, type OrderActionEvent, type OrderLineOptions, type PriceLineOptions, type TickerLineOptions, type ViewportState, type WatermarkOptions, createChart };
@@ -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",
@@ -150,6 +179,7 @@ var DEFAULT_OPTIONS = {
150
179
  labelTextColor: "#0b1220",
151
180
  labelBorderRadius: 3
152
181
  },
182
+ labels: DEFAULT_LABELS_OPTIONS,
153
183
  dashPatterns: DEFAULT_DASH_PATTERNS,
154
184
  indicators: []
155
185
  };
@@ -189,6 +219,10 @@ var mergeChartOptions = (baseOptions, options = {}) => ({
189
219
  ...baseOptions.tickerLine,
190
220
  ...options.tickerLine ?? {}
191
221
  },
222
+ labels: {
223
+ ...baseOptions.labels,
224
+ ...options.labels ?? {}
225
+ },
192
226
  dashPatterns: {
193
227
  ...baseOptions.dashPatterns,
194
228
  ...options.dashPatterns ?? {}
@@ -1255,6 +1289,14 @@ function createChart(element, options = {}) {
1255
1289
  ctx.textBaseline = baseline;
1256
1290
  ctx.fillText(text, x, y);
1257
1291
  };
1292
+ const formatDuration = (ms) => {
1293
+ const totalSeconds = Math.max(0, Math.floor(ms / 1e3));
1294
+ const hours = Math.floor(totalSeconds / 3600);
1295
+ const minutes = Math.floor(totalSeconds % 3600 / 60);
1296
+ const seconds = totalSeconds % 60;
1297
+ const pad = (value) => String(value).padStart(2, "0");
1298
+ return hours > 0 ? `${hours}:${pad(minutes)}:${pad(seconds)}` : `${pad(minutes)}:${pad(seconds)}`;
1299
+ };
1258
1300
  const drawPriceLine = (line, yFromPrice, chartLeft, chartTop, chartRight, chartBottom) => {
1259
1301
  const mergedLine = { ...DEFAULT_PRICE_LINE_OPTIONS, ...line };
1260
1302
  if (!mergedLine.visible || !Number.isFinite(mergedLine.price)) {
@@ -2014,14 +2056,39 @@ function createChart(element, options = {}) {
2014
2056
  drawText(formatPrice(price), chartRight + 6, y, "left", "middle", yAxis.textColor);
2015
2057
  ctx.font = prevFont;
2016
2058
  }
2059
+ const labels = { ...DEFAULT_LABELS_OPTIONS, ...mergedOptions.labels ?? {} };
2060
+ const priceAxisLabels = [];
2061
+ const addPriceAxisLabel = (label) => {
2062
+ if (!labels.visible || !Number.isFinite(label.price) || label.text.length === 0) {
2063
+ return;
2064
+ }
2065
+ priceAxisLabels.push(label);
2066
+ };
2067
+ const drawReferenceLine = (price, color, style = "dotted") => {
2068
+ if (!Number.isFinite(price)) {
2069
+ return;
2070
+ }
2071
+ const y = clamp(yFromPrice(price), chartTop + 1, chartBottom - 1);
2072
+ ctx.save();
2073
+ ctx.strokeStyle = color;
2074
+ ctx.lineWidth = 1;
2075
+ applyDashPattern(style, dashPatterns.dotted, dashPatterns.dashed);
2076
+ ctx.beginPath();
2077
+ ctx.moveTo(crisp(chartLeft), crisp(y));
2078
+ ctx.lineTo(crisp(chartRight), crisp(y));
2079
+ ctx.stroke();
2080
+ ctx.restore();
2081
+ };
2017
2082
  const ticker = mergedOptions.tickerLine ?? DEFAULT_OPTIONS.tickerLine;
2018
2083
  const lastPoint = data[data.length - 1];
2084
+ let tickerPrice = null;
2085
+ let tickerColor = null;
2019
2086
  if ((ticker.visible ?? true) && lastPoint) {
2020
- const tickerPrice = ticker.smoothing && smoothedTickerPrice !== null ? smoothedTickerPrice : lastPoint.c;
2087
+ tickerPrice = ticker.smoothing && smoothedTickerPrice !== null ? smoothedTickerPrice : lastPoint.c;
2021
2088
  const tickerY = yFromPrice(tickerPrice);
2022
2089
  const lineY = clamp(tickerY, chartTop + 1, chartBottom - 1);
2023
2090
  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);
2091
+ tickerColor = ticker.color ?? (lastDirection === "up" ? mergedOptions.upColor : mergedOptions.downColor);
2025
2092
  const tickerThickness = Math.max(1, ticker.thickness ?? 1);
2026
2093
  const tickerStyle = ticker.style ?? "solid";
2027
2094
  ctx.save();
@@ -2034,24 +2101,122 @@ function createChart(element, options = {}) {
2034
2101
  ctx.stroke();
2035
2102
  ctx.setLineDash([]);
2036
2103
  ctx.restore();
2037
- const tickerLabel = formatPrice(tickerPrice);
2104
+ }
2105
+ if ((ticker.visible ?? true) && labels.showLastPrice && tickerPrice !== null && tickerColor !== null) {
2106
+ addPriceAxisLabel({
2107
+ text: formatPrice(tickerPrice),
2108
+ price: tickerPrice,
2109
+ backgroundColor: ticker.labelBackgroundColor ?? tickerColor,
2110
+ textColor: ticker.labelTextColor ?? "#0b1220",
2111
+ color: tickerColor,
2112
+ priority: 100
2113
+ });
2114
+ }
2115
+ if (labels.showSymbolName && labels.symbolName.trim().length > 0 && tickerPrice !== null) {
2116
+ addPriceAxisLabel({
2117
+ text: labels.symbolName.trim(),
2118
+ price: tickerPrice,
2119
+ backgroundColor: labels.symbolNameBackgroundColor,
2120
+ textColor: labels.symbolNameTextColor,
2121
+ color: labels.symbolNameBackgroundColor,
2122
+ priority: 95
2123
+ });
2124
+ }
2125
+ if (labels.showPreviousClose) {
2126
+ const previousCloseCandidate = Number.isFinite(labels.previousClosePrice) ? labels.previousClosePrice : data.length > 1 ? data[data.length - 2]?.c : Number.NaN;
2127
+ if (previousCloseCandidate !== void 0 && Number.isFinite(previousCloseCandidate)) {
2128
+ const previousClose = previousCloseCandidate;
2129
+ drawReferenceLine(previousClose, labels.previousCloseColor, "dashed");
2130
+ addPriceAxisLabel({
2131
+ text: `PDC ${formatPrice(previousClose)}`,
2132
+ price: previousClose,
2133
+ backgroundColor: labels.backgroundColor,
2134
+ textColor: labels.mutedTextColor,
2135
+ color: labels.previousCloseColor,
2136
+ priority: 50
2137
+ });
2138
+ }
2139
+ }
2140
+ if (labels.showHighLow && visibleData.length > 0) {
2141
+ const visibleHigh = Math.max(...visibleData.map((point) => point.h));
2142
+ const visibleLow = Math.min(...visibleData.map((point) => point.l));
2143
+ addPriceAxisLabel({
2144
+ text: `H ${formatPrice(visibleHigh)}`,
2145
+ price: visibleHigh,
2146
+ backgroundColor: labels.backgroundColor,
2147
+ textColor: labels.textColor,
2148
+ color: labels.highLowColor,
2149
+ priority: 40
2150
+ });
2151
+ addPriceAxisLabel({
2152
+ text: `L ${formatPrice(visibleLow)}`,
2153
+ price: visibleLow,
2154
+ backgroundColor: labels.backgroundColor,
2155
+ textColor: labels.textColor,
2156
+ color: labels.highLowColor,
2157
+ priority: 40
2158
+ });
2159
+ }
2160
+ if (labels.showBidAsk) {
2161
+ if (Number.isFinite(labels.bidPrice)) {
2162
+ drawReferenceLine(labels.bidPrice, labels.bidColor, "dotted");
2163
+ addPriceAxisLabel({
2164
+ text: `B ${formatPrice(labels.bidPrice)}`,
2165
+ price: labels.bidPrice,
2166
+ backgroundColor: labels.bidColor,
2167
+ textColor: "#0b1220",
2168
+ color: labels.bidColor,
2169
+ priority: 80
2170
+ });
2171
+ }
2172
+ if (Number.isFinite(labels.askPrice)) {
2173
+ drawReferenceLine(labels.askPrice, labels.askColor, "dotted");
2174
+ addPriceAxisLabel({
2175
+ text: `A ${formatPrice(labels.askPrice)}`,
2176
+ price: labels.askPrice,
2177
+ backgroundColor: labels.askColor,
2178
+ textColor: "#0b1220",
2179
+ color: labels.askColor,
2180
+ priority: 80
2181
+ });
2182
+ }
2183
+ }
2184
+ if (priceAxisLabels.length > 0) {
2185
+ const labelPaddingX = Math.max(4, labels.labelPaddingX);
2186
+ const labelHeight = Math.max(14, labels.labelHeight);
2187
+ const labelRadius = Math.max(0, labels.borderRadius);
2038
2188
  ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
2039
- const labelPaddingX = 8;
2040
- const labelHeight = 20;
2041
- const labelWidth = getPriceLabelWidth(tickerLabel, labelPaddingX);
2189
+ 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;
2191
+ return {
2192
+ ...label,
2193
+ width: labelTextWidth,
2194
+ targetY: clamp(yFromPrice(label.price), chartTop + 1, chartBottom - 1) - labelHeight / 2,
2195
+ y: 0
2196
+ };
2197
+ }).sort((a, b) => a.targetY - b.targetY || b.priority - a.priority);
2198
+ const minY = chartTop;
2199
+ const maxY = chartBottom - labelHeight;
2200
+ let cursorY = minY;
2201
+ for (const label of positionedLabels) {
2202
+ label.y = labels.noOverlapping ? Math.max(clamp(label.targetY, minY, maxY), cursorY) : clamp(label.targetY, minY, maxY);
2203
+ cursorY = label.y + labelHeight + 2;
2204
+ }
2205
+ if (labels.noOverlapping && positionedLabels.length > 0) {
2206
+ const lastLabel = positionedLabels[positionedLabels.length - 1];
2207
+ const overflow = lastLabel ? lastLabel.y + labelHeight - maxY : 0;
2208
+ if (overflow > 0) {
2209
+ for (const label of positionedLabels) {
2210
+ label.y = Math.max(minY, label.y - overflow);
2211
+ }
2212
+ }
2213
+ }
2042
2214
  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
- );
2215
+ for (const label of positionedLabels) {
2216
+ 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);
2219
+ }
2055
2220
  }
2056
2221
  for (const priceLine of priceLines) {
2057
2222
  drawPriceLine(priceLine, yFromPrice, chartLeft, chartTop, chartRight, chartBottom);
@@ -2059,6 +2224,25 @@ function createChart(element, options = {}) {
2059
2224
  for (const orderLine of orderLines) {
2060
2225
  drawOrderLine(orderLine, yFromPrice, chartLeft, chartTop, chartRight, chartBottom);
2061
2226
  }
2227
+ if (labels.visible && (labels.showIndicatorNames || labels.showIndicatorValues)) {
2228
+ const labelEntries = [...activeOverlayIndicators, ...activeSeparateIndicators].map(({ indicator, plugin }) => {
2229
+ const inputValues = Object.entries(indicator.inputs).filter(([, value]) => typeof value === "number" || typeof value === "string" || typeof value === "boolean").slice(0, 2).map(([, value]) => String(value));
2230
+ if (labels.showIndicatorNames && labels.showIndicatorValues && inputValues.length > 0) {
2231
+ return `${plugin.name} ${inputValues.join(" ")}`;
2232
+ }
2233
+ if (labels.showIndicatorNames) {
2234
+ return plugin.name;
2235
+ }
2236
+ return inputValues.join(" ");
2237
+ }).filter((entry) => entry.length > 0);
2238
+ if (labelEntries.length > 0) {
2239
+ const prevFont = ctx.font;
2240
+ ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
2241
+ const legendText = labelEntries.join(" ");
2242
+ drawText(legendText, chartLeft + 10, chartTop + 10, "left", "top", labels.indicatorTextColor);
2243
+ ctx.font = prevFont;
2244
+ }
2245
+ }
2062
2246
  for (let index = tickStartIndex; index <= visibleTickEnd; index += xStep) {
2063
2247
  const tickTime = getTimeForIndex(index);
2064
2248
  if (!tickTime) {
@@ -2074,6 +2258,16 @@ function createChart(element, options = {}) {
2074
2258
  drawText(timeLabel, x, fullChartBottom + 8, "center", "top", xAxis.textColor);
2075
2259
  ctx.font = prevFont;
2076
2260
  }
2261
+ if (labels.visible && labels.showCountdownToBarClose && lastPoint) {
2262
+ const stepMs = getTimeStepMs();
2263
+ const rawRemainingMs = lastPoint.time.getTime() + stepMs - Date.now();
2264
+ const countdownMs = rawRemainingMs >= 0 && rawRemainingMs <= stepMs * 2 ? rawRemainingMs : stepMs - Date.now() % stepMs;
2265
+ const countdownText = formatDuration(countdownMs);
2266
+ const prevFont = ctx.font;
2267
+ ctx.font = `${xAxisFontSize}px ${mergedOptions.fontFamily}`;
2268
+ drawText(countdownText, chartRight + 6, fullChartBottom + 8, "left", "top", xAxis.textColor);
2269
+ ctx.font = prevFont;
2270
+ }
2077
2271
  if (crosshair.visible && crosshairPoint) {
2078
2272
  const cx = clamp(crosshairPoint.x, chartLeft, chartRight);
2079
2273
  const cy = clamp(crosshairPoint.y, chartTop, chartBottom);