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.
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:
@@ -94,6 +106,28 @@ const chart = createChart(root, {
94
106
  });
95
107
  ```
96
108
 
109
+ ## TradingView-Style Labels
110
+
111
+ ```ts
112
+ const chart = createChart(root, {
113
+ labels: {
114
+ symbolName: "ESH6",
115
+ showSymbolName: true,
116
+ showLastPrice: true,
117
+ showPreviousClose: true,
118
+ previousClosePrice: 5231.25,
119
+ showHighLow: true,
120
+ showBidAsk: true,
121
+ bidPrice: 5234.75,
122
+ askPrice: 5235.0,
123
+ showIndicatorNames: true,
124
+ showIndicatorValues: true,
125
+ showCountdownToBarClose: true,
126
+ noOverlapping: true
127
+ }
128
+ });
129
+ ```
130
+
97
131
  ## Axis Label Density
98
132
 
99
133
  ```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",
@@ -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);
@@ -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
  }
@@ -259,10 +260,43 @@ interface TickerLineOptions {
259
260
  color?: string;
260
261
  labelBackgroundColor?: string;
261
262
  labelTextColor?: string;
263
+ labelSubtext?: string;
264
+ labelSubtextColor?: string;
265
+ labelSubtextFontSize?: number;
266
+ showCountdownInLabel?: boolean;
262
267
  labelBorderRadius?: number;
263
268
  smoothing?: boolean;
264
269
  smoothingSpeed?: number;
265
270
  }
271
+ interface LabelsOptions {
272
+ visible?: boolean;
273
+ symbolName?: string;
274
+ showSymbolName?: boolean;
275
+ showLastPrice?: boolean;
276
+ showPreviousClose?: boolean;
277
+ previousClosePrice?: number;
278
+ showHighLow?: boolean;
279
+ showBidAsk?: boolean;
280
+ bidPrice?: number;
281
+ askPrice?: number;
282
+ showIndicatorNames?: boolean;
283
+ showIndicatorValues?: boolean;
284
+ showCountdownToBarClose?: boolean;
285
+ noOverlapping?: boolean;
286
+ backgroundColor?: string;
287
+ textColor?: string;
288
+ mutedTextColor?: string;
289
+ symbolNameBackgroundColor?: string;
290
+ symbolNameTextColor?: string;
291
+ previousCloseColor?: string;
292
+ highLowColor?: string;
293
+ bidColor?: string;
294
+ askColor?: string;
295
+ indicatorTextColor?: string;
296
+ borderRadius?: number;
297
+ labelHeight?: number;
298
+ labelPaddingX?: number;
299
+ }
266
300
  interface ChartInstance {
267
301
  updateOptions: (options: ChartOptions) => void;
268
302
  setData: (data: OhlcDataPoint[]) => void;
@@ -326,4 +360,4 @@ interface ViewportState {
326
360
  }
327
361
  declare function createChart(element: HTMLElement, options?: ChartOptions): ChartInstance;
328
362
 
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 };
363
+ 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 };