hyperprop-charting-library 0.1.146 → 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.
@@ -2072,8 +2072,8 @@ var BUILTIN_DONCHIAN_INDICATOR = {
2072
2072
  }
2073
2073
  };
2074
2074
  var computeVolumeProfile = (data, fromIndex, toIndex, rowCount, valueAreaPercent) => {
2075
- const from = Math.max(0, fromIndex);
2076
- const to = Math.min(data.length - 1, toIndex);
2075
+ const from = Math.max(0, Math.round(fromIndex));
2076
+ const to = Math.min(data.length - 1, Math.round(toIndex));
2077
2077
  if (to < from) return null;
2078
2078
  let low = Number.POSITIVE_INFINITY;
2079
2079
  let high = Number.NEGATIVE_INFINITY;
@@ -3932,6 +3932,9 @@ function createChart(element, options = {}) {
3932
3932
  pointValue: Number(drawing.pointValue) || 1,
3933
3933
  qtyPrecision: Number.isFinite(drawing.qtyPrecision) ? Math.max(0, Math.floor(Number(drawing.qtyPrecision))) : 0,
3934
3934
  fontSize: Math.max(6, Number(drawing.fontSize) || 14),
3935
+ profileRows: Math.max(4, Math.min(200, Math.round(Number(drawing.profileRows)) || 24)),
3936
+ profileValueAreaPercent: Math.max(10, Math.min(99, Number(drawing.profileValueAreaPercent) || 70)),
3937
+ profileWidthRatio: Math.max(0.1, Math.min(1, Number(drawing.profileWidthRatio) || 0.5)),
3935
3938
  ...drawing.label === void 0 ? {} : { label: drawing.label }
3936
3939
  };
3937
3940
  };
@@ -3965,6 +3968,9 @@ function createChart(element, options = {}) {
3965
3968
  pointValue: drawing.pointValue,
3966
3969
  qtyPrecision: drawing.qtyPrecision,
3967
3970
  fontSize: drawing.fontSize,
3971
+ profileRows: drawing.profileRows,
3972
+ profileValueAreaPercent: drawing.profileValueAreaPercent,
3973
+ profileWidthRatio: drawing.profileWidthRatio,
3968
3974
  ...drawing.label === void 0 ? {} : { label: drawing.label }
3969
3975
  });
3970
3976
  let tradeMarkers = [];
@@ -4291,6 +4297,8 @@ function createChart(element, options = {}) {
4291
4297
  let paneButtonRegions = [];
4292
4298
  let hoveredPaneId = null;
4293
4299
  let hoveredPaneButton = null;
4300
+ let legendRowRegions = [];
4301
+ let hoveredLegendId = null;
4294
4302
  let paneDividerDrag = null;
4295
4303
  let indicatorPaneActionHandler = null;
4296
4304
  let indicatorPaneHeightChangeHandler = null;
@@ -5587,6 +5595,85 @@ function createChart(element, options = {}) {
5587
5595
  ctx.closePath();
5588
5596
  ctx.stroke();
5589
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
+ };
5590
5677
  const drawMarkBadge = (x, y, radius, color, shape, label, textColor) => {
5591
5678
  ctx.save();
5592
5679
  ctx.fillStyle = color;
@@ -5667,6 +5754,8 @@ function createChart(element, options = {}) {
5667
5754
  orderDragRegions = [];
5668
5755
  alertRegions = [];
5669
5756
  markRegions = [];
5757
+ paneButtonRegions = [];
5758
+ legendRowRegions = [];
5670
5759
  crosshairPriceActionRegion = null;
5671
5760
  const pixelRatio = getPixelRatio();
5672
5761
  canvas.style.width = `${width}px`;
@@ -6671,6 +6760,81 @@ function createChart(element, options = {}) {
6671
6760
  handleAt(ax, ay, drawing.color);
6672
6761
  handleAt(bx, by, drawing.color);
6673
6762
  }
6763
+ } else if (drawing.type === "fixed-range-volume-profile") {
6764
+ const p0 = drawing.points[0];
6765
+ const p1 = drawing.points[1];
6766
+ if (p0 && p1 && data.length > 0) {
6767
+ const fromIndex = Math.max(0, Math.round(Math.min(p0.index, p1.index)));
6768
+ const toIndex = Math.min(data.length - 1, Math.round(Math.max(p0.index, p1.index)));
6769
+ const leftX = xFromDrawingPoint(p0.index <= p1.index ? p0 : p1) - candleSpacing / 2;
6770
+ const rightX = xFromDrawingPoint(p0.index <= p1.index ? p1 : p0) + candleSpacing / 2;
6771
+ const rangeWidth = Math.max(1, rightX - leftX);
6772
+ const rows = Math.max(4, Math.min(200, Math.round(drawing.profileRows) || 24));
6773
+ const valueAreaPercent = Math.max(10, Math.min(99, Number(drawing.profileValueAreaPercent) || 70));
6774
+ const profile = toIndex >= fromIndex ? computeVolumeProfile(data, fromIndex, toIndex, rows, valueAreaPercent) : null;
6775
+ ctx.save();
6776
+ ctx.globalAlpha = draft ? 0.5 : 1;
6777
+ ctx.fillStyle = hexToRgba(drawing.color, 0.06);
6778
+ ctx.fillRect(leftX, chartTop, rangeWidth, chartBottom - chartTop);
6779
+ ctx.strokeStyle = hexToRgba(drawing.color, 0.5);
6780
+ ctx.lineWidth = 1;
6781
+ ctx.setLineDash([4, 3]);
6782
+ ctx.beginPath();
6783
+ ctx.moveTo(crisp(leftX), chartTop);
6784
+ ctx.lineTo(crisp(leftX), chartBottom);
6785
+ ctx.moveTo(crisp(rightX), chartTop);
6786
+ ctx.lineTo(crisp(rightX), chartBottom);
6787
+ ctx.stroke();
6788
+ ctx.setLineDash([]);
6789
+ if (profile && profile.maxRowVolume > 0) {
6790
+ const widthRatio = Math.max(0.1, Math.min(1, Number(drawing.profileWidthRatio) || 0.5));
6791
+ const maxWidth = rangeWidth * widthRatio;
6792
+ for (let row = 0; row < profile.rows.length; row += 1) {
6793
+ const bucket = profile.rows[row];
6794
+ if (bucket.total <= 0) continue;
6795
+ const yTop = yFromPrice(bucket.high);
6796
+ const yBottom = yFromPrice(bucket.low);
6797
+ const top = Math.min(yTop, yBottom);
6798
+ const height2 = Math.max(1, Math.abs(yBottom - yTop) - 1);
6799
+ if (top > chartBottom || top + height2 < chartTop) continue;
6800
+ const inValueArea = row >= profile.valueAreaFrom && row <= profile.valueAreaTo;
6801
+ ctx.globalAlpha = (draft ? 0.5 : 1) * (inValueArea ? 0.5 : 0.2);
6802
+ const rowWidth = bucket.total / profile.maxRowVolume * maxWidth;
6803
+ const upWidth = rowWidth * (bucket.up / bucket.total);
6804
+ ctx.fillStyle = mergedOptions.upColor;
6805
+ ctx.fillRect(leftX, top, upWidth, height2);
6806
+ ctx.fillStyle = mergedOptions.downColor;
6807
+ ctx.fillRect(leftX + upWidth, top, rowWidth - upWidth, height2);
6808
+ }
6809
+ ctx.globalAlpha = draft ? 0.6 : 1;
6810
+ const poc = profile.rows[profile.pocIndex];
6811
+ const drawProfileLevel = (price, color, label, dashed) => {
6812
+ const y = yFromPrice(price);
6813
+ if (y < chartTop || y > chartBottom) return;
6814
+ ctx.strokeStyle = color;
6815
+ ctx.lineWidth = 1;
6816
+ ctx.setLineDash(dashed ? [4, 3] : []);
6817
+ ctx.beginPath();
6818
+ ctx.moveTo(leftX, crisp(y));
6819
+ ctx.lineTo(leftX + maxWidth, crisp(y));
6820
+ ctx.stroke();
6821
+ ctx.setLineDash([]);
6822
+ const prevFont = ctx.font;
6823
+ ctx.font = `10px ${mergedOptions.fontFamily}`;
6824
+ ctx.fillStyle = color;
6825
+ ctx.textAlign = "left";
6826
+ ctx.textBaseline = "bottom";
6827
+ ctx.fillText(label, leftX + 3, y - 1);
6828
+ ctx.font = prevFont;
6829
+ };
6830
+ drawProfileLevel(profile.rows[profile.valueAreaTo].high, "#7e8aa3", "VAH", true);
6831
+ drawProfileLevel(profile.rows[profile.valueAreaFrom].low, "#7e8aa3", "VAL", true);
6832
+ drawProfileLevel((poc.low + poc.high) / 2, "#f7a600", "POC", false);
6833
+ }
6834
+ ctx.restore();
6835
+ handleAt(leftX, yFromPrice(p0.index <= p1.index ? p0.price : p1.price), drawing.color);
6836
+ handleAt(rightX, yFromPrice(p0.index <= p1.index ? p1.price : p0.price), drawing.color);
6837
+ }
6674
6838
  } else if (drawing.type === "price-range") {
6675
6839
  const p0 = drawing.points[0];
6676
6840
  const p1 = drawing.points[1];
@@ -7680,87 +7844,10 @@ function createChart(element, options = {}) {
7680
7844
  }
7681
7845
  paneLayoutFullChartHeight = fullChartHeight;
7682
7846
  paneLayoutInfos = [];
7683
- paneButtonRegions = [];
7684
7847
  if (activeSeparateIndicators.length > 0) {
7685
7848
  const xFromIndex = (index) => chartLeft + (index + 0.5 - xStart) / xSpan * chartWidth;
7686
7849
  let paneTopCursor = chartBottom + paneGap;
7687
- const paneButtonSize = 16;
7688
- const paneButtonGap = 4;
7689
- const drawPaneButtons = (indicatorId, indicatorType, indicatorVisible, startX, centerY) => {
7690
- const actionList = [
7691
- "visibility",
7692
- "settings",
7693
- "source",
7694
- "remove"
7695
- ];
7696
- let buttonX = startX;
7697
- for (const action of actionList) {
7698
- const buttonY = centerY - paneButtonSize / 2;
7699
- const hovered = hoveredPaneButton?.id === indicatorId && hoveredPaneButton.action === action;
7700
- const iconColor = labels.indicatorTextColor;
7701
- ctx.save();
7702
- ctx.fillStyle = hovered ? "rgba(148,163,184,0.30)" : "rgba(148,163,184,0.12)";
7703
- fillRoundedRect(Math.round(buttonX), Math.round(buttonY), paneButtonSize, paneButtonSize, 4);
7704
- ctx.strokeStyle = iconColor;
7705
- ctx.fillStyle = iconColor;
7706
- ctx.globalAlpha = hovered ? 1 : 0.75;
7707
- ctx.lineWidth = 1.2;
7708
- ctx.setLineDash([]);
7709
- const cx = buttonX + paneButtonSize / 2;
7710
- const cy = centerY;
7711
- if (action === "visibility") {
7712
- ctx.beginPath();
7713
- ctx.ellipse(cx, cy, 4.6, 3, 0, 0, Math.PI * 2);
7714
- ctx.stroke();
7715
- ctx.beginPath();
7716
- ctx.arc(cx, cy, 1.3, 0, Math.PI * 2);
7717
- ctx.fill();
7718
- if (!indicatorVisible) {
7719
- ctx.beginPath();
7720
- ctx.moveTo(cx - 5.2, cy + 4.6);
7721
- ctx.lineTo(cx + 5.2, cy - 4.6);
7722
- ctx.stroke();
7723
- }
7724
- } else if (action === "settings") {
7725
- ctx.beginPath();
7726
- ctx.arc(cx, cy, 3.4, 0, Math.PI * 2);
7727
- ctx.stroke();
7728
- for (let toothIndex = 0; toothIndex < 8; toothIndex += 1) {
7729
- const angle = Math.PI / 4 * toothIndex;
7730
- ctx.beginPath();
7731
- ctx.moveTo(cx + Math.cos(angle) * 3.4, cy + Math.sin(angle) * 3.4);
7732
- ctx.lineTo(cx + Math.cos(angle) * 5.2, cy + Math.sin(angle) * 5.2);
7733
- ctx.stroke();
7734
- }
7735
- ctx.beginPath();
7736
- ctx.arc(cx, cy, 1.1, 0, Math.PI * 2);
7737
- ctx.fill();
7738
- } else if (action === "source") {
7739
- const prevSourceFont = ctx.font;
7740
- ctx.font = `bold 9px ${mergedOptions.fontFamily}`;
7741
- drawText("{}", cx, cy + 0.5, "center", "middle", iconColor);
7742
- ctx.font = prevSourceFont;
7743
- } else {
7744
- ctx.beginPath();
7745
- ctx.moveTo(cx - 3.4, cy - 3.4);
7746
- ctx.lineTo(cx + 3.4, cy + 3.4);
7747
- ctx.moveTo(cx + 3.4, cy - 3.4);
7748
- ctx.lineTo(cx - 3.4, cy + 3.4);
7749
- ctx.stroke();
7750
- }
7751
- ctx.restore();
7752
- paneButtonRegions.push({
7753
- id: indicatorId,
7754
- type: indicatorType,
7755
- action,
7756
- x: buttonX,
7757
- y: buttonY,
7758
- width: paneButtonSize,
7759
- height: paneButtonSize
7760
- });
7761
- buttonX += paneButtonSize + paneButtonGap;
7762
- }
7763
- };
7850
+ const drawPaneButtons = drawIndicatorControls;
7764
7851
  activeSeparateIndicators.forEach(({ indicator, plugin }, paneIndex) => {
7765
7852
  const paneHeight = separatePaneHeights[paneIndex] ?? 80;
7766
7853
  const paneTop = paneTopCursor;
@@ -8262,26 +8349,56 @@ function createChart(element, options = {}) {
8262
8349
  };
8263
8350
  const labelEntries = activeOverlayIndicators.map(({ indicator, plugin }) => {
8264
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));
8265
- if (labels.showIndicatorNames && labels.showIndicatorValues && inputValues.length > 0) {
8266
- return `${plugin.name} ${inputValues.join(" ")}`;
8267
- }
8268
- if (labels.showIndicatorNames) {
8269
- return plugin.name;
8270
- }
8271
- return inputValues.join(" ");
8272
- }).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);
8273
8355
  if (labelEntries.length > 0) {
8274
8356
  const prevFont = ctx.font;
8275
- ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
8276
- const legendText = labelEntries.join(" ");
8357
+ const legendFontSize = Math.max(8, axis.fontSize);
8358
+ ctx.font = `${legendFontSize}px ${mergedOptions.fontFamily}`;
8277
8359
  const offsetX = Math.max(0, Number(labels.indicatorLegendOffsetX) || 0);
8278
8360
  const offsetY = Math.max(0, Number(labels.indicatorLegendOffsetY) || 0);
8279
8361
  const position = labels.indicatorLegendPosition;
8280
8362
  const isRight = position === "top-right" || position === "bottom-right";
8281
8363
  const isBottom = position === "bottom-left" || position === "bottom-right";
8282
8364
  const legendX = isRight ? chartRight - offsetX : chartLeft + offsetX;
8283
- const legendY = isBottom ? chartBottom - offsetY : Math.max(chartTop + offsetY, dataLineBottom);
8284
- 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
+ });
8285
8402
  ctx.font = prevFont;
8286
8403
  }
8287
8404
  }
@@ -8953,6 +9070,9 @@ function createChart(element, options = {}) {
8953
9070
  if (hits.length === 0) return null;
8954
9071
  return hits.find((region) => region.part === "remove") ?? hits[0];
8955
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;
8956
9076
  const getMarkAt = (x, y) => {
8957
9077
  const tolerance = touchInputActive ? Math.max(1, mergedOptions.touch?.hitToleranceScale ?? 2.2) : 1;
8958
9078
  for (let index = markRegions.length - 1; index >= 0; index -= 1) {
@@ -9443,6 +9563,19 @@ function createChart(element, options = {}) {
9443
9563
  if (distanceToSegment(x, y, bx + blockW / 2, by + blockH / 2, ax, ay) <= 5 * hitTolerance) {
9444
9564
  return { drawing, target: "line" };
9445
9565
  }
9566
+ } else if (drawing.type === "fixed-range-volume-profile") {
9567
+ const p0 = drawing.points[0];
9568
+ const p1 = drawing.points[1];
9569
+ if (!p0 || !p1) continue;
9570
+ const px0 = canvasXFromDrawingPoint(p0);
9571
+ const px1 = canvasXFromDrawingPoint(p1);
9572
+ const y0 = canvasYFromDrawingPrice(p0.price);
9573
+ const y1 = canvasYFromDrawingPrice(p1.price);
9574
+ if (Math.hypot(x - px0, y - y0) <= 9 * hitTolerance) return { drawing, target: "handle", pointIndex: 0 };
9575
+ if (Math.hypot(x - px1, y - y1) <= 9 * hitTolerance) return { drawing, target: "handle", pointIndex: 1 };
9576
+ if (x >= Math.min(px0, px1) && x <= Math.max(px0, px1)) {
9577
+ return { drawing, target: "line" };
9578
+ }
9446
9579
  } else if (drawing.type === "price-range") {
9447
9580
  const p0 = drawing.points[0];
9448
9581
  const p1 = drawing.points[1];
@@ -9894,6 +10027,33 @@ function createChart(element, options = {}) {
9894
10027
  scheduleDraw();
9895
10028
  return true;
9896
10029
  }
10030
+ if (activeDrawingTool === "fixed-range-volume-profile") {
10031
+ if (draftDrawing?.type === "fixed-range-volume-profile") {
10032
+ const completed = normalizeDrawingState({
10033
+ ...serializeDrawing(draftDrawing),
10034
+ points: [draftDrawing.points[0], point]
10035
+ });
10036
+ drawings.push(completed);
10037
+ draftDrawing = null;
10038
+ activeDrawingTool = null;
10039
+ emitDrawingsChange();
10040
+ scheduleDraw();
10041
+ return true;
10042
+ }
10043
+ const defaults = getDrawingToolDefaults("fixed-range-volume-profile");
10044
+ draftDrawing = normalizeDrawingState({
10045
+ type: "fixed-range-volume-profile",
10046
+ points: [point, point],
10047
+ color: defaults.color ?? "#5b8def",
10048
+ style: defaults.style ?? "solid",
10049
+ width: defaults.width ?? 1,
10050
+ ...defaults.profileRows === void 0 ? {} : { profileRows: defaults.profileRows },
10051
+ ...defaults.profileValueAreaPercent === void 0 ? {} : { profileValueAreaPercent: defaults.profileValueAreaPercent },
10052
+ ...defaults.profileWidthRatio === void 0 ? {} : { profileWidthRatio: defaults.profileWidthRatio }
10053
+ });
10054
+ scheduleDraw();
10055
+ return true;
10056
+ }
9897
10057
  if (activeDrawingTool === "price-range") {
9898
10058
  const tick = getConfiguredTickSize();
9899
10059
  const visibleRange = drawState.yMax - drawState.yMin;
@@ -10662,6 +10822,12 @@ function createChart(element, options = {}) {
10662
10822
  setCrosshairPoint(null);
10663
10823
  return;
10664
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
+ }
10665
10831
  const hoveredPane = getPaneAt(point.x, point.y);
10666
10832
  const nextHoveredPaneId = hoveredPane?.id ?? null;
10667
10833
  if (nextHoveredPaneId !== hoveredPaneId) {
@@ -10880,10 +11046,11 @@ function createChart(element, options = {}) {
10880
11046
  });
10881
11047
  paneDividerDrag = null;
10882
11048
  }
10883
- 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)) {
10884
11050
  hoveredPaneId = null;
10885
11051
  hoveredPaneButton = null;
10886
11052
  hoveredAlertId = null;
11053
+ hoveredLegendId = null;
10887
11054
  if (hoveredMarkKey !== null) {
10888
11055
  hoveredMarkKey = null;
10889
11056
  markHoverHandler?.(null);
@@ -120,6 +120,12 @@ type DrawingToolType = "horizontal-line" | "vertical-line" | "trendline" | "ray"
120
120
  | "brush"
121
121
  /** Text box with a connector line pointing at an anchored bar/price. */
122
122
  | "callout"
123
+ /**
124
+ * Volume profile over a bar range picked by two clicks (TradingView's
125
+ * fixed-range volume profile). Only the two anchors' bar indexes matter —
126
+ * the price extent comes from the bars they enclose.
127
+ */
128
+ | "fixed-range-volume-profile"
123
129
  /**
124
130
  * TradingView-style ruler. Unlike every other tool it is transient: the
125
131
  * measurement overlay is never added to the drawings list (no
@@ -157,6 +163,12 @@ interface DrawingObjectOptions {
157
163
  pointValue?: number;
158
164
  qtyPrecision?: number;
159
165
  fontSize?: number;
166
+ /** Price buckets across the range. Default 24. */
167
+ profileRows?: number;
168
+ /** Share of volume enclosed by the value area. Default 70. */
169
+ profileValueAreaPercent?: number;
170
+ /** Profile width as a share of the selected range. Default 0.5. */
171
+ profileWidthRatio?: number;
160
172
  }
161
173
  /** Default colors for position tools: [profit, loss, label text]. */
162
174
  declare const POSITION_DEFAULT_COLORS: string[];
@@ -220,7 +232,7 @@ interface DrawingHoverEvent {
220
232
  x: number;
221
233
  y: number;
222
234
  }
223
- type DrawingDefaults = Partial<Pick<DrawingObjectOptions, "color" | "colors" | "style" | "width" | "accountSize" | "lotSize" | "risk" | "riskMode" | "leverage" | "pointValue" | "qtyPrecision" | "fontSize">>;
235
+ type DrawingDefaults = Partial<Pick<DrawingObjectOptions, "color" | "colors" | "style" | "width" | "accountSize" | "lotSize" | "risk" | "riskMode" | "leverage" | "pointValue" | "qtyPrecision" | "fontSize" | "profileRows" | "profileValueAreaPercent" | "profileWidthRatio">>;
224
236
  interface IndicatorInstanceOptions<TInputs extends Record<string, unknown> = Record<string, unknown>> {
225
237
  id?: string;
226
238
  type: string;