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.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;
@@ -4304,6 +4306,7 @@ function createChart(element, options = {}) {
4304
4306
  let actionDragState = null;
4305
4307
  let pointerDownInfo = null;
4306
4308
  let crosshairPoint = null;
4309
+ let externalCrosshair = null;
4307
4310
  let doubleClickEnabled = mergedOptions.doubleClickEnabled;
4308
4311
  let doubleClickAction = mergedOptions.doubleClickAction;
4309
4312
  setIndicatorLiveThrottleMs(mergedOptions.indicatorUpdate?.liveThrottleMs ?? 80);
@@ -5593,6 +5596,85 @@ function createChart(element, options = {}) {
5593
5596
  ctx.closePath();
5594
5597
  ctx.stroke();
5595
5598
  };
5599
+ const INDICATOR_CONTROL_SIZE = 16;
5600
+ const INDICATOR_CONTROL_GAP = 4;
5601
+ const LEGEND_CONTROLS_WIDTH = (INDICATOR_CONTROL_SIZE + INDICATOR_CONTROL_GAP) * 4 + 8;
5602
+ const drawIndicatorControls = (indicatorId, indicatorType, indicatorVisible, startX, centerY) => {
5603
+ const labels = resolvedLabels;
5604
+ const actionList = [
5605
+ "visibility",
5606
+ "settings",
5607
+ "source",
5608
+ "remove"
5609
+ ];
5610
+ let buttonX = startX;
5611
+ for (const action of actionList) {
5612
+ const buttonY = centerY - INDICATOR_CONTROL_SIZE / 2;
5613
+ const hovered = hoveredPaneButton?.id === indicatorId && hoveredPaneButton.action === action;
5614
+ const iconColor = labels.indicatorTextColor;
5615
+ ctx.save();
5616
+ ctx.fillStyle = hovered ? "rgba(148,163,184,0.30)" : "rgba(148,163,184,0.12)";
5617
+ fillRoundedRect(Math.round(buttonX), Math.round(buttonY), INDICATOR_CONTROL_SIZE, INDICATOR_CONTROL_SIZE, 4);
5618
+ ctx.strokeStyle = iconColor;
5619
+ ctx.fillStyle = iconColor;
5620
+ ctx.globalAlpha = hovered ? 1 : 0.75;
5621
+ ctx.lineWidth = 1.2;
5622
+ ctx.setLineDash([]);
5623
+ const cx = buttonX + INDICATOR_CONTROL_SIZE / 2;
5624
+ const cy = centerY;
5625
+ if (action === "visibility") {
5626
+ ctx.beginPath();
5627
+ ctx.ellipse(cx, cy, 4.6, 3, 0, 0, Math.PI * 2);
5628
+ ctx.stroke();
5629
+ ctx.beginPath();
5630
+ ctx.arc(cx, cy, 1.3, 0, Math.PI * 2);
5631
+ ctx.fill();
5632
+ if (!indicatorVisible) {
5633
+ ctx.beginPath();
5634
+ ctx.moveTo(cx - 5.2, cy + 4.6);
5635
+ ctx.lineTo(cx + 5.2, cy - 4.6);
5636
+ ctx.stroke();
5637
+ }
5638
+ } else if (action === "settings") {
5639
+ ctx.beginPath();
5640
+ ctx.arc(cx, cy, 3.4, 0, Math.PI * 2);
5641
+ ctx.stroke();
5642
+ for (let toothIndex = 0; toothIndex < 8; toothIndex += 1) {
5643
+ const angle = Math.PI / 4 * toothIndex;
5644
+ ctx.beginPath();
5645
+ ctx.moveTo(cx + Math.cos(angle) * 3.4, cy + Math.sin(angle) * 3.4);
5646
+ ctx.lineTo(cx + Math.cos(angle) * 5.2, cy + Math.sin(angle) * 5.2);
5647
+ ctx.stroke();
5648
+ }
5649
+ ctx.beginPath();
5650
+ ctx.arc(cx, cy, 1.1, 0, Math.PI * 2);
5651
+ ctx.fill();
5652
+ } else if (action === "source") {
5653
+ const prevSourceFont = ctx.font;
5654
+ ctx.font = `bold 9px ${mergedOptions.fontFamily}`;
5655
+ drawText("{}", cx, cy + 0.5, "center", "middle", iconColor);
5656
+ ctx.font = prevSourceFont;
5657
+ } else {
5658
+ ctx.beginPath();
5659
+ ctx.moveTo(cx - 3.4, cy - 3.4);
5660
+ ctx.lineTo(cx + 3.4, cy + 3.4);
5661
+ ctx.moveTo(cx + 3.4, cy - 3.4);
5662
+ ctx.lineTo(cx - 3.4, cy + 3.4);
5663
+ ctx.stroke();
5664
+ }
5665
+ ctx.restore();
5666
+ paneButtonRegions.push({
5667
+ id: indicatorId,
5668
+ type: indicatorType,
5669
+ action,
5670
+ x: buttonX,
5671
+ y: buttonY,
5672
+ width: INDICATOR_CONTROL_SIZE,
5673
+ height: INDICATOR_CONTROL_SIZE
5674
+ });
5675
+ buttonX += INDICATOR_CONTROL_SIZE + INDICATOR_CONTROL_GAP;
5676
+ }
5677
+ };
5596
5678
  const drawMarkBadge = (x, y, radius, color, shape, label, textColor) => {
5597
5679
  ctx.save();
5598
5680
  ctx.fillStyle = color;
@@ -5673,6 +5755,8 @@ function createChart(element, options = {}) {
5673
5755
  orderDragRegions = [];
5674
5756
  alertRegions = [];
5675
5757
  markRegions = [];
5758
+ paneButtonRegions = [];
5759
+ legendRowRegions = [];
5676
5760
  crosshairPriceActionRegion = null;
5677
5761
  const pixelRatio = getPixelRatio();
5678
5762
  canvas.style.width = `${width}px`;
@@ -7761,87 +7845,10 @@ function createChart(element, options = {}) {
7761
7845
  }
7762
7846
  paneLayoutFullChartHeight = fullChartHeight;
7763
7847
  paneLayoutInfos = [];
7764
- paneButtonRegions = [];
7765
7848
  if (activeSeparateIndicators.length > 0) {
7766
7849
  const xFromIndex = (index) => chartLeft + (index + 0.5 - xStart) / xSpan * chartWidth;
7767
7850
  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
- };
7851
+ const drawPaneButtons = drawIndicatorControls;
7845
7852
  activeSeparateIndicators.forEach(({ indicator, plugin }, paneIndex) => {
7846
7853
  const paneHeight = separatePaneHeights[paneIndex] ?? 80;
7847
7854
  const paneTop = paneTopCursor;
@@ -8343,26 +8350,56 @@ function createChart(element, options = {}) {
8343
8350
  };
8344
8351
  const labelEntries = activeOverlayIndicators.map(({ indicator, plugin }) => {
8345
8352
  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);
8353
+ const text = labels.showIndicatorNames && labels.showIndicatorValues && inputValues.length > 0 ? `${plugin.name} ${inputValues.join(" ")}` : labels.showIndicatorNames ? plugin.name : inputValues.join(" ");
8354
+ return { id: indicator.id, type: indicator.type, visible: indicator.visible, text };
8355
+ }).filter((entry) => entry.text.length > 0);
8354
8356
  if (labelEntries.length > 0) {
8355
8357
  const prevFont = ctx.font;
8356
- ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
8357
- const legendText = labelEntries.join(" ");
8358
+ const legendFontSize = Math.max(8, axis.fontSize);
8359
+ ctx.font = `${legendFontSize}px ${mergedOptions.fontFamily}`;
8358
8360
  const offsetX = Math.max(0, Number(labels.indicatorLegendOffsetX) || 0);
8359
8361
  const offsetY = Math.max(0, Number(labels.indicatorLegendOffsetY) || 0);
8360
8362
  const position = labels.indicatorLegendPosition;
8361
8363
  const isRight = position === "top-right" || position === "bottom-right";
8362
8364
  const isBottom = position === "bottom-left" || position === "bottom-right";
8363
8365
  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);
8366
+ const legendTop = isBottom ? chartBottom - offsetY : Math.max(chartTop + offsetY, dataLineBottom);
8367
+ const rowHeight = Math.round(legendFontSize * 1.55);
8368
+ labelEntries.forEach((entry, rowIndex) => {
8369
+ const rowTop = isBottom ? legendTop - (labelEntries.length - rowIndex) * rowHeight : legendTop + rowIndex * rowHeight;
8370
+ const rowCenter = rowTop + rowHeight / 2;
8371
+ const dimmed = entry.visible === false;
8372
+ ctx.save();
8373
+ if (dimmed) ctx.globalAlpha = 0.45;
8374
+ drawText(
8375
+ entry.text,
8376
+ legendX,
8377
+ rowCenter,
8378
+ isRight ? "right" : "left",
8379
+ "middle",
8380
+ labels.indicatorTextColor
8381
+ );
8382
+ ctx.restore();
8383
+ const textWidth = measureTextWidth(entry.text);
8384
+ const rowLeft = isRight ? legendX - textWidth : legendX;
8385
+ legendRowRegions.push({
8386
+ id: entry.id,
8387
+ type: entry.type,
8388
+ x: rowLeft,
8389
+ y: rowTop,
8390
+ width: textWidth + LEGEND_CONTROLS_WIDTH,
8391
+ height: rowHeight
8392
+ });
8393
+ if (hoveredLegendId === entry.id) {
8394
+ drawIndicatorControls(
8395
+ entry.id,
8396
+ entry.type,
8397
+ entry.visible !== false,
8398
+ isRight ? legendX + 6 : rowLeft + textWidth + 8,
8399
+ rowCenter
8400
+ );
8401
+ }
8402
+ });
8366
8403
  ctx.font = prevFont;
8367
8404
  }
8368
8405
  }
@@ -8472,7 +8509,26 @@ function createChart(element, options = {}) {
8472
8509
  crosshairPriceActionRegion = null;
8473
8510
  const layout = overlayLayout;
8474
8511
  const crosshair = resolvedCrosshair;
8475
- if (!layout || !crosshair.visible || !crosshairPoint) {
8512
+ if (!layout) return;
8513
+ if (!crosshairPoint && externalCrosshair !== null && crosshair.visible) {
8514
+ const index = findNearestIndexForTimeMs(externalCrosshair.timeMs);
8515
+ if (index !== null) {
8516
+ const x = layout.chartLeft + (index + 0.5 - layout.xStart) / layout.xSpan * layout.chartWidth;
8517
+ if (x >= layout.chartLeft && x <= layout.chartRight) {
8518
+ ctx.save();
8519
+ ctx.globalAlpha = 0.55;
8520
+ ctx.strokeStyle = crosshair.color;
8521
+ ctx.lineWidth = Math.max(1, crosshair.width);
8522
+ applyDashPattern(crosshair.style, dashPatterns.dotted, dashPatterns.dashed);
8523
+ ctx.beginPath();
8524
+ ctx.moveTo(crisp(x), crisp(layout.chartTop));
8525
+ ctx.lineTo(crisp(x), crisp(layout.fullChartBottom));
8526
+ ctx.stroke();
8527
+ ctx.restore();
8528
+ }
8529
+ }
8530
+ }
8531
+ if (!crosshair.visible || !crosshairPoint) {
8476
8532
  return;
8477
8533
  }
8478
8534
  const { chartLeft, chartTop, chartRight, chartBottom, chartWidth, chartHeight, fullChartBottom, yMin, yRange, xStart, xSpan: xSpan2 } = layout;
@@ -9034,6 +9090,9 @@ function createChart(element, options = {}) {
9034
9090
  if (hits.length === 0) return null;
9035
9091
  return hits.find((region) => region.part === "remove") ?? hits[0];
9036
9092
  };
9093
+ const getLegendRowAt = (x, y) => legendRowRegions.find(
9094
+ (region) => x >= region.x && x <= region.x + region.width && y >= region.y && y <= region.y + region.height
9095
+ ) ?? null;
9037
9096
  const getMarkAt = (x, y) => {
9038
9097
  const tolerance = touchInputActive ? Math.max(1, mergedOptions.touch?.hitToleranceScale ?? 2.2) : 1;
9039
9098
  for (let index = markRegions.length - 1; index >= 0; index -= 1) {
@@ -10783,6 +10842,12 @@ function createChart(element, options = {}) {
10783
10842
  setCrosshairPoint(null);
10784
10843
  return;
10785
10844
  }
10845
+ const legendRow = getLegendRowAt(point.x, point.y);
10846
+ const nextLegendId = legendRow?.id ?? null;
10847
+ if (nextLegendId !== hoveredLegendId) {
10848
+ hoveredLegendId = nextLegendId;
10849
+ scheduleDraw();
10850
+ }
10786
10851
  const hoveredPane = getPaneAt(point.x, point.y);
10787
10852
  const nextHoveredPaneId = hoveredPane?.id ?? null;
10788
10853
  if (nextHoveredPaneId !== hoveredPaneId) {
@@ -11001,10 +11066,11 @@ function createChart(element, options = {}) {
11001
11066
  });
11002
11067
  paneDividerDrag = null;
11003
11068
  }
11004
- if (event?.type === "pointerleave" && (hoveredPaneId !== null || hoveredPaneButton !== null || hoveredAlertId !== null || hoveredMarkKey !== null)) {
11069
+ if (event?.type === "pointerleave" && (hoveredPaneId !== null || hoveredPaneButton !== null || hoveredAlertId !== null || hoveredMarkKey !== null || hoveredLegendId !== null)) {
11005
11070
  hoveredPaneId = null;
11006
11071
  hoveredPaneButton = null;
11007
11072
  hoveredAlertId = null;
11073
+ hoveredLegendId = null;
11008
11074
  if (hoveredMarkKey !== null) {
11009
11075
  hoveredMarkKey = null;
11010
11076
  markHoverHandler?.(null);
@@ -11944,6 +12010,12 @@ function createChart(element, options = {}) {
11944
12010
  const onAlertAction = (handler) => {
11945
12011
  alertActionHandler = handler;
11946
12012
  };
12013
+ const setExternalCrosshair = (point) => {
12014
+ const nextMs = point && Number.isFinite(point.timeMs) ? point.timeMs : null;
12015
+ if (nextMs === (externalCrosshair?.timeMs ?? null)) return;
12016
+ externalCrosshair = nextMs === null ? null : { timeMs: nextMs };
12017
+ scheduleDraw({ overlayOnly: true });
12018
+ };
11947
12019
  const setTimezone = (timezone) => {
11948
12020
  timezoneSetting = timezone || "local";
11949
12021
  zoneClock = createZoneClock(timezoneSetting);
@@ -12254,6 +12326,7 @@ function createChart(element, options = {}) {
12254
12326
  saveState,
12255
12327
  loadState,
12256
12328
  takeScreenshot,
12329
+ setExternalCrosshair,
12257
12330
  setTimezone,
12258
12331
  getTimezone,
12259
12332
  setTimeFormat,
package/dist/index.d.cts CHANGED
@@ -1030,6 +1030,14 @@ interface ChartInstance {
1030
1030
  * the crosshair tag and day/session boundaries move. Unknown ids fall back
1031
1031
  * to local time rather than throwing.
1032
1032
  */
1033
+ /**
1034
+ * Mirror a sibling chart's crosshair in a multi-chart layout. Matching is
1035
+ * by timestamp, so charts on different intervals or instruments still line
1036
+ * up. Pass null to clear.
1037
+ */
1038
+ setExternalCrosshair: (point: {
1039
+ timeMs: number;
1040
+ } | null) => void;
1033
1041
  setTimezone: (timezone: ChartTimezone) => void;
1034
1042
  getTimezone: () => ChartTimezone;
1035
1043
  /** Switch rendered clock times between 24-hour and 12-hour. */
package/dist/index.d.ts CHANGED
@@ -1030,6 +1030,14 @@ interface ChartInstance {
1030
1030
  * the crosshair tag and day/session boundaries move. Unknown ids fall back
1031
1031
  * to local time rather than throwing.
1032
1032
  */
1033
+ /**
1034
+ * Mirror a sibling chart's crosshair in a multi-chart layout. Matching is
1035
+ * by timestamp, so charts on different intervals or instruments still line
1036
+ * up. Pass null to clear.
1037
+ */
1038
+ setExternalCrosshair: (point: {
1039
+ timeMs: number;
1040
+ } | null) => void;
1033
1041
  setTimezone: (timezone: ChartTimezone) => void;
1034
1042
  getTimezone: () => ChartTimezone;
1035
1043
  /** Switch rendered clock times between 24-hour and 12-hour. */