hyperprop-charting-library 0.1.147 → 0.1.149

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/dist/index.js CHANGED
@@ -4261,6 +4261,8 @@ function createChart(element, options = {}) {
4261
4261
  let paneButtonRegions = [];
4262
4262
  let hoveredPaneId = null;
4263
4263
  let hoveredPaneButton = null;
4264
+ let legendRowRegions = [];
4265
+ let hoveredLegendId = null;
4264
4266
  let paneDividerDrag = null;
4265
4267
  let indicatorPaneActionHandler = null;
4266
4268
  let indicatorPaneHeightChangeHandler = null;
@@ -4268,6 +4270,7 @@ function createChart(element, options = {}) {
4268
4270
  let actionDragState = null;
4269
4271
  let pointerDownInfo = null;
4270
4272
  let crosshairPoint = null;
4273
+ let externalCrosshair = null;
4271
4274
  let doubleClickEnabled = mergedOptions.doubleClickEnabled;
4272
4275
  let doubleClickAction = mergedOptions.doubleClickAction;
4273
4276
  setIndicatorLiveThrottleMs(mergedOptions.indicatorUpdate?.liveThrottleMs ?? 80);
@@ -5557,6 +5560,85 @@ function createChart(element, options = {}) {
5557
5560
  ctx.closePath();
5558
5561
  ctx.stroke();
5559
5562
  };
5563
+ const INDICATOR_CONTROL_SIZE = 16;
5564
+ const INDICATOR_CONTROL_GAP = 4;
5565
+ const LEGEND_CONTROLS_WIDTH = (INDICATOR_CONTROL_SIZE + INDICATOR_CONTROL_GAP) * 4 + 8;
5566
+ const drawIndicatorControls = (indicatorId, indicatorType, indicatorVisible, startX, centerY) => {
5567
+ const labels = resolvedLabels;
5568
+ const actionList = [
5569
+ "visibility",
5570
+ "settings",
5571
+ "source",
5572
+ "remove"
5573
+ ];
5574
+ let buttonX = startX;
5575
+ for (const action of actionList) {
5576
+ const buttonY = centerY - INDICATOR_CONTROL_SIZE / 2;
5577
+ const hovered = hoveredPaneButton?.id === indicatorId && hoveredPaneButton.action === action;
5578
+ const iconColor = labels.indicatorTextColor;
5579
+ ctx.save();
5580
+ ctx.fillStyle = hovered ? "rgba(148,163,184,0.30)" : "rgba(148,163,184,0.12)";
5581
+ fillRoundedRect(Math.round(buttonX), Math.round(buttonY), INDICATOR_CONTROL_SIZE, INDICATOR_CONTROL_SIZE, 4);
5582
+ ctx.strokeStyle = iconColor;
5583
+ ctx.fillStyle = iconColor;
5584
+ ctx.globalAlpha = hovered ? 1 : 0.75;
5585
+ ctx.lineWidth = 1.2;
5586
+ ctx.setLineDash([]);
5587
+ const cx = buttonX + INDICATOR_CONTROL_SIZE / 2;
5588
+ const cy = centerY;
5589
+ if (action === "visibility") {
5590
+ ctx.beginPath();
5591
+ ctx.ellipse(cx, cy, 4.6, 3, 0, 0, Math.PI * 2);
5592
+ ctx.stroke();
5593
+ ctx.beginPath();
5594
+ ctx.arc(cx, cy, 1.3, 0, Math.PI * 2);
5595
+ ctx.fill();
5596
+ if (!indicatorVisible) {
5597
+ ctx.beginPath();
5598
+ ctx.moveTo(cx - 5.2, cy + 4.6);
5599
+ ctx.lineTo(cx + 5.2, cy - 4.6);
5600
+ ctx.stroke();
5601
+ }
5602
+ } else if (action === "settings") {
5603
+ ctx.beginPath();
5604
+ ctx.arc(cx, cy, 3.4, 0, Math.PI * 2);
5605
+ ctx.stroke();
5606
+ for (let toothIndex = 0; toothIndex < 8; toothIndex += 1) {
5607
+ const angle = Math.PI / 4 * toothIndex;
5608
+ ctx.beginPath();
5609
+ ctx.moveTo(cx + Math.cos(angle) * 3.4, cy + Math.sin(angle) * 3.4);
5610
+ ctx.lineTo(cx + Math.cos(angle) * 5.2, cy + Math.sin(angle) * 5.2);
5611
+ ctx.stroke();
5612
+ }
5613
+ ctx.beginPath();
5614
+ ctx.arc(cx, cy, 1.1, 0, Math.PI * 2);
5615
+ ctx.fill();
5616
+ } else if (action === "source") {
5617
+ const prevSourceFont = ctx.font;
5618
+ ctx.font = `bold 9px ${mergedOptions.fontFamily}`;
5619
+ drawText("{}", cx, cy + 0.5, "center", "middle", iconColor);
5620
+ ctx.font = prevSourceFont;
5621
+ } else {
5622
+ ctx.beginPath();
5623
+ ctx.moveTo(cx - 3.4, cy - 3.4);
5624
+ ctx.lineTo(cx + 3.4, cy + 3.4);
5625
+ ctx.moveTo(cx + 3.4, cy - 3.4);
5626
+ ctx.lineTo(cx - 3.4, cy + 3.4);
5627
+ ctx.stroke();
5628
+ }
5629
+ ctx.restore();
5630
+ paneButtonRegions.push({
5631
+ id: indicatorId,
5632
+ type: indicatorType,
5633
+ action,
5634
+ x: buttonX,
5635
+ y: buttonY,
5636
+ width: INDICATOR_CONTROL_SIZE,
5637
+ height: INDICATOR_CONTROL_SIZE
5638
+ });
5639
+ buttonX += INDICATOR_CONTROL_SIZE + INDICATOR_CONTROL_GAP;
5640
+ }
5641
+ };
5560
5642
  const drawMarkBadge = (x, y, radius, color, shape, label, textColor) => {
5561
5643
  ctx.save();
5562
5644
  ctx.fillStyle = color;
@@ -5637,6 +5719,8 @@ function createChart(element, options = {}) {
5637
5719
  orderDragRegions = [];
5638
5720
  alertRegions = [];
5639
5721
  markRegions = [];
5722
+ paneButtonRegions = [];
5723
+ legendRowRegions = [];
5640
5724
  crosshairPriceActionRegion = null;
5641
5725
  const pixelRatio = getPixelRatio();
5642
5726
  canvas.style.width = `${width}px`;
@@ -7725,87 +7809,10 @@ function createChart(element, options = {}) {
7725
7809
  }
7726
7810
  paneLayoutFullChartHeight = fullChartHeight;
7727
7811
  paneLayoutInfos = [];
7728
- paneButtonRegions = [];
7729
7812
  if (activeSeparateIndicators.length > 0) {
7730
7813
  const xFromIndex = (index) => chartLeft + (index + 0.5 - xStart) / xSpan * chartWidth;
7731
7814
  let paneTopCursor = chartBottom + paneGap;
7732
- const paneButtonSize = 16;
7733
- const paneButtonGap = 4;
7734
- const drawPaneButtons = (indicatorId, indicatorType, indicatorVisible, startX, centerY) => {
7735
- const actionList = [
7736
- "visibility",
7737
- "settings",
7738
- "source",
7739
- "remove"
7740
- ];
7741
- let buttonX = startX;
7742
- for (const action of actionList) {
7743
- const buttonY = centerY - paneButtonSize / 2;
7744
- const hovered = hoveredPaneButton?.id === indicatorId && hoveredPaneButton.action === action;
7745
- const iconColor = labels.indicatorTextColor;
7746
- ctx.save();
7747
- ctx.fillStyle = hovered ? "rgba(148,163,184,0.30)" : "rgba(148,163,184,0.12)";
7748
- fillRoundedRect(Math.round(buttonX), Math.round(buttonY), paneButtonSize, paneButtonSize, 4);
7749
- ctx.strokeStyle = iconColor;
7750
- ctx.fillStyle = iconColor;
7751
- ctx.globalAlpha = hovered ? 1 : 0.75;
7752
- ctx.lineWidth = 1.2;
7753
- ctx.setLineDash([]);
7754
- const cx = buttonX + paneButtonSize / 2;
7755
- const cy = centerY;
7756
- if (action === "visibility") {
7757
- ctx.beginPath();
7758
- ctx.ellipse(cx, cy, 4.6, 3, 0, 0, Math.PI * 2);
7759
- ctx.stroke();
7760
- ctx.beginPath();
7761
- ctx.arc(cx, cy, 1.3, 0, Math.PI * 2);
7762
- ctx.fill();
7763
- if (!indicatorVisible) {
7764
- ctx.beginPath();
7765
- ctx.moveTo(cx - 5.2, cy + 4.6);
7766
- ctx.lineTo(cx + 5.2, cy - 4.6);
7767
- ctx.stroke();
7768
- }
7769
- } else if (action === "settings") {
7770
- ctx.beginPath();
7771
- ctx.arc(cx, cy, 3.4, 0, Math.PI * 2);
7772
- ctx.stroke();
7773
- for (let toothIndex = 0; toothIndex < 8; toothIndex += 1) {
7774
- const angle = Math.PI / 4 * toothIndex;
7775
- ctx.beginPath();
7776
- ctx.moveTo(cx + Math.cos(angle) * 3.4, cy + Math.sin(angle) * 3.4);
7777
- ctx.lineTo(cx + Math.cos(angle) * 5.2, cy + Math.sin(angle) * 5.2);
7778
- ctx.stroke();
7779
- }
7780
- ctx.beginPath();
7781
- ctx.arc(cx, cy, 1.1, 0, Math.PI * 2);
7782
- ctx.fill();
7783
- } else if (action === "source") {
7784
- const prevSourceFont = ctx.font;
7785
- ctx.font = `bold 9px ${mergedOptions.fontFamily}`;
7786
- drawText("{}", cx, cy + 0.5, "center", "middle", iconColor);
7787
- ctx.font = prevSourceFont;
7788
- } else {
7789
- ctx.beginPath();
7790
- ctx.moveTo(cx - 3.4, cy - 3.4);
7791
- ctx.lineTo(cx + 3.4, cy + 3.4);
7792
- ctx.moveTo(cx + 3.4, cy - 3.4);
7793
- ctx.lineTo(cx - 3.4, cy + 3.4);
7794
- ctx.stroke();
7795
- }
7796
- ctx.restore();
7797
- paneButtonRegions.push({
7798
- id: indicatorId,
7799
- type: indicatorType,
7800
- action,
7801
- x: buttonX,
7802
- y: buttonY,
7803
- width: paneButtonSize,
7804
- height: paneButtonSize
7805
- });
7806
- buttonX += paneButtonSize + paneButtonGap;
7807
- }
7808
- };
7815
+ const drawPaneButtons = drawIndicatorControls;
7809
7816
  activeSeparateIndicators.forEach(({ indicator, plugin }, paneIndex) => {
7810
7817
  const paneHeight = separatePaneHeights[paneIndex] ?? 80;
7811
7818
  const paneTop = paneTopCursor;
@@ -8307,26 +8314,56 @@ function createChart(element, options = {}) {
8307
8314
  };
8308
8315
  const labelEntries = activeOverlayIndicators.map(({ indicator, plugin }) => {
8309
8316
  const inputValues = Object.entries(indicator.inputs).filter(([key, value]) => !LEGEND_EXCLUDED_INPUT_KEYS.has(key) && typeof value !== "boolean" && isLegendInputValue(value)).slice(0, 2).map(([, value]) => formatLegendValue(value));
8310
- if (labels.showIndicatorNames && labels.showIndicatorValues && inputValues.length > 0) {
8311
- return `${plugin.name} ${inputValues.join(" ")}`;
8312
- }
8313
- if (labels.showIndicatorNames) {
8314
- return plugin.name;
8315
- }
8316
- return inputValues.join(" ");
8317
- }).filter((entry) => entry.length > 0);
8317
+ const text = labels.showIndicatorNames && labels.showIndicatorValues && inputValues.length > 0 ? `${plugin.name} ${inputValues.join(" ")}` : labels.showIndicatorNames ? plugin.name : inputValues.join(" ");
8318
+ return { id: indicator.id, type: indicator.type, visible: indicator.visible, text };
8319
+ }).filter((entry) => entry.text.length > 0);
8318
8320
  if (labelEntries.length > 0) {
8319
8321
  const prevFont = ctx.font;
8320
- ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
8321
- const legendText = labelEntries.join(" ");
8322
+ const legendFontSize = Math.max(8, axis.fontSize);
8323
+ ctx.font = `${legendFontSize}px ${mergedOptions.fontFamily}`;
8322
8324
  const offsetX = Math.max(0, Number(labels.indicatorLegendOffsetX) || 0);
8323
8325
  const offsetY = Math.max(0, Number(labels.indicatorLegendOffsetY) || 0);
8324
8326
  const position = labels.indicatorLegendPosition;
8325
8327
  const isRight = position === "top-right" || position === "bottom-right";
8326
8328
  const isBottom = position === "bottom-left" || position === "bottom-right";
8327
8329
  const legendX = isRight ? chartRight - offsetX : chartLeft + offsetX;
8328
- const legendY = isBottom ? chartBottom - offsetY : Math.max(chartTop + offsetY, dataLineBottom);
8329
- drawText(legendText, legendX, legendY, isRight ? "right" : "left", isBottom ? "bottom" : "top", labels.indicatorTextColor);
8330
+ const legendTop = isBottom ? chartBottom - offsetY : Math.max(chartTop + offsetY, dataLineBottom);
8331
+ const rowHeight = Math.round(legendFontSize * 1.55);
8332
+ labelEntries.forEach((entry, rowIndex) => {
8333
+ const rowTop = isBottom ? legendTop - (labelEntries.length - rowIndex) * rowHeight : legendTop + rowIndex * rowHeight;
8334
+ const rowCenter = rowTop + rowHeight / 2;
8335
+ const dimmed = entry.visible === false;
8336
+ ctx.save();
8337
+ if (dimmed) ctx.globalAlpha = 0.45;
8338
+ drawText(
8339
+ entry.text,
8340
+ legendX,
8341
+ rowCenter,
8342
+ isRight ? "right" : "left",
8343
+ "middle",
8344
+ labels.indicatorTextColor
8345
+ );
8346
+ ctx.restore();
8347
+ const textWidth = measureTextWidth(entry.text);
8348
+ const rowLeft = isRight ? legendX - textWidth : legendX;
8349
+ legendRowRegions.push({
8350
+ id: entry.id,
8351
+ type: entry.type,
8352
+ x: rowLeft,
8353
+ y: rowTop,
8354
+ width: textWidth + LEGEND_CONTROLS_WIDTH,
8355
+ height: rowHeight
8356
+ });
8357
+ if (hoveredLegendId === entry.id) {
8358
+ drawIndicatorControls(
8359
+ entry.id,
8360
+ entry.type,
8361
+ entry.visible !== false,
8362
+ isRight ? legendX + 6 : rowLeft + textWidth + 8,
8363
+ rowCenter
8364
+ );
8365
+ }
8366
+ });
8330
8367
  ctx.font = prevFont;
8331
8368
  }
8332
8369
  }
@@ -8436,7 +8473,26 @@ function createChart(element, options = {}) {
8436
8473
  crosshairPriceActionRegion = null;
8437
8474
  const layout = overlayLayout;
8438
8475
  const crosshair = resolvedCrosshair;
8439
- if (!layout || !crosshair.visible || !crosshairPoint) {
8476
+ if (!layout) return;
8477
+ if (!crosshairPoint && externalCrosshair !== null && crosshair.visible) {
8478
+ const index = findNearestIndexForTimeMs(externalCrosshair.timeMs);
8479
+ if (index !== null) {
8480
+ const x = layout.chartLeft + (index + 0.5 - layout.xStart) / layout.xSpan * layout.chartWidth;
8481
+ if (x >= layout.chartLeft && x <= layout.chartRight) {
8482
+ ctx.save();
8483
+ ctx.globalAlpha = 0.55;
8484
+ ctx.strokeStyle = crosshair.color;
8485
+ ctx.lineWidth = Math.max(1, crosshair.width);
8486
+ applyDashPattern(crosshair.style, dashPatterns.dotted, dashPatterns.dashed);
8487
+ ctx.beginPath();
8488
+ ctx.moveTo(crisp(x), crisp(layout.chartTop));
8489
+ ctx.lineTo(crisp(x), crisp(layout.fullChartBottom));
8490
+ ctx.stroke();
8491
+ ctx.restore();
8492
+ }
8493
+ }
8494
+ }
8495
+ if (!crosshair.visible || !crosshairPoint) {
8440
8496
  return;
8441
8497
  }
8442
8498
  const { chartLeft, chartTop, chartRight, chartBottom, chartWidth, chartHeight, fullChartBottom, yMin, yRange, xStart, xSpan: xSpan2 } = layout;
@@ -8998,6 +9054,9 @@ function createChart(element, options = {}) {
8998
9054
  if (hits.length === 0) return null;
8999
9055
  return hits.find((region) => region.part === "remove") ?? hits[0];
9000
9056
  };
9057
+ const getLegendRowAt = (x, y) => legendRowRegions.find(
9058
+ (region) => x >= region.x && x <= region.x + region.width && y >= region.y && y <= region.y + region.height
9059
+ ) ?? null;
9001
9060
  const getMarkAt = (x, y) => {
9002
9061
  const tolerance = touchInputActive ? Math.max(1, mergedOptions.touch?.hitToleranceScale ?? 2.2) : 1;
9003
9062
  for (let index = markRegions.length - 1; index >= 0; index -= 1) {
@@ -10747,6 +10806,12 @@ function createChart(element, options = {}) {
10747
10806
  setCrosshairPoint(null);
10748
10807
  return;
10749
10808
  }
10809
+ const legendRow = getLegendRowAt(point.x, point.y);
10810
+ const nextLegendId = legendRow?.id ?? null;
10811
+ if (nextLegendId !== hoveredLegendId) {
10812
+ hoveredLegendId = nextLegendId;
10813
+ scheduleDraw();
10814
+ }
10750
10815
  const hoveredPane = getPaneAt(point.x, point.y);
10751
10816
  const nextHoveredPaneId = hoveredPane?.id ?? null;
10752
10817
  if (nextHoveredPaneId !== hoveredPaneId) {
@@ -10965,10 +11030,11 @@ function createChart(element, options = {}) {
10965
11030
  });
10966
11031
  paneDividerDrag = null;
10967
11032
  }
10968
- if (event?.type === "pointerleave" && (hoveredPaneId !== null || hoveredPaneButton !== null || hoveredAlertId !== null || hoveredMarkKey !== null)) {
11033
+ if (event?.type === "pointerleave" && (hoveredPaneId !== null || hoveredPaneButton !== null || hoveredAlertId !== null || hoveredMarkKey !== null || hoveredLegendId !== null)) {
10969
11034
  hoveredPaneId = null;
10970
11035
  hoveredPaneButton = null;
10971
11036
  hoveredAlertId = null;
11037
+ hoveredLegendId = null;
10972
11038
  if (hoveredMarkKey !== null) {
10973
11039
  hoveredMarkKey = null;
10974
11040
  markHoverHandler?.(null);
@@ -11908,6 +11974,12 @@ function createChart(element, options = {}) {
11908
11974
  const onAlertAction = (handler) => {
11909
11975
  alertActionHandler = handler;
11910
11976
  };
11977
+ const setExternalCrosshair = (point) => {
11978
+ const nextMs = point && Number.isFinite(point.timeMs) ? point.timeMs : null;
11979
+ if (nextMs === (externalCrosshair?.timeMs ?? null)) return;
11980
+ externalCrosshair = nextMs === null ? null : { timeMs: nextMs };
11981
+ scheduleDraw({ overlayOnly: true });
11982
+ };
11911
11983
  const setTimezone = (timezone) => {
11912
11984
  timezoneSetting = timezone || "local";
11913
11985
  zoneClock = createZoneClock(timezoneSetting);
@@ -12218,6 +12290,7 @@ function createChart(element, options = {}) {
12218
12290
  saveState,
12219
12291
  loadState,
12220
12292
  takeScreenshot,
12293
+ setExternalCrosshair,
12221
12294
  setTimezone,
12222
12295
  getTimezone,
12223
12296
  setTimeFormat,
package/docs/API.md CHANGED
@@ -690,6 +690,27 @@ runs 18:00–17:00, but the levels traders mark are relative to the 09:30 equity
690
690
  open inside it. Everything in the current session outside those hours counts as
691
691
  overnight.
692
692
 
693
+ ### Fixed-range volume profile (drawing tool)
694
+
695
+ The third profile variant is a *drawing* rather than an indicator, because it
696
+ needs a range: arm `fixed-range-volume-profile` and click twice to bracket the
697
+ bars. Only the anchors' bar indexes matter — the price extent comes from the
698
+ bars they enclose.
699
+
700
+ ```ts
701
+ chart.setActiveDrawingTool("fixed-range-volume-profile");
702
+ chart.setDrawingDefaults("fixed-range-volume-profile", {
703
+ color: "#5b8def",
704
+ profileRows: 24,
705
+ profileValueAreaPercent: 70,
706
+ profileWidthRatio: 0.5
707
+ });
708
+ ```
709
+
710
+ It draws the selected span, the volume rows (buy/sell split, brighter inside
711
+ the value area) growing right from the range start, and POC/VAH/VAL. Being a
712
+ drawing, it is draggable, undoable and part of `saveState()` like any other.
713
+
693
714
  ### `anchored-vwap`
694
715
 
695
716
  VWAP measured from a bar you choose rather than the session open.
@@ -860,6 +881,28 @@ they are not part of `saveState()`.
860
881
 
861
882
  ---
862
883
 
884
+ ## Overlay indicator legend
885
+
886
+ With `labels.showIndicatorNames` / `showIndicatorValues`, overlay indicators
887
+ are listed top-left, one row each. Hovering a row reveals the same four
888
+ controls the separate panes have — eye, gear, `{}`, X — and they emit the same
889
+ `onIndicatorPaneAction` event, so a host that already handles pane actions gets
890
+ the overlay legend for free:
891
+
892
+ ```ts
893
+ chart.updateOptions({ labels: { showIndicatorNames: true, showIndicatorValues: true } });
894
+ chart.onIndicatorPaneAction(({ id, action }) => {
895
+ if (action === "remove") store.removeIndicator(id);
896
+ if (action === "settings") store.openSettings(id);
897
+ });
898
+ ```
899
+
900
+ The eye also toggles visibility internally so it works without a host, and
901
+ hidden indicators render their row dimmed. Position and offsets come from
902
+ `labels.indicatorLegendPosition` / `indicatorLegendOffsetX` / `OffsetY`.
903
+
904
+ ---
905
+
863
906
  ## In-chart data line
864
907
 
865
908
  The symbol/OHLC line TradingView draws in the top-left of the plot. Hosts have
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hyperprop-charting-library",
3
- "version": "0.1.147",
3
+ "version": "0.1.149",
4
4
  "description": "Lightweight TypeScript charting core",
5
5
  "type": "module",
6
6
  "main": "./dist/hyperprop-charting-library.cjs",