hyperprop-charting-library 0.1.49 → 0.1.51

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.
@@ -74,6 +74,10 @@ var DEFAULT_LABELS_OPTIONS = {
74
74
  askPrice: Number.NaN,
75
75
  showIndicatorNames: false,
76
76
  showIndicatorValues: false,
77
+ showIndicatorValueLabels: true,
78
+ indicatorLegendPosition: "top-left",
79
+ indicatorLegendOffsetX: 10,
80
+ indicatorLegendOffsetY: 10,
77
81
  showCountdownToBarClose: false,
78
82
  noOverlapping: true,
79
83
  backgroundColor: "#0b1220",
@@ -507,7 +511,7 @@ var drawOverlaySeries = (ctx, renderContext, values, color, width) => {
507
511
  }
508
512
  ctx.restore();
509
513
  };
510
- var drawSeparateSeries = (ctx, renderContext, values, color, width, minOverride, maxOverride, guideLines) => {
514
+ var drawSeparateSeries = (ctx, renderContext, values, color, width, minOverride, maxOverride, guideLines, options = {}) => {
511
515
  const visible = [];
512
516
  for (let index = renderContext.startIndex; index <= renderContext.endIndex; index += 1) {
513
517
  const value = values[index];
@@ -515,7 +519,7 @@ var drawSeparateSeries = (ctx, renderContext, values, color, width, minOverride,
515
519
  visible.push(value);
516
520
  }
517
521
  }
518
- if (visible.length === 0) return;
522
+ if (visible.length === 0) return void 0;
519
523
  const minValue = minOverride ?? Math.min(...visible);
520
524
  const maxValue = maxOverride ?? Math.max(...visible);
521
525
  const range = maxValue - minValue || 1;
@@ -564,6 +568,51 @@ var drawSeparateSeries = (ctx, renderContext, values, color, width, minOverride,
564
568
  }
565
569
  }
566
570
  ctx.restore();
571
+ let latestValue = null;
572
+ for (let index = values.length - 1; index >= 0; index -= 1) {
573
+ const value = values[index];
574
+ if (Number.isFinite(value ?? Number.NaN)) {
575
+ latestValue = value;
576
+ break;
577
+ }
578
+ }
579
+ const decimals = options.decimals ?? 2;
580
+ const formatValue = (value) => value.toFixed(decimals);
581
+ const axisTicks = options.axisTicks ?? guideLines;
582
+ const paneInfo = {
583
+ ...options.title ? { title: options.title } : {},
584
+ axis: {
585
+ min: minValue,
586
+ max: maxValue,
587
+ ...axisTicks ? { ticks: axisTicks } : {},
588
+ decimals
589
+ }
590
+ };
591
+ if (guideLines) {
592
+ paneInfo.guideLines = guideLines.map((value) => ({ value, label: formatValue(value), style: "dotted" }));
593
+ }
594
+ if (latestValue !== null) {
595
+ paneInfo.legendValues = [
596
+ {
597
+ ...options.legendLabel ? { label: options.legendLabel } : {},
598
+ value: latestValue,
599
+ text: formatValue(latestValue),
600
+ color
601
+ }
602
+ ];
603
+ }
604
+ if (options.valueLabel !== false && latestValue !== null) {
605
+ paneInfo.valueLabels = [
606
+ {
607
+ value: latestValue,
608
+ text: formatValue(latestValue),
609
+ color: options.valueLabelColor ?? color,
610
+ backgroundColor: options.valueLabelBackgroundColor ?? options.valueLabelColor ?? color,
611
+ textColor: options.valueLabelTextColor ?? "#0f172a"
612
+ }
613
+ ];
614
+ }
615
+ return paneInfo;
567
616
  };
568
617
  var getPercentileValue = (values, percentile) => {
569
618
  if (values.length === 0) {
@@ -736,7 +785,10 @@ var BUILTIN_STDDEV_INDICATOR = {
736
785
  renderContext.data,
737
786
  () => computeStdDevSeries(renderContext.data, length, inputs.source ?? "close")
738
787
  );
739
- drawSeparateSeries(ctx, renderContext, values, inputs.color ?? "#f97316", Number(inputs.width) || 2);
788
+ return drawSeparateSeries(ctx, renderContext, values, inputs.color ?? "#f97316", Number(inputs.width) || 2, void 0, void 0, void 0, {
789
+ title: `StdDev ${length}`,
790
+ decimals: 2
791
+ });
740
792
  }
741
793
  };
742
794
  var BUILTIN_ATR_INDICATOR = {
@@ -748,7 +800,10 @@ var BUILTIN_ATR_INDICATOR = {
748
800
  draw: (ctx, renderContext, inputs) => {
749
801
  const length = clampIndicatorLength(inputs.length, 14);
750
802
  const values = withCachedSeries(`atr|${length}`, renderContext.data, () => computeAtrSeries(renderContext.data, length));
751
- drawSeparateSeries(ctx, renderContext, values, inputs.color ?? "#eab308", Number(inputs.width) || 2);
803
+ return drawSeparateSeries(ctx, renderContext, values, inputs.color ?? "#eab308", Number(inputs.width) || 2, void 0, void 0, void 0, {
804
+ title: `ATR ${length}`,
805
+ decimals: 2
806
+ });
752
807
  }
753
808
  };
754
809
  var BUILTIN_RSI_INDICATOR = {
@@ -760,7 +815,7 @@ var BUILTIN_RSI_INDICATOR = {
760
815
  draw: (ctx, renderContext, inputs) => {
761
816
  const length = clampIndicatorLength(inputs.length, 14);
762
817
  const values = withCachedSeries(`rsi|${length}`, renderContext.data, () => computeRsiSeries(renderContext.data, length));
763
- drawSeparateSeries(
818
+ return drawSeparateSeries(
764
819
  ctx,
765
820
  renderContext,
766
821
  values,
@@ -768,7 +823,15 @@ var BUILTIN_RSI_INDICATOR = {
768
823
  Number(inputs.width) || 2,
769
824
  0,
770
825
  100,
771
- [30, 50, 70]
826
+ [30, 50, 70],
827
+ {
828
+ title: `RSI ${length}`,
829
+ axisTicks: [0, 30, 50, 70, 100],
830
+ decimals: 2,
831
+ valueLabelColor: "#9E9E9E",
832
+ valueLabelBackgroundColor: "#9E9E9E",
833
+ valueLabelTextColor: "#0f172a"
834
+ }
772
835
  );
773
836
  }
774
837
  };
@@ -1332,6 +1395,11 @@ function createChart(element, options = {}) {
1332
1395
  const pad = (value) => String(value).padStart(2, "0");
1333
1396
  return hours > 0 ? `${hours}:${pad(minutes)}:${pad(seconds)}` : `${pad(minutes)}:${pad(seconds)}`;
1334
1397
  };
1398
+ const getRightAxisLabelX = (chartRight) => chartRight + 1;
1399
+ const getRightAxisLabelWidth = (chartRight, contentWidth) => {
1400
+ const scaleWidth = Math.max(0, Math.floor(width - getRightAxisLabelX(chartRight)));
1401
+ return Math.max(contentWidth, scaleWidth);
1402
+ };
1335
1403
  const drawPriceLine = (line, yFromPrice, chartLeft, chartTop, chartRight, chartBottom) => {
1336
1404
  const mergedLine = { ...DEFAULT_PRICE_LINE_OPTIONS, ...line };
1337
1405
  if (!mergedLine.visible || !Number.isFinite(mergedLine.price)) {
@@ -1356,8 +1424,9 @@ function createChart(element, options = {}) {
1356
1424
  ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
1357
1425
  const labelPaddingX = 8;
1358
1426
  const labelHeight = 20;
1359
- const labelWidth = mergedLine.label === void 0 ? getPriceLabelWidth(labelText, labelPaddingX) : Math.ceil(ctx.measureText(labelText).width) + labelPaddingX * 2;
1360
- const labelX = chartRight + 4;
1427
+ const measuredLabelWidth = mergedLine.label === void 0 ? getPriceLabelWidth(labelText, labelPaddingX) : Math.ceil(ctx.measureText(labelText).width) + labelPaddingX * 2;
1428
+ const labelWidth = getRightAxisLabelWidth(chartRight, measuredLabelWidth);
1429
+ const labelX = getRightAxisLabelX(chartRight);
1361
1430
  const labelY = placeRightAxisLabel(lineY - labelHeight / 2, labelHeight, chartTop, chartBottom - labelHeight);
1362
1431
  ctx.fillStyle = mergedLine.labelBackgroundColor;
1363
1432
  fillRoundedRect(
@@ -1595,11 +1664,11 @@ function createChart(element, options = {}) {
1595
1664
  const priceText = formatPrice(renderPrice);
1596
1665
  const pricePaddingX = 8;
1597
1666
  const measuredPriceWidth = getPriceLabelWidth(priceText, pricePaddingX);
1598
- const priceWidth = mergedLine.id === void 0 ? measuredPriceWidth : Math.max(measuredPriceWidth, orderPriceTagWidthById.get(mergedLine.id) ?? 0);
1667
+ const priceWidth = mergedLine.id === void 0 ? getRightAxisLabelWidth(chartRight, measuredPriceWidth) : getRightAxisLabelWidth(chartRight, Math.max(measuredPriceWidth, orderPriceTagWidthById.get(mergedLine.id) ?? 0));
1599
1668
  if (mergedLine.id) {
1600
1669
  orderPriceTagWidthById.set(mergedLine.id, priceWidth);
1601
1670
  }
1602
- const priceX = chartRight + 4;
1671
+ const priceX = getRightAxisLabelX(chartRight);
1603
1672
  const priceY = placeRightAxisLabel(targetLabelY, labelHeight, chartTop, chartBottom - labelHeight);
1604
1673
  ctx.fillStyle = mergedLine.labelBackgroundColor ?? color;
1605
1674
  fillRoundedRect(Math.round(priceX), Math.round(priceY), priceWidth, labelHeight, borderRadius);
@@ -1674,9 +1743,63 @@ function createChart(element, options = {}) {
1674
1743
  ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
1675
1744
  ctx.fillStyle = mergedOptions.backgroundColor;
1676
1745
  ctx.fillRect(0, 0, width, height);
1746
+ const labels = { ...DEFAULT_LABELS_OPTIONS, ...mergedOptions.labels ?? {} };
1747
+ const estimateRightMargin = () => {
1748
+ const paddingX = Math.max(4, labels.labelPaddingX);
1749
+ const measure = (text) => Math.ceil(ctx.measureText(text).width) + paddingX * 2;
1750
+ let required = margin.right - 1;
1751
+ const include = (text) => {
1752
+ const normalized = text?.trim();
1753
+ if (normalized) {
1754
+ required = Math.max(required, measure(normalized));
1755
+ }
1756
+ };
1757
+ const ticker2 = mergedOptions.tickerLine ?? DEFAULT_OPTIONS.tickerLine;
1758
+ const lastPoint2 = data[data.length - 1];
1759
+ if ((ticker2.visible ?? true) && labels.showLastPrice && lastPoint2) {
1760
+ include(formatPrice(lastPoint2.c));
1761
+ if (ticker2.labelSubtext) include(String(ticker2.labelSubtext));
1762
+ for (const subtext of ticker2.labelSubtexts ?? []) {
1763
+ include(String(subtext));
1764
+ }
1765
+ }
1766
+ if (labels.showSymbolName) {
1767
+ include(labels.symbolName);
1768
+ }
1769
+ if (labels.showPreviousClose) {
1770
+ const previousCloseCandidate = Number.isFinite(labels.previousClosePrice) ? labels.previousClosePrice : data.length > 1 ? data[data.length - 2]?.c : Number.NaN;
1771
+ if (previousCloseCandidate !== void 0 && Number.isFinite(previousCloseCandidate)) {
1772
+ include(`PDC ${formatPrice(previousCloseCandidate)}`);
1773
+ }
1774
+ }
1775
+ if (labels.showHighLow && data.length > 0) {
1776
+ include(`H ${formatPrice(Math.max(...data.map((point) => point.h)))}`);
1777
+ include(`L ${formatPrice(Math.min(...data.map((point) => point.l)))}`);
1778
+ }
1779
+ if (labels.showBidAsk) {
1780
+ if (Number.isFinite(labels.bidPrice)) include(`B ${formatPrice(labels.bidPrice)}`);
1781
+ if (Number.isFinite(labels.askPrice)) include(`A ${formatPrice(labels.askPrice)}`);
1782
+ }
1783
+ for (const line of priceLines) {
1784
+ const mergedLine = { ...DEFAULT_PRICE_LINE_OPTIONS, ...line };
1785
+ if (mergedLine.visible && Number.isFinite(mergedLine.price)) {
1786
+ include(mergedLine.label ?? formatPrice(mergedLine.price));
1787
+ }
1788
+ }
1789
+ for (const line of orderLines) {
1790
+ const mergedLine = { ...DEFAULT_ORDER_LINE_OPTIONS, ...line };
1791
+ const renderPrice = mergedLine.behavior === "follow" && Number.isFinite(mergedLine.followPrice) ? mergedLine.followPrice : mergedLine.price;
1792
+ if (mergedLine.visible && Number.isFinite(renderPrice)) {
1793
+ include(formatPrice(renderPrice));
1794
+ }
1795
+ }
1796
+ const maxRightMargin = Math.max(margin.right, width - margin.left - 160);
1797
+ return Math.min(maxRightMargin, Math.max(margin.right, Math.ceil(required + 1)));
1798
+ };
1799
+ const rightMargin = estimateRightMargin();
1677
1800
  const chartLeft = margin.left;
1678
1801
  const chartTop = margin.top;
1679
- const chartWidth = width - margin.left - margin.right;
1802
+ const chartWidth = width - margin.left - rightMargin;
1680
1803
  const fullChartHeight = height - margin.top - margin.bottom;
1681
1804
  const fullChartBottom = chartTop + fullChartHeight;
1682
1805
  const chartRight = chartLeft + chartWidth;
@@ -2029,7 +2152,7 @@ function createChart(element, options = {}) {
2029
2152
  ctx.beginPath();
2030
2153
  ctx.rect(chartLeft + 1, paneTop + 1, Math.max(0, chartWidth - 2), Math.max(0, paneHeight - 2));
2031
2154
  ctx.clip();
2032
- plugin.draw(
2155
+ const paneInfo = plugin.draw(
2033
2156
  ctx,
2034
2157
  {
2035
2158
  data,
@@ -2062,6 +2185,63 @@ function createChart(element, options = {}) {
2062
2185
  ctx.lineTo(crisp(chartRight), crisp(paneTop));
2063
2186
  ctx.stroke();
2064
2187
  ctx.restore();
2188
+ const axisInfo = paneInfo?.axis;
2189
+ if (axisInfo && Number.isFinite(axisInfo.min) && Number.isFinite(axisInfo.max) && axisInfo.max !== axisInfo.min) {
2190
+ const paneRange = axisInfo.max - axisInfo.min;
2191
+ const yFromPaneValue = (value) => {
2192
+ const ratio = (value - axisInfo.min) / paneRange;
2193
+ return paneBottom - ratio * paneHeight;
2194
+ };
2195
+ const formatPaneValue = (value) => {
2196
+ if (axisInfo.format) {
2197
+ return axisInfo.format(value);
2198
+ }
2199
+ const decimals = axisInfo.decimals ?? (Math.abs(paneRange) <= 2 ? 2 : Math.abs(paneRange) <= 20 ? 1 : 0);
2200
+ return value.toFixed(Math.max(0, Math.min(8, Math.round(decimals))));
2201
+ };
2202
+ const axisTicks = axisInfo.ticks && axisInfo.ticks.length > 0 ? axisInfo.ticks : [axisInfo.min, axisInfo.min + paneRange / 2, axisInfo.max];
2203
+ const uniqueTicks = Array.from(new Set(axisTicks.filter((tick) => Number.isFinite(tick))));
2204
+ ctx.save();
2205
+ ctx.font = `${yAxisFontSize}px ${mergedOptions.fontFamily}`;
2206
+ for (const tick of uniqueTicks) {
2207
+ if (tick < axisInfo.min || tick > axisInfo.max) {
2208
+ continue;
2209
+ }
2210
+ drawText(formatPaneValue(tick), chartRight + 6, yFromPaneValue(tick), "left", "middle", yAxis.textColor);
2211
+ }
2212
+ ctx.restore();
2213
+ if (labels.visible) {
2214
+ const prevFont = ctx.font;
2215
+ ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
2216
+ const legendTitle = paneInfo.title ?? plugin.name;
2217
+ const legendValues = paneInfo.legendValues ?? [];
2218
+ const legendParts = [
2219
+ legendTitle,
2220
+ ...legendValues.map((value) => value.text ?? (value.value === void 0 ? "" : formatPaneValue(value.value))).filter(Boolean)
2221
+ ].filter(Boolean);
2222
+ if (legendParts.length > 0) {
2223
+ drawText(legendParts.join(" "), chartLeft + 10, paneTop + 8, "left", "top", labels.indicatorTextColor);
2224
+ }
2225
+ for (const label of paneInfo.valueLabels ?? []) {
2226
+ if (!labels.showIndicatorValueLabels) {
2227
+ continue;
2228
+ }
2229
+ if (!Number.isFinite(label.value) || label.value < axisInfo.min || label.value > axisInfo.max) {
2230
+ continue;
2231
+ }
2232
+ const text = label.text ?? formatPaneValue(label.value);
2233
+ const labelPaddingX = 7;
2234
+ const labelHeight = Math.max(16, yAxisFontSize + 8);
2235
+ const labelWidth = getRightAxisLabelWidth(chartRight, Math.ceil(ctx.measureText(text).width) + labelPaddingX * 2);
2236
+ const labelX = getRightAxisLabelX(chartRight);
2237
+ const labelY = clamp(yFromPaneValue(label.value) - labelHeight / 2, paneTop + 2, paneBottom - labelHeight - 2);
2238
+ ctx.fillStyle = label.backgroundColor ?? label.color ?? labels.backgroundColor;
2239
+ fillRoundedRect(Math.round(labelX), Math.round(labelY), labelWidth, labelHeight, Math.max(0, labels.borderRadius));
2240
+ drawText(text, labelX + labelPaddingX, labelY + labelHeight / 2, "left", "middle", label.textColor ?? labels.textColor);
2241
+ }
2242
+ ctx.font = prevFont;
2243
+ }
2244
+ }
2065
2245
  });
2066
2246
  }
2067
2247
  if (crosshair.visible && crosshairPoint && crosshair.showVertical) {
@@ -2093,7 +2273,6 @@ function createChart(element, options = {}) {
2093
2273
  drawText(formatPrice(price), chartRight + 6, y, "left", "middle", yAxis.textColor);
2094
2274
  ctx.font = prevFont;
2095
2275
  }
2096
- const labels = { ...DEFAULT_LABELS_OPTIONS, ...mergedOptions.labels ?? {} };
2097
2276
  resetLabelSlots(labels.noOverlapping);
2098
2277
  const priceAxisLabels = [];
2099
2278
  const addPriceAxisLabel = (label) => {
@@ -2257,12 +2436,13 @@ function createChart(element, options = {}) {
2257
2436
  ctx.font = baseFont;
2258
2437
  }
2259
2438
  const labelTextWidth = Math.max(primaryWidth, subtextWidth);
2439
+ const labelWidth = getRightAxisLabelWidth(chartRight, labelTextWidth);
2260
2440
  return {
2261
2441
  ...label,
2262
2442
  subtexts,
2263
2443
  subtextFontSize,
2264
2444
  height: labelHeight,
2265
- width: labelTextWidth,
2445
+ width: labelWidth,
2266
2446
  targetY: clamp(yFromPrice(label.price), chartTop + 1, chartBottom - 1) - labelHeight / 2,
2267
2447
  y: 0
2268
2448
  };
@@ -2292,7 +2472,7 @@ function createChart(element, options = {}) {
2292
2472
  );
2293
2473
  rightAxisLabelSlots.sort((a, b) => a.y - b.y);
2294
2474
  }
2295
- const labelX = chartRight + 4;
2475
+ const labelX = getRightAxisLabelX(chartRight);
2296
2476
  for (const label of positionedLabels) {
2297
2477
  ctx.fillStyle = label.backgroundColor;
2298
2478
  fillRoundedRect(Math.round(labelX), Math.round(label.y), label.width, label.height, labelRadius);
@@ -2337,7 +2517,14 @@ function createChart(element, options = {}) {
2337
2517
  const prevFont = ctx.font;
2338
2518
  ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
2339
2519
  const legendText = labelEntries.join(" ");
2340
- drawText(legendText, chartLeft + 10, chartTop + 10, "left", "top", labels.indicatorTextColor);
2520
+ const offsetX = Math.max(0, Number(labels.indicatorLegendOffsetX) || 0);
2521
+ const offsetY = Math.max(0, Number(labels.indicatorLegendOffsetY) || 0);
2522
+ const position = labels.indicatorLegendPosition;
2523
+ const isRight = position === "top-right" || position === "bottom-right";
2524
+ const isBottom = position === "bottom-left" || position === "bottom-right";
2525
+ const legendX = isRight ? chartRight - offsetX : chartLeft + offsetX;
2526
+ const legendY = isBottom ? chartBottom - offsetY : chartTop + offsetY;
2527
+ drawText(legendText, legendX, legendY, isRight ? "right" : "left", isBottom ? "bottom" : "top", labels.indicatorTextColor);
2341
2528
  ctx.font = prevFont;
2342
2529
  }
2343
2530
  }
@@ -2395,8 +2582,8 @@ function createChart(element, options = {}) {
2395
2582
  if (crosshair.showPriceLabel) {
2396
2583
  const hoverPrice = yMin + (chartBottom - cy) / chartHeight * yRange;
2397
2584
  const priceText = formatPrice(hoverPrice);
2398
- const priceWidth = getPriceLabelWidth(priceText, labelPaddingX);
2399
- const priceX = chartRight + 4;
2585
+ const priceWidth = getRightAxisLabelWidth(chartRight, getPriceLabelWidth(priceText, labelPaddingX));
2586
+ const priceX = getRightAxisLabelX(chartRight);
2400
2587
  const priceY = clamp(cy - labelHeight / 2, chartTop, chartBottom - labelHeight);
2401
2588
  ctx.fillStyle = labelBackground;
2402
2589
  fillRoundedRect(Math.round(priceX), Math.round(priceY), priceWidth, labelHeight, labelRadius);