hyperprop-charting-library 0.1.147 → 0.1.148

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.
@@ -4297,6 +4297,8 @@ function createChart(element, options = {}) {
4297
4297
  let paneButtonRegions = [];
4298
4298
  let hoveredPaneId = null;
4299
4299
  let hoveredPaneButton = null;
4300
+ let legendRowRegions = [];
4301
+ let hoveredLegendId = null;
4300
4302
  let paneDividerDrag = null;
4301
4303
  let indicatorPaneActionHandler = null;
4302
4304
  let indicatorPaneHeightChangeHandler = null;
@@ -5593,6 +5595,85 @@ function createChart(element, options = {}) {
5593
5595
  ctx.closePath();
5594
5596
  ctx.stroke();
5595
5597
  };
5598
+ const INDICATOR_CONTROL_SIZE = 16;
5599
+ const INDICATOR_CONTROL_GAP = 4;
5600
+ const LEGEND_CONTROLS_WIDTH = (INDICATOR_CONTROL_SIZE + INDICATOR_CONTROL_GAP) * 4 + 8;
5601
+ const drawIndicatorControls = (indicatorId, indicatorType, indicatorVisible, startX, centerY) => {
5602
+ const labels = resolvedLabels;
5603
+ const actionList = [
5604
+ "visibility",
5605
+ "settings",
5606
+ "source",
5607
+ "remove"
5608
+ ];
5609
+ let buttonX = startX;
5610
+ for (const action of actionList) {
5611
+ const buttonY = centerY - INDICATOR_CONTROL_SIZE / 2;
5612
+ const hovered = hoveredPaneButton?.id === indicatorId && hoveredPaneButton.action === action;
5613
+ const iconColor = labels.indicatorTextColor;
5614
+ ctx.save();
5615
+ ctx.fillStyle = hovered ? "rgba(148,163,184,0.30)" : "rgba(148,163,184,0.12)";
5616
+ fillRoundedRect(Math.round(buttonX), Math.round(buttonY), INDICATOR_CONTROL_SIZE, INDICATOR_CONTROL_SIZE, 4);
5617
+ ctx.strokeStyle = iconColor;
5618
+ ctx.fillStyle = iconColor;
5619
+ ctx.globalAlpha = hovered ? 1 : 0.75;
5620
+ ctx.lineWidth = 1.2;
5621
+ ctx.setLineDash([]);
5622
+ const cx = buttonX + INDICATOR_CONTROL_SIZE / 2;
5623
+ const cy = centerY;
5624
+ if (action === "visibility") {
5625
+ ctx.beginPath();
5626
+ ctx.ellipse(cx, cy, 4.6, 3, 0, 0, Math.PI * 2);
5627
+ ctx.stroke();
5628
+ ctx.beginPath();
5629
+ ctx.arc(cx, cy, 1.3, 0, Math.PI * 2);
5630
+ ctx.fill();
5631
+ if (!indicatorVisible) {
5632
+ ctx.beginPath();
5633
+ ctx.moveTo(cx - 5.2, cy + 4.6);
5634
+ ctx.lineTo(cx + 5.2, cy - 4.6);
5635
+ ctx.stroke();
5636
+ }
5637
+ } else if (action === "settings") {
5638
+ ctx.beginPath();
5639
+ ctx.arc(cx, cy, 3.4, 0, Math.PI * 2);
5640
+ ctx.stroke();
5641
+ for (let toothIndex = 0; toothIndex < 8; toothIndex += 1) {
5642
+ const angle = Math.PI / 4 * toothIndex;
5643
+ ctx.beginPath();
5644
+ ctx.moveTo(cx + Math.cos(angle) * 3.4, cy + Math.sin(angle) * 3.4);
5645
+ ctx.lineTo(cx + Math.cos(angle) * 5.2, cy + Math.sin(angle) * 5.2);
5646
+ ctx.stroke();
5647
+ }
5648
+ ctx.beginPath();
5649
+ ctx.arc(cx, cy, 1.1, 0, Math.PI * 2);
5650
+ ctx.fill();
5651
+ } else if (action === "source") {
5652
+ const prevSourceFont = ctx.font;
5653
+ ctx.font = `bold 9px ${mergedOptions.fontFamily}`;
5654
+ drawText("{}", cx, cy + 0.5, "center", "middle", iconColor);
5655
+ ctx.font = prevSourceFont;
5656
+ } else {
5657
+ ctx.beginPath();
5658
+ ctx.moveTo(cx - 3.4, cy - 3.4);
5659
+ ctx.lineTo(cx + 3.4, cy + 3.4);
5660
+ ctx.moveTo(cx + 3.4, cy - 3.4);
5661
+ ctx.lineTo(cx - 3.4, cy + 3.4);
5662
+ ctx.stroke();
5663
+ }
5664
+ ctx.restore();
5665
+ paneButtonRegions.push({
5666
+ id: indicatorId,
5667
+ type: indicatorType,
5668
+ action,
5669
+ x: buttonX,
5670
+ y: buttonY,
5671
+ width: INDICATOR_CONTROL_SIZE,
5672
+ height: INDICATOR_CONTROL_SIZE
5673
+ });
5674
+ buttonX += INDICATOR_CONTROL_SIZE + INDICATOR_CONTROL_GAP;
5675
+ }
5676
+ };
5596
5677
  const drawMarkBadge = (x, y, radius, color, shape, label, textColor) => {
5597
5678
  ctx.save();
5598
5679
  ctx.fillStyle = color;
@@ -5673,6 +5754,8 @@ function createChart(element, options = {}) {
5673
5754
  orderDragRegions = [];
5674
5755
  alertRegions = [];
5675
5756
  markRegions = [];
5757
+ paneButtonRegions = [];
5758
+ legendRowRegions = [];
5676
5759
  crosshairPriceActionRegion = null;
5677
5760
  const pixelRatio = getPixelRatio();
5678
5761
  canvas.style.width = `${width}px`;
@@ -7761,87 +7844,10 @@ function createChart(element, options = {}) {
7761
7844
  }
7762
7845
  paneLayoutFullChartHeight = fullChartHeight;
7763
7846
  paneLayoutInfos = [];
7764
- paneButtonRegions = [];
7765
7847
  if (activeSeparateIndicators.length > 0) {
7766
7848
  const xFromIndex = (index) => chartLeft + (index + 0.5 - xStart) / xSpan * chartWidth;
7767
7849
  let paneTopCursor = chartBottom + paneGap;
7768
- const paneButtonSize = 16;
7769
- const paneButtonGap = 4;
7770
- const drawPaneButtons = (indicatorId, indicatorType, indicatorVisible, startX, centerY) => {
7771
- const actionList = [
7772
- "visibility",
7773
- "settings",
7774
- "source",
7775
- "remove"
7776
- ];
7777
- let buttonX = startX;
7778
- for (const action of actionList) {
7779
- const buttonY = centerY - paneButtonSize / 2;
7780
- const hovered = hoveredPaneButton?.id === indicatorId && hoveredPaneButton.action === action;
7781
- const iconColor = labels.indicatorTextColor;
7782
- ctx.save();
7783
- ctx.fillStyle = hovered ? "rgba(148,163,184,0.30)" : "rgba(148,163,184,0.12)";
7784
- fillRoundedRect(Math.round(buttonX), Math.round(buttonY), paneButtonSize, paneButtonSize, 4);
7785
- ctx.strokeStyle = iconColor;
7786
- ctx.fillStyle = iconColor;
7787
- ctx.globalAlpha = hovered ? 1 : 0.75;
7788
- ctx.lineWidth = 1.2;
7789
- ctx.setLineDash([]);
7790
- const cx = buttonX + paneButtonSize / 2;
7791
- const cy = centerY;
7792
- if (action === "visibility") {
7793
- ctx.beginPath();
7794
- ctx.ellipse(cx, cy, 4.6, 3, 0, 0, Math.PI * 2);
7795
- ctx.stroke();
7796
- ctx.beginPath();
7797
- ctx.arc(cx, cy, 1.3, 0, Math.PI * 2);
7798
- ctx.fill();
7799
- if (!indicatorVisible) {
7800
- ctx.beginPath();
7801
- ctx.moveTo(cx - 5.2, cy + 4.6);
7802
- ctx.lineTo(cx + 5.2, cy - 4.6);
7803
- ctx.stroke();
7804
- }
7805
- } else if (action === "settings") {
7806
- ctx.beginPath();
7807
- ctx.arc(cx, cy, 3.4, 0, Math.PI * 2);
7808
- ctx.stroke();
7809
- for (let toothIndex = 0; toothIndex < 8; toothIndex += 1) {
7810
- const angle = Math.PI / 4 * toothIndex;
7811
- ctx.beginPath();
7812
- ctx.moveTo(cx + Math.cos(angle) * 3.4, cy + Math.sin(angle) * 3.4);
7813
- ctx.lineTo(cx + Math.cos(angle) * 5.2, cy + Math.sin(angle) * 5.2);
7814
- ctx.stroke();
7815
- }
7816
- ctx.beginPath();
7817
- ctx.arc(cx, cy, 1.1, 0, Math.PI * 2);
7818
- ctx.fill();
7819
- } else if (action === "source") {
7820
- const prevSourceFont = ctx.font;
7821
- ctx.font = `bold 9px ${mergedOptions.fontFamily}`;
7822
- drawText("{}", cx, cy + 0.5, "center", "middle", iconColor);
7823
- ctx.font = prevSourceFont;
7824
- } else {
7825
- ctx.beginPath();
7826
- ctx.moveTo(cx - 3.4, cy - 3.4);
7827
- ctx.lineTo(cx + 3.4, cy + 3.4);
7828
- ctx.moveTo(cx + 3.4, cy - 3.4);
7829
- ctx.lineTo(cx - 3.4, cy + 3.4);
7830
- ctx.stroke();
7831
- }
7832
- ctx.restore();
7833
- paneButtonRegions.push({
7834
- id: indicatorId,
7835
- type: indicatorType,
7836
- action,
7837
- x: buttonX,
7838
- y: buttonY,
7839
- width: paneButtonSize,
7840
- height: paneButtonSize
7841
- });
7842
- buttonX += paneButtonSize + paneButtonGap;
7843
- }
7844
- };
7850
+ const drawPaneButtons = drawIndicatorControls;
7845
7851
  activeSeparateIndicators.forEach(({ indicator, plugin }, paneIndex) => {
7846
7852
  const paneHeight = separatePaneHeights[paneIndex] ?? 80;
7847
7853
  const paneTop = paneTopCursor;
@@ -8343,26 +8349,56 @@ function createChart(element, options = {}) {
8343
8349
  };
8344
8350
  const labelEntries = activeOverlayIndicators.map(({ indicator, plugin }) => {
8345
8351
  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));
8346
- if (labels.showIndicatorNames && labels.showIndicatorValues && inputValues.length > 0) {
8347
- return `${plugin.name} ${inputValues.join(" ")}`;
8348
- }
8349
- if (labels.showIndicatorNames) {
8350
- return plugin.name;
8351
- }
8352
- return inputValues.join(" ");
8353
- }).filter((entry) => entry.length > 0);
8352
+ const text = labels.showIndicatorNames && labels.showIndicatorValues && inputValues.length > 0 ? `${plugin.name} ${inputValues.join(" ")}` : labels.showIndicatorNames ? plugin.name : inputValues.join(" ");
8353
+ return { id: indicator.id, type: indicator.type, visible: indicator.visible, text };
8354
+ }).filter((entry) => entry.text.length > 0);
8354
8355
  if (labelEntries.length > 0) {
8355
8356
  const prevFont = ctx.font;
8356
- ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
8357
- const legendText = labelEntries.join(" ");
8357
+ const legendFontSize = Math.max(8, axis.fontSize);
8358
+ ctx.font = `${legendFontSize}px ${mergedOptions.fontFamily}`;
8358
8359
  const offsetX = Math.max(0, Number(labels.indicatorLegendOffsetX) || 0);
8359
8360
  const offsetY = Math.max(0, Number(labels.indicatorLegendOffsetY) || 0);
8360
8361
  const position = labels.indicatorLegendPosition;
8361
8362
  const isRight = position === "top-right" || position === "bottom-right";
8362
8363
  const isBottom = position === "bottom-left" || position === "bottom-right";
8363
8364
  const legendX = isRight ? chartRight - offsetX : chartLeft + offsetX;
8364
- const legendY = isBottom ? chartBottom - offsetY : Math.max(chartTop + offsetY, dataLineBottom);
8365
- drawText(legendText, legendX, legendY, isRight ? "right" : "left", isBottom ? "bottom" : "top", labels.indicatorTextColor);
8365
+ const legendTop = isBottom ? chartBottom - offsetY : Math.max(chartTop + offsetY, dataLineBottom);
8366
+ const rowHeight = Math.round(legendFontSize * 1.55);
8367
+ labelEntries.forEach((entry, rowIndex) => {
8368
+ const rowTop = isBottom ? legendTop - (labelEntries.length - rowIndex) * rowHeight : legendTop + rowIndex * rowHeight;
8369
+ const rowCenter = rowTop + rowHeight / 2;
8370
+ const dimmed = entry.visible === false;
8371
+ ctx.save();
8372
+ if (dimmed) ctx.globalAlpha = 0.45;
8373
+ drawText(
8374
+ entry.text,
8375
+ legendX,
8376
+ rowCenter,
8377
+ isRight ? "right" : "left",
8378
+ "middle",
8379
+ labels.indicatorTextColor
8380
+ );
8381
+ ctx.restore();
8382
+ const textWidth = measureTextWidth(entry.text);
8383
+ const rowLeft = isRight ? legendX - textWidth : legendX;
8384
+ legendRowRegions.push({
8385
+ id: entry.id,
8386
+ type: entry.type,
8387
+ x: rowLeft,
8388
+ y: rowTop,
8389
+ width: textWidth + LEGEND_CONTROLS_WIDTH,
8390
+ height: rowHeight
8391
+ });
8392
+ if (hoveredLegendId === entry.id) {
8393
+ drawIndicatorControls(
8394
+ entry.id,
8395
+ entry.type,
8396
+ entry.visible !== false,
8397
+ isRight ? legendX + 6 : rowLeft + textWidth + 8,
8398
+ rowCenter
8399
+ );
8400
+ }
8401
+ });
8366
8402
  ctx.font = prevFont;
8367
8403
  }
8368
8404
  }
@@ -9034,6 +9070,9 @@ function createChart(element, options = {}) {
9034
9070
  if (hits.length === 0) return null;
9035
9071
  return hits.find((region) => region.part === "remove") ?? hits[0];
9036
9072
  };
9073
+ const getLegendRowAt = (x, y) => legendRowRegions.find(
9074
+ (region) => x >= region.x && x <= region.x + region.width && y >= region.y && y <= region.y + region.height
9075
+ ) ?? null;
9037
9076
  const getMarkAt = (x, y) => {
9038
9077
  const tolerance = touchInputActive ? Math.max(1, mergedOptions.touch?.hitToleranceScale ?? 2.2) : 1;
9039
9078
  for (let index = markRegions.length - 1; index >= 0; index -= 1) {
@@ -10783,6 +10822,12 @@ function createChart(element, options = {}) {
10783
10822
  setCrosshairPoint(null);
10784
10823
  return;
10785
10824
  }
10825
+ const legendRow = getLegendRowAt(point.x, point.y);
10826
+ const nextLegendId = legendRow?.id ?? null;
10827
+ if (nextLegendId !== hoveredLegendId) {
10828
+ hoveredLegendId = nextLegendId;
10829
+ scheduleDraw();
10830
+ }
10786
10831
  const hoveredPane = getPaneAt(point.x, point.y);
10787
10832
  const nextHoveredPaneId = hoveredPane?.id ?? null;
10788
10833
  if (nextHoveredPaneId !== hoveredPaneId) {
@@ -11001,10 +11046,11 @@ function createChart(element, options = {}) {
11001
11046
  });
11002
11047
  paneDividerDrag = null;
11003
11048
  }
11004
- if (event?.type === "pointerleave" && (hoveredPaneId !== null || hoveredPaneButton !== null || hoveredAlertId !== null || hoveredMarkKey !== null)) {
11049
+ if (event?.type === "pointerleave" && (hoveredPaneId !== null || hoveredPaneButton !== null || hoveredAlertId !== null || hoveredMarkKey !== null || hoveredLegendId !== null)) {
11005
11050
  hoveredPaneId = null;
11006
11051
  hoveredPaneButton = null;
11007
11052
  hoveredAlertId = null;
11053
+ hoveredLegendId = null;
11008
11054
  if (hoveredMarkKey !== null) {
11009
11055
  hoveredMarkKey = null;
11010
11056
  markHoverHandler?.(null);
@@ -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;
@@ -5557,6 +5559,85 @@ function createChart(element, options = {}) {
5557
5559
  ctx.closePath();
5558
5560
  ctx.stroke();
5559
5561
  };
5562
+ const INDICATOR_CONTROL_SIZE = 16;
5563
+ const INDICATOR_CONTROL_GAP = 4;
5564
+ const LEGEND_CONTROLS_WIDTH = (INDICATOR_CONTROL_SIZE + INDICATOR_CONTROL_GAP) * 4 + 8;
5565
+ const drawIndicatorControls = (indicatorId, indicatorType, indicatorVisible, startX, centerY) => {
5566
+ const labels = resolvedLabels;
5567
+ const actionList = [
5568
+ "visibility",
5569
+ "settings",
5570
+ "source",
5571
+ "remove"
5572
+ ];
5573
+ let buttonX = startX;
5574
+ for (const action of actionList) {
5575
+ const buttonY = centerY - INDICATOR_CONTROL_SIZE / 2;
5576
+ const hovered = hoveredPaneButton?.id === indicatorId && hoveredPaneButton.action === action;
5577
+ const iconColor = labels.indicatorTextColor;
5578
+ ctx.save();
5579
+ ctx.fillStyle = hovered ? "rgba(148,163,184,0.30)" : "rgba(148,163,184,0.12)";
5580
+ fillRoundedRect(Math.round(buttonX), Math.round(buttonY), INDICATOR_CONTROL_SIZE, INDICATOR_CONTROL_SIZE, 4);
5581
+ ctx.strokeStyle = iconColor;
5582
+ ctx.fillStyle = iconColor;
5583
+ ctx.globalAlpha = hovered ? 1 : 0.75;
5584
+ ctx.lineWidth = 1.2;
5585
+ ctx.setLineDash([]);
5586
+ const cx = buttonX + INDICATOR_CONTROL_SIZE / 2;
5587
+ const cy = centerY;
5588
+ if (action === "visibility") {
5589
+ ctx.beginPath();
5590
+ ctx.ellipse(cx, cy, 4.6, 3, 0, 0, Math.PI * 2);
5591
+ ctx.stroke();
5592
+ ctx.beginPath();
5593
+ ctx.arc(cx, cy, 1.3, 0, Math.PI * 2);
5594
+ ctx.fill();
5595
+ if (!indicatorVisible) {
5596
+ ctx.beginPath();
5597
+ ctx.moveTo(cx - 5.2, cy + 4.6);
5598
+ ctx.lineTo(cx + 5.2, cy - 4.6);
5599
+ ctx.stroke();
5600
+ }
5601
+ } else if (action === "settings") {
5602
+ ctx.beginPath();
5603
+ ctx.arc(cx, cy, 3.4, 0, Math.PI * 2);
5604
+ ctx.stroke();
5605
+ for (let toothIndex = 0; toothIndex < 8; toothIndex += 1) {
5606
+ const angle = Math.PI / 4 * toothIndex;
5607
+ ctx.beginPath();
5608
+ ctx.moveTo(cx + Math.cos(angle) * 3.4, cy + Math.sin(angle) * 3.4);
5609
+ ctx.lineTo(cx + Math.cos(angle) * 5.2, cy + Math.sin(angle) * 5.2);
5610
+ ctx.stroke();
5611
+ }
5612
+ ctx.beginPath();
5613
+ ctx.arc(cx, cy, 1.1, 0, Math.PI * 2);
5614
+ ctx.fill();
5615
+ } else if (action === "source") {
5616
+ const prevSourceFont = ctx.font;
5617
+ ctx.font = `bold 9px ${mergedOptions.fontFamily}`;
5618
+ drawText("{}", cx, cy + 0.5, "center", "middle", iconColor);
5619
+ ctx.font = prevSourceFont;
5620
+ } else {
5621
+ ctx.beginPath();
5622
+ ctx.moveTo(cx - 3.4, cy - 3.4);
5623
+ ctx.lineTo(cx + 3.4, cy + 3.4);
5624
+ ctx.moveTo(cx + 3.4, cy - 3.4);
5625
+ ctx.lineTo(cx - 3.4, cy + 3.4);
5626
+ ctx.stroke();
5627
+ }
5628
+ ctx.restore();
5629
+ paneButtonRegions.push({
5630
+ id: indicatorId,
5631
+ type: indicatorType,
5632
+ action,
5633
+ x: buttonX,
5634
+ y: buttonY,
5635
+ width: INDICATOR_CONTROL_SIZE,
5636
+ height: INDICATOR_CONTROL_SIZE
5637
+ });
5638
+ buttonX += INDICATOR_CONTROL_SIZE + INDICATOR_CONTROL_GAP;
5639
+ }
5640
+ };
5560
5641
  const drawMarkBadge = (x, y, radius, color, shape, label, textColor) => {
5561
5642
  ctx.save();
5562
5643
  ctx.fillStyle = color;
@@ -5637,6 +5718,8 @@ function createChart(element, options = {}) {
5637
5718
  orderDragRegions = [];
5638
5719
  alertRegions = [];
5639
5720
  markRegions = [];
5721
+ paneButtonRegions = [];
5722
+ legendRowRegions = [];
5640
5723
  crosshairPriceActionRegion = null;
5641
5724
  const pixelRatio = getPixelRatio();
5642
5725
  canvas.style.width = `${width}px`;
@@ -7725,87 +7808,10 @@ function createChart(element, options = {}) {
7725
7808
  }
7726
7809
  paneLayoutFullChartHeight = fullChartHeight;
7727
7810
  paneLayoutInfos = [];
7728
- paneButtonRegions = [];
7729
7811
  if (activeSeparateIndicators.length > 0) {
7730
7812
  const xFromIndex = (index) => chartLeft + (index + 0.5 - xStart) / xSpan * chartWidth;
7731
7813
  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
- };
7814
+ const drawPaneButtons = drawIndicatorControls;
7809
7815
  activeSeparateIndicators.forEach(({ indicator, plugin }, paneIndex) => {
7810
7816
  const paneHeight = separatePaneHeights[paneIndex] ?? 80;
7811
7817
  const paneTop = paneTopCursor;
@@ -8307,26 +8313,56 @@ function createChart(element, options = {}) {
8307
8313
  };
8308
8314
  const labelEntries = activeOverlayIndicators.map(({ indicator, plugin }) => {
8309
8315
  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);
8316
+ const text = labels.showIndicatorNames && labels.showIndicatorValues && inputValues.length > 0 ? `${plugin.name} ${inputValues.join(" ")}` : labels.showIndicatorNames ? plugin.name : inputValues.join(" ");
8317
+ return { id: indicator.id, type: indicator.type, visible: indicator.visible, text };
8318
+ }).filter((entry) => entry.text.length > 0);
8318
8319
  if (labelEntries.length > 0) {
8319
8320
  const prevFont = ctx.font;
8320
- ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
8321
- const legendText = labelEntries.join(" ");
8321
+ const legendFontSize = Math.max(8, axis.fontSize);
8322
+ ctx.font = `${legendFontSize}px ${mergedOptions.fontFamily}`;
8322
8323
  const offsetX = Math.max(0, Number(labels.indicatorLegendOffsetX) || 0);
8323
8324
  const offsetY = Math.max(0, Number(labels.indicatorLegendOffsetY) || 0);
8324
8325
  const position = labels.indicatorLegendPosition;
8325
8326
  const isRight = position === "top-right" || position === "bottom-right";
8326
8327
  const isBottom = position === "bottom-left" || position === "bottom-right";
8327
8328
  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);
8329
+ const legendTop = isBottom ? chartBottom - offsetY : Math.max(chartTop + offsetY, dataLineBottom);
8330
+ const rowHeight = Math.round(legendFontSize * 1.55);
8331
+ labelEntries.forEach((entry, rowIndex) => {
8332
+ const rowTop = isBottom ? legendTop - (labelEntries.length - rowIndex) * rowHeight : legendTop + rowIndex * rowHeight;
8333
+ const rowCenter = rowTop + rowHeight / 2;
8334
+ const dimmed = entry.visible === false;
8335
+ ctx.save();
8336
+ if (dimmed) ctx.globalAlpha = 0.45;
8337
+ drawText(
8338
+ entry.text,
8339
+ legendX,
8340
+ rowCenter,
8341
+ isRight ? "right" : "left",
8342
+ "middle",
8343
+ labels.indicatorTextColor
8344
+ );
8345
+ ctx.restore();
8346
+ const textWidth = measureTextWidth(entry.text);
8347
+ const rowLeft = isRight ? legendX - textWidth : legendX;
8348
+ legendRowRegions.push({
8349
+ id: entry.id,
8350
+ type: entry.type,
8351
+ x: rowLeft,
8352
+ y: rowTop,
8353
+ width: textWidth + LEGEND_CONTROLS_WIDTH,
8354
+ height: rowHeight
8355
+ });
8356
+ if (hoveredLegendId === entry.id) {
8357
+ drawIndicatorControls(
8358
+ entry.id,
8359
+ entry.type,
8360
+ entry.visible !== false,
8361
+ isRight ? legendX + 6 : rowLeft + textWidth + 8,
8362
+ rowCenter
8363
+ );
8364
+ }
8365
+ });
8330
8366
  ctx.font = prevFont;
8331
8367
  }
8332
8368
  }
@@ -8998,6 +9034,9 @@ function createChart(element, options = {}) {
8998
9034
  if (hits.length === 0) return null;
8999
9035
  return hits.find((region) => region.part === "remove") ?? hits[0];
9000
9036
  };
9037
+ const getLegendRowAt = (x, y) => legendRowRegions.find(
9038
+ (region) => x >= region.x && x <= region.x + region.width && y >= region.y && y <= region.y + region.height
9039
+ ) ?? null;
9001
9040
  const getMarkAt = (x, y) => {
9002
9041
  const tolerance = touchInputActive ? Math.max(1, mergedOptions.touch?.hitToleranceScale ?? 2.2) : 1;
9003
9042
  for (let index = markRegions.length - 1; index >= 0; index -= 1) {
@@ -10747,6 +10786,12 @@ function createChart(element, options = {}) {
10747
10786
  setCrosshairPoint(null);
10748
10787
  return;
10749
10788
  }
10789
+ const legendRow = getLegendRowAt(point.x, point.y);
10790
+ const nextLegendId = legendRow?.id ?? null;
10791
+ if (nextLegendId !== hoveredLegendId) {
10792
+ hoveredLegendId = nextLegendId;
10793
+ scheduleDraw();
10794
+ }
10750
10795
  const hoveredPane = getPaneAt(point.x, point.y);
10751
10796
  const nextHoveredPaneId = hoveredPane?.id ?? null;
10752
10797
  if (nextHoveredPaneId !== hoveredPaneId) {
@@ -10965,10 +11010,11 @@ function createChart(element, options = {}) {
10965
11010
  });
10966
11011
  paneDividerDrag = null;
10967
11012
  }
10968
- if (event?.type === "pointerleave" && (hoveredPaneId !== null || hoveredPaneButton !== null || hoveredAlertId !== null || hoveredMarkKey !== null)) {
11013
+ if (event?.type === "pointerleave" && (hoveredPaneId !== null || hoveredPaneButton !== null || hoveredAlertId !== null || hoveredMarkKey !== null || hoveredLegendId !== null)) {
10969
11014
  hoveredPaneId = null;
10970
11015
  hoveredPaneButton = null;
10971
11016
  hoveredAlertId = null;
11017
+ hoveredLegendId = null;
10972
11018
  if (hoveredMarkKey !== null) {
10973
11019
  hoveredMarkKey = null;
10974
11020
  markHoverHandler?.(null);
package/dist/index.cjs CHANGED
@@ -4297,6 +4297,8 @@ function createChart(element, options = {}) {
4297
4297
  let paneButtonRegions = [];
4298
4298
  let hoveredPaneId = null;
4299
4299
  let hoveredPaneButton = null;
4300
+ let legendRowRegions = [];
4301
+ let hoveredLegendId = null;
4300
4302
  let paneDividerDrag = null;
4301
4303
  let indicatorPaneActionHandler = null;
4302
4304
  let indicatorPaneHeightChangeHandler = null;
@@ -5593,6 +5595,85 @@ function createChart(element, options = {}) {
5593
5595
  ctx.closePath();
5594
5596
  ctx.stroke();
5595
5597
  };
5598
+ const INDICATOR_CONTROL_SIZE = 16;
5599
+ const INDICATOR_CONTROL_GAP = 4;
5600
+ const LEGEND_CONTROLS_WIDTH = (INDICATOR_CONTROL_SIZE + INDICATOR_CONTROL_GAP) * 4 + 8;
5601
+ const drawIndicatorControls = (indicatorId, indicatorType, indicatorVisible, startX, centerY) => {
5602
+ const labels = resolvedLabels;
5603
+ const actionList = [
5604
+ "visibility",
5605
+ "settings",
5606
+ "source",
5607
+ "remove"
5608
+ ];
5609
+ let buttonX = startX;
5610
+ for (const action of actionList) {
5611
+ const buttonY = centerY - INDICATOR_CONTROL_SIZE / 2;
5612
+ const hovered = hoveredPaneButton?.id === indicatorId && hoveredPaneButton.action === action;
5613
+ const iconColor = labels.indicatorTextColor;
5614
+ ctx.save();
5615
+ ctx.fillStyle = hovered ? "rgba(148,163,184,0.30)" : "rgba(148,163,184,0.12)";
5616
+ fillRoundedRect(Math.round(buttonX), Math.round(buttonY), INDICATOR_CONTROL_SIZE, INDICATOR_CONTROL_SIZE, 4);
5617
+ ctx.strokeStyle = iconColor;
5618
+ ctx.fillStyle = iconColor;
5619
+ ctx.globalAlpha = hovered ? 1 : 0.75;
5620
+ ctx.lineWidth = 1.2;
5621
+ ctx.setLineDash([]);
5622
+ const cx = buttonX + INDICATOR_CONTROL_SIZE / 2;
5623
+ const cy = centerY;
5624
+ if (action === "visibility") {
5625
+ ctx.beginPath();
5626
+ ctx.ellipse(cx, cy, 4.6, 3, 0, 0, Math.PI * 2);
5627
+ ctx.stroke();
5628
+ ctx.beginPath();
5629
+ ctx.arc(cx, cy, 1.3, 0, Math.PI * 2);
5630
+ ctx.fill();
5631
+ if (!indicatorVisible) {
5632
+ ctx.beginPath();
5633
+ ctx.moveTo(cx - 5.2, cy + 4.6);
5634
+ ctx.lineTo(cx + 5.2, cy - 4.6);
5635
+ ctx.stroke();
5636
+ }
5637
+ } else if (action === "settings") {
5638
+ ctx.beginPath();
5639
+ ctx.arc(cx, cy, 3.4, 0, Math.PI * 2);
5640
+ ctx.stroke();
5641
+ for (let toothIndex = 0; toothIndex < 8; toothIndex += 1) {
5642
+ const angle = Math.PI / 4 * toothIndex;
5643
+ ctx.beginPath();
5644
+ ctx.moveTo(cx + Math.cos(angle) * 3.4, cy + Math.sin(angle) * 3.4);
5645
+ ctx.lineTo(cx + Math.cos(angle) * 5.2, cy + Math.sin(angle) * 5.2);
5646
+ ctx.stroke();
5647
+ }
5648
+ ctx.beginPath();
5649
+ ctx.arc(cx, cy, 1.1, 0, Math.PI * 2);
5650
+ ctx.fill();
5651
+ } else if (action === "source") {
5652
+ const prevSourceFont = ctx.font;
5653
+ ctx.font = `bold 9px ${mergedOptions.fontFamily}`;
5654
+ drawText("{}", cx, cy + 0.5, "center", "middle", iconColor);
5655
+ ctx.font = prevSourceFont;
5656
+ } else {
5657
+ ctx.beginPath();
5658
+ ctx.moveTo(cx - 3.4, cy - 3.4);
5659
+ ctx.lineTo(cx + 3.4, cy + 3.4);
5660
+ ctx.moveTo(cx + 3.4, cy - 3.4);
5661
+ ctx.lineTo(cx - 3.4, cy + 3.4);
5662
+ ctx.stroke();
5663
+ }
5664
+ ctx.restore();
5665
+ paneButtonRegions.push({
5666
+ id: indicatorId,
5667
+ type: indicatorType,
5668
+ action,
5669
+ x: buttonX,
5670
+ y: buttonY,
5671
+ width: INDICATOR_CONTROL_SIZE,
5672
+ height: INDICATOR_CONTROL_SIZE
5673
+ });
5674
+ buttonX += INDICATOR_CONTROL_SIZE + INDICATOR_CONTROL_GAP;
5675
+ }
5676
+ };
5596
5677
  const drawMarkBadge = (x, y, radius, color, shape, label, textColor) => {
5597
5678
  ctx.save();
5598
5679
  ctx.fillStyle = color;
@@ -5673,6 +5754,8 @@ function createChart(element, options = {}) {
5673
5754
  orderDragRegions = [];
5674
5755
  alertRegions = [];
5675
5756
  markRegions = [];
5757
+ paneButtonRegions = [];
5758
+ legendRowRegions = [];
5676
5759
  crosshairPriceActionRegion = null;
5677
5760
  const pixelRatio = getPixelRatio();
5678
5761
  canvas.style.width = `${width}px`;
@@ -7761,87 +7844,10 @@ function createChart(element, options = {}) {
7761
7844
  }
7762
7845
  paneLayoutFullChartHeight = fullChartHeight;
7763
7846
  paneLayoutInfos = [];
7764
- paneButtonRegions = [];
7765
7847
  if (activeSeparateIndicators.length > 0) {
7766
7848
  const xFromIndex = (index) => chartLeft + (index + 0.5 - xStart) / xSpan * chartWidth;
7767
7849
  let paneTopCursor = chartBottom + paneGap;
7768
- const paneButtonSize = 16;
7769
- const paneButtonGap = 4;
7770
- const drawPaneButtons = (indicatorId, indicatorType, indicatorVisible, startX, centerY) => {
7771
- const actionList = [
7772
- "visibility",
7773
- "settings",
7774
- "source",
7775
- "remove"
7776
- ];
7777
- let buttonX = startX;
7778
- for (const action of actionList) {
7779
- const buttonY = centerY - paneButtonSize / 2;
7780
- const hovered = hoveredPaneButton?.id === indicatorId && hoveredPaneButton.action === action;
7781
- const iconColor = labels.indicatorTextColor;
7782
- ctx.save();
7783
- ctx.fillStyle = hovered ? "rgba(148,163,184,0.30)" : "rgba(148,163,184,0.12)";
7784
- fillRoundedRect(Math.round(buttonX), Math.round(buttonY), paneButtonSize, paneButtonSize, 4);
7785
- ctx.strokeStyle = iconColor;
7786
- ctx.fillStyle = iconColor;
7787
- ctx.globalAlpha = hovered ? 1 : 0.75;
7788
- ctx.lineWidth = 1.2;
7789
- ctx.setLineDash([]);
7790
- const cx = buttonX + paneButtonSize / 2;
7791
- const cy = centerY;
7792
- if (action === "visibility") {
7793
- ctx.beginPath();
7794
- ctx.ellipse(cx, cy, 4.6, 3, 0, 0, Math.PI * 2);
7795
- ctx.stroke();
7796
- ctx.beginPath();
7797
- ctx.arc(cx, cy, 1.3, 0, Math.PI * 2);
7798
- ctx.fill();
7799
- if (!indicatorVisible) {
7800
- ctx.beginPath();
7801
- ctx.moveTo(cx - 5.2, cy + 4.6);
7802
- ctx.lineTo(cx + 5.2, cy - 4.6);
7803
- ctx.stroke();
7804
- }
7805
- } else if (action === "settings") {
7806
- ctx.beginPath();
7807
- ctx.arc(cx, cy, 3.4, 0, Math.PI * 2);
7808
- ctx.stroke();
7809
- for (let toothIndex = 0; toothIndex < 8; toothIndex += 1) {
7810
- const angle = Math.PI / 4 * toothIndex;
7811
- ctx.beginPath();
7812
- ctx.moveTo(cx + Math.cos(angle) * 3.4, cy + Math.sin(angle) * 3.4);
7813
- ctx.lineTo(cx + Math.cos(angle) * 5.2, cy + Math.sin(angle) * 5.2);
7814
- ctx.stroke();
7815
- }
7816
- ctx.beginPath();
7817
- ctx.arc(cx, cy, 1.1, 0, Math.PI * 2);
7818
- ctx.fill();
7819
- } else if (action === "source") {
7820
- const prevSourceFont = ctx.font;
7821
- ctx.font = `bold 9px ${mergedOptions.fontFamily}`;
7822
- drawText("{}", cx, cy + 0.5, "center", "middle", iconColor);
7823
- ctx.font = prevSourceFont;
7824
- } else {
7825
- ctx.beginPath();
7826
- ctx.moveTo(cx - 3.4, cy - 3.4);
7827
- ctx.lineTo(cx + 3.4, cy + 3.4);
7828
- ctx.moveTo(cx + 3.4, cy - 3.4);
7829
- ctx.lineTo(cx - 3.4, cy + 3.4);
7830
- ctx.stroke();
7831
- }
7832
- ctx.restore();
7833
- paneButtonRegions.push({
7834
- id: indicatorId,
7835
- type: indicatorType,
7836
- action,
7837
- x: buttonX,
7838
- y: buttonY,
7839
- width: paneButtonSize,
7840
- height: paneButtonSize
7841
- });
7842
- buttonX += paneButtonSize + paneButtonGap;
7843
- }
7844
- };
7850
+ const drawPaneButtons = drawIndicatorControls;
7845
7851
  activeSeparateIndicators.forEach(({ indicator, plugin }, paneIndex) => {
7846
7852
  const paneHeight = separatePaneHeights[paneIndex] ?? 80;
7847
7853
  const paneTop = paneTopCursor;
@@ -8343,26 +8349,56 @@ function createChart(element, options = {}) {
8343
8349
  };
8344
8350
  const labelEntries = activeOverlayIndicators.map(({ indicator, plugin }) => {
8345
8351
  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));
8346
- if (labels.showIndicatorNames && labels.showIndicatorValues && inputValues.length > 0) {
8347
- return `${plugin.name} ${inputValues.join(" ")}`;
8348
- }
8349
- if (labels.showIndicatorNames) {
8350
- return plugin.name;
8351
- }
8352
- return inputValues.join(" ");
8353
- }).filter((entry) => entry.length > 0);
8352
+ const text = labels.showIndicatorNames && labels.showIndicatorValues && inputValues.length > 0 ? `${plugin.name} ${inputValues.join(" ")}` : labels.showIndicatorNames ? plugin.name : inputValues.join(" ");
8353
+ return { id: indicator.id, type: indicator.type, visible: indicator.visible, text };
8354
+ }).filter((entry) => entry.text.length > 0);
8354
8355
  if (labelEntries.length > 0) {
8355
8356
  const prevFont = ctx.font;
8356
- ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
8357
- const legendText = labelEntries.join(" ");
8357
+ const legendFontSize = Math.max(8, axis.fontSize);
8358
+ ctx.font = `${legendFontSize}px ${mergedOptions.fontFamily}`;
8358
8359
  const offsetX = Math.max(0, Number(labels.indicatorLegendOffsetX) || 0);
8359
8360
  const offsetY = Math.max(0, Number(labels.indicatorLegendOffsetY) || 0);
8360
8361
  const position = labels.indicatorLegendPosition;
8361
8362
  const isRight = position === "top-right" || position === "bottom-right";
8362
8363
  const isBottom = position === "bottom-left" || position === "bottom-right";
8363
8364
  const legendX = isRight ? chartRight - offsetX : chartLeft + offsetX;
8364
- const legendY = isBottom ? chartBottom - offsetY : Math.max(chartTop + offsetY, dataLineBottom);
8365
- drawText(legendText, legendX, legendY, isRight ? "right" : "left", isBottom ? "bottom" : "top", labels.indicatorTextColor);
8365
+ const legendTop = isBottom ? chartBottom - offsetY : Math.max(chartTop + offsetY, dataLineBottom);
8366
+ const rowHeight = Math.round(legendFontSize * 1.55);
8367
+ labelEntries.forEach((entry, rowIndex) => {
8368
+ const rowTop = isBottom ? legendTop - (labelEntries.length - rowIndex) * rowHeight : legendTop + rowIndex * rowHeight;
8369
+ const rowCenter = rowTop + rowHeight / 2;
8370
+ const dimmed = entry.visible === false;
8371
+ ctx.save();
8372
+ if (dimmed) ctx.globalAlpha = 0.45;
8373
+ drawText(
8374
+ entry.text,
8375
+ legendX,
8376
+ rowCenter,
8377
+ isRight ? "right" : "left",
8378
+ "middle",
8379
+ labels.indicatorTextColor
8380
+ );
8381
+ ctx.restore();
8382
+ const textWidth = measureTextWidth(entry.text);
8383
+ const rowLeft = isRight ? legendX - textWidth : legendX;
8384
+ legendRowRegions.push({
8385
+ id: entry.id,
8386
+ type: entry.type,
8387
+ x: rowLeft,
8388
+ y: rowTop,
8389
+ width: textWidth + LEGEND_CONTROLS_WIDTH,
8390
+ height: rowHeight
8391
+ });
8392
+ if (hoveredLegendId === entry.id) {
8393
+ drawIndicatorControls(
8394
+ entry.id,
8395
+ entry.type,
8396
+ entry.visible !== false,
8397
+ isRight ? legendX + 6 : rowLeft + textWidth + 8,
8398
+ rowCenter
8399
+ );
8400
+ }
8401
+ });
8366
8402
  ctx.font = prevFont;
8367
8403
  }
8368
8404
  }
@@ -9034,6 +9070,9 @@ function createChart(element, options = {}) {
9034
9070
  if (hits.length === 0) return null;
9035
9071
  return hits.find((region) => region.part === "remove") ?? hits[0];
9036
9072
  };
9073
+ const getLegendRowAt = (x, y) => legendRowRegions.find(
9074
+ (region) => x >= region.x && x <= region.x + region.width && y >= region.y && y <= region.y + region.height
9075
+ ) ?? null;
9037
9076
  const getMarkAt = (x, y) => {
9038
9077
  const tolerance = touchInputActive ? Math.max(1, mergedOptions.touch?.hitToleranceScale ?? 2.2) : 1;
9039
9078
  for (let index = markRegions.length - 1; index >= 0; index -= 1) {
@@ -10783,6 +10822,12 @@ function createChart(element, options = {}) {
10783
10822
  setCrosshairPoint(null);
10784
10823
  return;
10785
10824
  }
10825
+ const legendRow = getLegendRowAt(point.x, point.y);
10826
+ const nextLegendId = legendRow?.id ?? null;
10827
+ if (nextLegendId !== hoveredLegendId) {
10828
+ hoveredLegendId = nextLegendId;
10829
+ scheduleDraw();
10830
+ }
10786
10831
  const hoveredPane = getPaneAt(point.x, point.y);
10787
10832
  const nextHoveredPaneId = hoveredPane?.id ?? null;
10788
10833
  if (nextHoveredPaneId !== hoveredPaneId) {
@@ -11001,10 +11046,11 @@ function createChart(element, options = {}) {
11001
11046
  });
11002
11047
  paneDividerDrag = null;
11003
11048
  }
11004
- if (event?.type === "pointerleave" && (hoveredPaneId !== null || hoveredPaneButton !== null || hoveredAlertId !== null || hoveredMarkKey !== null)) {
11049
+ if (event?.type === "pointerleave" && (hoveredPaneId !== null || hoveredPaneButton !== null || hoveredAlertId !== null || hoveredMarkKey !== null || hoveredLegendId !== null)) {
11005
11050
  hoveredPaneId = null;
11006
11051
  hoveredPaneButton = null;
11007
11052
  hoveredAlertId = null;
11053
+ hoveredLegendId = null;
11008
11054
  if (hoveredMarkKey !== null) {
11009
11055
  hoveredMarkKey = null;
11010
11056
  markHoverHandler?.(null);
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;
@@ -5557,6 +5559,85 @@ function createChart(element, options = {}) {
5557
5559
  ctx.closePath();
5558
5560
  ctx.stroke();
5559
5561
  };
5562
+ const INDICATOR_CONTROL_SIZE = 16;
5563
+ const INDICATOR_CONTROL_GAP = 4;
5564
+ const LEGEND_CONTROLS_WIDTH = (INDICATOR_CONTROL_SIZE + INDICATOR_CONTROL_GAP) * 4 + 8;
5565
+ const drawIndicatorControls = (indicatorId, indicatorType, indicatorVisible, startX, centerY) => {
5566
+ const labels = resolvedLabels;
5567
+ const actionList = [
5568
+ "visibility",
5569
+ "settings",
5570
+ "source",
5571
+ "remove"
5572
+ ];
5573
+ let buttonX = startX;
5574
+ for (const action of actionList) {
5575
+ const buttonY = centerY - INDICATOR_CONTROL_SIZE / 2;
5576
+ const hovered = hoveredPaneButton?.id === indicatorId && hoveredPaneButton.action === action;
5577
+ const iconColor = labels.indicatorTextColor;
5578
+ ctx.save();
5579
+ ctx.fillStyle = hovered ? "rgba(148,163,184,0.30)" : "rgba(148,163,184,0.12)";
5580
+ fillRoundedRect(Math.round(buttonX), Math.round(buttonY), INDICATOR_CONTROL_SIZE, INDICATOR_CONTROL_SIZE, 4);
5581
+ ctx.strokeStyle = iconColor;
5582
+ ctx.fillStyle = iconColor;
5583
+ ctx.globalAlpha = hovered ? 1 : 0.75;
5584
+ ctx.lineWidth = 1.2;
5585
+ ctx.setLineDash([]);
5586
+ const cx = buttonX + INDICATOR_CONTROL_SIZE / 2;
5587
+ const cy = centerY;
5588
+ if (action === "visibility") {
5589
+ ctx.beginPath();
5590
+ ctx.ellipse(cx, cy, 4.6, 3, 0, 0, Math.PI * 2);
5591
+ ctx.stroke();
5592
+ ctx.beginPath();
5593
+ ctx.arc(cx, cy, 1.3, 0, Math.PI * 2);
5594
+ ctx.fill();
5595
+ if (!indicatorVisible) {
5596
+ ctx.beginPath();
5597
+ ctx.moveTo(cx - 5.2, cy + 4.6);
5598
+ ctx.lineTo(cx + 5.2, cy - 4.6);
5599
+ ctx.stroke();
5600
+ }
5601
+ } else if (action === "settings") {
5602
+ ctx.beginPath();
5603
+ ctx.arc(cx, cy, 3.4, 0, Math.PI * 2);
5604
+ ctx.stroke();
5605
+ for (let toothIndex = 0; toothIndex < 8; toothIndex += 1) {
5606
+ const angle = Math.PI / 4 * toothIndex;
5607
+ ctx.beginPath();
5608
+ ctx.moveTo(cx + Math.cos(angle) * 3.4, cy + Math.sin(angle) * 3.4);
5609
+ ctx.lineTo(cx + Math.cos(angle) * 5.2, cy + Math.sin(angle) * 5.2);
5610
+ ctx.stroke();
5611
+ }
5612
+ ctx.beginPath();
5613
+ ctx.arc(cx, cy, 1.1, 0, Math.PI * 2);
5614
+ ctx.fill();
5615
+ } else if (action === "source") {
5616
+ const prevSourceFont = ctx.font;
5617
+ ctx.font = `bold 9px ${mergedOptions.fontFamily}`;
5618
+ drawText("{}", cx, cy + 0.5, "center", "middle", iconColor);
5619
+ ctx.font = prevSourceFont;
5620
+ } else {
5621
+ ctx.beginPath();
5622
+ ctx.moveTo(cx - 3.4, cy - 3.4);
5623
+ ctx.lineTo(cx + 3.4, cy + 3.4);
5624
+ ctx.moveTo(cx + 3.4, cy - 3.4);
5625
+ ctx.lineTo(cx - 3.4, cy + 3.4);
5626
+ ctx.stroke();
5627
+ }
5628
+ ctx.restore();
5629
+ paneButtonRegions.push({
5630
+ id: indicatorId,
5631
+ type: indicatorType,
5632
+ action,
5633
+ x: buttonX,
5634
+ y: buttonY,
5635
+ width: INDICATOR_CONTROL_SIZE,
5636
+ height: INDICATOR_CONTROL_SIZE
5637
+ });
5638
+ buttonX += INDICATOR_CONTROL_SIZE + INDICATOR_CONTROL_GAP;
5639
+ }
5640
+ };
5560
5641
  const drawMarkBadge = (x, y, radius, color, shape, label, textColor) => {
5561
5642
  ctx.save();
5562
5643
  ctx.fillStyle = color;
@@ -5637,6 +5718,8 @@ function createChart(element, options = {}) {
5637
5718
  orderDragRegions = [];
5638
5719
  alertRegions = [];
5639
5720
  markRegions = [];
5721
+ paneButtonRegions = [];
5722
+ legendRowRegions = [];
5640
5723
  crosshairPriceActionRegion = null;
5641
5724
  const pixelRatio = getPixelRatio();
5642
5725
  canvas.style.width = `${width}px`;
@@ -7725,87 +7808,10 @@ function createChart(element, options = {}) {
7725
7808
  }
7726
7809
  paneLayoutFullChartHeight = fullChartHeight;
7727
7810
  paneLayoutInfos = [];
7728
- paneButtonRegions = [];
7729
7811
  if (activeSeparateIndicators.length > 0) {
7730
7812
  const xFromIndex = (index) => chartLeft + (index + 0.5 - xStart) / xSpan * chartWidth;
7731
7813
  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
- };
7814
+ const drawPaneButtons = drawIndicatorControls;
7809
7815
  activeSeparateIndicators.forEach(({ indicator, plugin }, paneIndex) => {
7810
7816
  const paneHeight = separatePaneHeights[paneIndex] ?? 80;
7811
7817
  const paneTop = paneTopCursor;
@@ -8307,26 +8313,56 @@ function createChart(element, options = {}) {
8307
8313
  };
8308
8314
  const labelEntries = activeOverlayIndicators.map(({ indicator, plugin }) => {
8309
8315
  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);
8316
+ const text = labels.showIndicatorNames && labels.showIndicatorValues && inputValues.length > 0 ? `${plugin.name} ${inputValues.join(" ")}` : labels.showIndicatorNames ? plugin.name : inputValues.join(" ");
8317
+ return { id: indicator.id, type: indicator.type, visible: indicator.visible, text };
8318
+ }).filter((entry) => entry.text.length > 0);
8318
8319
  if (labelEntries.length > 0) {
8319
8320
  const prevFont = ctx.font;
8320
- ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
8321
- const legendText = labelEntries.join(" ");
8321
+ const legendFontSize = Math.max(8, axis.fontSize);
8322
+ ctx.font = `${legendFontSize}px ${mergedOptions.fontFamily}`;
8322
8323
  const offsetX = Math.max(0, Number(labels.indicatorLegendOffsetX) || 0);
8323
8324
  const offsetY = Math.max(0, Number(labels.indicatorLegendOffsetY) || 0);
8324
8325
  const position = labels.indicatorLegendPosition;
8325
8326
  const isRight = position === "top-right" || position === "bottom-right";
8326
8327
  const isBottom = position === "bottom-left" || position === "bottom-right";
8327
8328
  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);
8329
+ const legendTop = isBottom ? chartBottom - offsetY : Math.max(chartTop + offsetY, dataLineBottom);
8330
+ const rowHeight = Math.round(legendFontSize * 1.55);
8331
+ labelEntries.forEach((entry, rowIndex) => {
8332
+ const rowTop = isBottom ? legendTop - (labelEntries.length - rowIndex) * rowHeight : legendTop + rowIndex * rowHeight;
8333
+ const rowCenter = rowTop + rowHeight / 2;
8334
+ const dimmed = entry.visible === false;
8335
+ ctx.save();
8336
+ if (dimmed) ctx.globalAlpha = 0.45;
8337
+ drawText(
8338
+ entry.text,
8339
+ legendX,
8340
+ rowCenter,
8341
+ isRight ? "right" : "left",
8342
+ "middle",
8343
+ labels.indicatorTextColor
8344
+ );
8345
+ ctx.restore();
8346
+ const textWidth = measureTextWidth(entry.text);
8347
+ const rowLeft = isRight ? legendX - textWidth : legendX;
8348
+ legendRowRegions.push({
8349
+ id: entry.id,
8350
+ type: entry.type,
8351
+ x: rowLeft,
8352
+ y: rowTop,
8353
+ width: textWidth + LEGEND_CONTROLS_WIDTH,
8354
+ height: rowHeight
8355
+ });
8356
+ if (hoveredLegendId === entry.id) {
8357
+ drawIndicatorControls(
8358
+ entry.id,
8359
+ entry.type,
8360
+ entry.visible !== false,
8361
+ isRight ? legendX + 6 : rowLeft + textWidth + 8,
8362
+ rowCenter
8363
+ );
8364
+ }
8365
+ });
8330
8366
  ctx.font = prevFont;
8331
8367
  }
8332
8368
  }
@@ -8998,6 +9034,9 @@ function createChart(element, options = {}) {
8998
9034
  if (hits.length === 0) return null;
8999
9035
  return hits.find((region) => region.part === "remove") ?? hits[0];
9000
9036
  };
9037
+ const getLegendRowAt = (x, y) => legendRowRegions.find(
9038
+ (region) => x >= region.x && x <= region.x + region.width && y >= region.y && y <= region.y + region.height
9039
+ ) ?? null;
9001
9040
  const getMarkAt = (x, y) => {
9002
9041
  const tolerance = touchInputActive ? Math.max(1, mergedOptions.touch?.hitToleranceScale ?? 2.2) : 1;
9003
9042
  for (let index = markRegions.length - 1; index >= 0; index -= 1) {
@@ -10747,6 +10786,12 @@ function createChart(element, options = {}) {
10747
10786
  setCrosshairPoint(null);
10748
10787
  return;
10749
10788
  }
10789
+ const legendRow = getLegendRowAt(point.x, point.y);
10790
+ const nextLegendId = legendRow?.id ?? null;
10791
+ if (nextLegendId !== hoveredLegendId) {
10792
+ hoveredLegendId = nextLegendId;
10793
+ scheduleDraw();
10794
+ }
10750
10795
  const hoveredPane = getPaneAt(point.x, point.y);
10751
10796
  const nextHoveredPaneId = hoveredPane?.id ?? null;
10752
10797
  if (nextHoveredPaneId !== hoveredPaneId) {
@@ -10965,10 +11010,11 @@ function createChart(element, options = {}) {
10965
11010
  });
10966
11011
  paneDividerDrag = null;
10967
11012
  }
10968
- if (event?.type === "pointerleave" && (hoveredPaneId !== null || hoveredPaneButton !== null || hoveredAlertId !== null || hoveredMarkKey !== null)) {
11013
+ if (event?.type === "pointerleave" && (hoveredPaneId !== null || hoveredPaneButton !== null || hoveredAlertId !== null || hoveredMarkKey !== null || hoveredLegendId !== null)) {
10969
11014
  hoveredPaneId = null;
10970
11015
  hoveredPaneButton = null;
10971
11016
  hoveredAlertId = null;
11017
+ hoveredLegendId = null;
10972
11018
  if (hoveredMarkKey !== null) {
10973
11019
  hoveredMarkKey = null;
10974
11020
  markHoverHandler?.(null);
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.
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.148",
4
4
  "description": "Lightweight TypeScript charting core",
5
5
  "type": "module",
6
6
  "main": "./dist/hyperprop-charting-library.cjs",