hyperprop-charting-library 0.1.49 → 0.1.50

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