hyperprop-charting-library 0.1.129 → 0.1.131

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.
@@ -759,6 +759,40 @@ var fillBetweenSeries = (ctx, renderContext, upper, lower, color, opacity) => {
759
759
  flushRun(renderContext.endIndex + 1);
760
760
  ctx.restore();
761
761
  };
762
+ var drawPaneHorizontalGrid = (ctx, renderContext, minValue, maxValue) => {
763
+ const grid = renderContext.grid;
764
+ if (!grid || !grid.horizontalLines) return;
765
+ const range = maxValue - minValue;
766
+ if (!(range > 0) || !Number.isFinite(range)) return;
767
+ const targetCount = Math.max(2, Math.round(renderContext.chartHeight / 42));
768
+ const targetStep = range / targetCount;
769
+ if (!(targetStep > 0) || !Number.isFinite(targetStep)) return;
770
+ const base = Math.pow(10, Math.floor(Math.log10(targetStep)));
771
+ const frac = targetStep / base;
772
+ const step = (frac <= 1 ? 1 : frac <= 2 ? 2 : frac <= 2.5 ? 2.5 : frac <= 5 ? 5 : 10) * base;
773
+ ctx.save();
774
+ ctx.globalAlpha = Math.min(1, Math.max(0, grid.opacity));
775
+ ctx.strokeStyle = grid.color;
776
+ ctx.lineWidth = 1;
777
+ ctx.setLineDash([]);
778
+ ctx.beginPath();
779
+ const firstTick = Math.ceil(minValue / step) * step;
780
+ const edgeInset = renderContext.chartHeight * 0.04;
781
+ for (let i = 0; i < 64; i += 1) {
782
+ const value = firstTick + i * step;
783
+ if (value > maxValue) break;
784
+ const ratio = (value - minValue) / range;
785
+ const y = renderContext.chartBottom - ratio * renderContext.chartHeight;
786
+ if (y > renderContext.chartBottom - edgeInset || y < renderContext.chartBottom - renderContext.chartHeight + edgeInset) {
787
+ continue;
788
+ }
789
+ const crispY = Math.round(y) + 0.5;
790
+ ctx.moveTo(renderContext.chartLeft, crispY);
791
+ ctx.lineTo(renderContext.chartRight, crispY);
792
+ }
793
+ ctx.stroke();
794
+ ctx.restore();
795
+ };
762
796
  var drawSeparateSeries = (ctx, renderContext, values, color, width, minOverride, maxOverride, guideLines, options = {}) => {
763
797
  const visible = [];
764
798
  for (let index = renderContext.startIndex; index <= renderContext.endIndex; index += 1) {
@@ -770,6 +804,7 @@ var drawSeparateSeries = (ctx, renderContext, values, color, width, minOverride,
770
804
  if (visible.length === 0) return void 0;
771
805
  const minValue = minOverride ?? Math.min(...visible);
772
806
  const maxValue = maxOverride ?? Math.max(...visible);
807
+ drawPaneHorizontalGrid(ctx, renderContext, minValue, maxValue);
773
808
  const range = maxValue - minValue || 1;
774
809
  const yFromValue = (value) => {
775
810
  const ratio = (value - minValue) / range;
@@ -1164,7 +1199,7 @@ var BUILTIN_STDDEV_INDICATOR = {
1164
1199
  name: "StdDev",
1165
1200
  pane: "separate",
1166
1201
  paneHeightRatio: 0.16,
1167
- defaultInputs: { length: 20, source: "close", color: "#f97316", width: 2, showValueLine: false },
1202
+ defaultInputs: { length: 20, source: "close", color: "#f97316", width: 2, showValueLine: true },
1168
1203
  draw: (ctx, renderContext, inputs) => {
1169
1204
  const length = clampIndicatorLength(inputs.length, 20);
1170
1205
  const values = withCachedSeries(
@@ -1175,7 +1210,7 @@ var BUILTIN_STDDEV_INDICATOR = {
1175
1210
  return drawSeparateSeries(ctx, renderContext, values, inputs.color ?? "#f97316", Number(inputs.width) || 2, void 0, void 0, void 0, {
1176
1211
  title: `StdDev ${length}`,
1177
1212
  decimals: 2,
1178
- valueLine: inputs.showValueLine === true
1213
+ valueLine: inputs.showValueLine !== false
1179
1214
  });
1180
1215
  }
1181
1216
  };
@@ -1184,14 +1219,14 @@ var BUILTIN_ATR_INDICATOR = {
1184
1219
  name: "ATR",
1185
1220
  pane: "separate",
1186
1221
  paneHeightRatio: 0.16,
1187
- defaultInputs: { length: 14, color: "#eab308", width: 2, showValueLine: false },
1222
+ defaultInputs: { length: 14, color: "#eab308", width: 2, showValueLine: true },
1188
1223
  draw: (ctx, renderContext, inputs) => {
1189
1224
  const length = clampIndicatorLength(inputs.length, 14);
1190
1225
  const values = withCachedSeries(`atr|${length}`, renderContext.data, () => computeAtrSeries(renderContext.data, length));
1191
1226
  return drawSeparateSeries(ctx, renderContext, values, inputs.color ?? "#eab308", Number(inputs.width) || 2, void 0, void 0, void 0, {
1192
1227
  title: `ATR ${length}`,
1193
1228
  decimals: 2,
1194
- valueLine: inputs.showValueLine === true
1229
+ valueLine: inputs.showValueLine !== false
1195
1230
  });
1196
1231
  }
1197
1232
  };
@@ -1208,7 +1243,7 @@ var BUILTIN_RSI_INDICATOR = {
1208
1243
  showValueLabel: true,
1209
1244
  showGuideLines: true,
1210
1245
  showScaleLabels: true,
1211
- showValueLine: false
1246
+ showValueLine: true
1212
1247
  },
1213
1248
  draw: (ctx, renderContext, inputs) => {
1214
1249
  const length = clampIndicatorLength(inputs.length, 14);
@@ -1232,7 +1267,7 @@ var BUILTIN_RSI_INDICATOR = {
1232
1267
  valueLabelBackgroundColor: "#9E9E9E",
1233
1268
  valueLabelTextColor: "#0f172a",
1234
1269
  showLegend: inputs.showLegend !== false,
1235
- valueLine: inputs.showValueLine === true
1270
+ valueLine: inputs.showValueLine !== false
1236
1271
  }
1237
1272
  );
1238
1273
  }
@@ -1289,6 +1324,7 @@ var drawSeparateMultiSeries = (ctx, renderContext, seriesList, options = {}) =>
1289
1324
  const ratio = (value - minValue) / range;
1290
1325
  return renderContext.chartBottom - ratio * renderContext.chartHeight;
1291
1326
  };
1327
+ drawPaneHorizontalGrid(ctx, renderContext, minValue, maxValue);
1292
1328
  if (options.guideLines && options.guideLines.length > 0) {
1293
1329
  ctx.save();
1294
1330
  ctx.strokeStyle = "rgba(148,163,184,0.35)";
@@ -1740,7 +1776,7 @@ var BUILTIN_MACD_INDICATOR = {
1740
1776
  signalColor: "#ff6d00",
1741
1777
  histUpColor: "#26a69a",
1742
1778
  histDownColor: "#ef5350",
1743
- showValueLine: false
1779
+ showValueLine: true
1744
1780
  },
1745
1781
  draw: (ctx, renderContext, inputs) => {
1746
1782
  const fast = clampIndicatorLength(inputs.fast, 12);
@@ -1770,7 +1806,7 @@ var BUILTIN_MACD_INDICATOR = {
1770
1806
  guideLines: [0],
1771
1807
  decimals: 2,
1772
1808
  valueLabelSeriesIndex: 1,
1773
- valueLines: inputs.showValueLine === true
1809
+ valueLines: inputs.showValueLine !== false
1774
1810
  }
1775
1811
  );
1776
1812
  }
@@ -1780,7 +1816,7 @@ var BUILTIN_STOCHASTIC_INDICATOR = {
1780
1816
  name: "Stoch",
1781
1817
  pane: "separate",
1782
1818
  paneHeightRatio: 0.18,
1783
- defaultInputs: { kLength: 14, kSmoothing: 1, dLength: 3, kColor: "#2962ff", dColor: "#ff6d00", showValueLine: false },
1819
+ defaultInputs: { kLength: 14, kSmoothing: 1, dLength: 3, kColor: "#2962ff", dColor: "#ff6d00", showValueLine: true },
1784
1820
  draw: (ctx, renderContext, inputs) => {
1785
1821
  const kLength = clampIndicatorLength(inputs.kLength, 14);
1786
1822
  const kSmoothing = clampIndicatorLength(inputs.kSmoothing, 1);
@@ -1804,7 +1840,7 @@ var BUILTIN_STOCHASTIC_INDICATOR = {
1804
1840
  guideLines: [20, 80],
1805
1841
  axisTicks: [0, 20, 50, 80, 100],
1806
1842
  decimals: 2,
1807
- valueLines: inputs.showValueLine === true
1843
+ valueLines: inputs.showValueLine !== false
1808
1844
  }
1809
1845
  );
1810
1846
  }
@@ -1821,7 +1857,7 @@ var BUILTIN_STOCHRSI_INDICATOR = {
1821
1857
  dSmoothing: 3,
1822
1858
  kColor: "#2962ff",
1823
1859
  dColor: "#ff6d00",
1824
- showValueLine: false
1860
+ showValueLine: true
1825
1861
  },
1826
1862
  draw: (ctx, renderContext, inputs) => {
1827
1863
  const rsiLength = clampIndicatorLength(inputs.rsiLength, 14);
@@ -1847,7 +1883,7 @@ var BUILTIN_STOCHRSI_INDICATOR = {
1847
1883
  guideLines: [20, 80],
1848
1884
  axisTicks: [0, 20, 50, 80, 100],
1849
1885
  decimals: 2,
1850
- valueLines: inputs.showValueLine === true
1886
+ valueLines: inputs.showValueLine !== false
1851
1887
  }
1852
1888
  );
1853
1889
  }
@@ -1863,7 +1899,7 @@ var BUILTIN_ADX_INDICATOR = {
1863
1899
  plusDiColor: "#26a69a",
1864
1900
  minusDiColor: "#ef5350",
1865
1901
  showDi: true,
1866
- showValueLine: false
1902
+ showValueLine: true
1867
1903
  },
1868
1904
  draw: (ctx, renderContext, inputs) => {
1869
1905
  const length = clampIndicatorLength(inputs.length, 14);
@@ -1886,7 +1922,7 @@ var BUILTIN_ADX_INDICATOR = {
1886
1922
  minOverride: 0,
1887
1923
  guideLines: [20],
1888
1924
  decimals: 2,
1889
- valueLines: inputs.showValueLine === true
1925
+ valueLines: inputs.showValueLine !== false
1890
1926
  });
1891
1927
  }
1892
1928
  };
@@ -1895,14 +1931,14 @@ var BUILTIN_OBV_INDICATOR = {
1895
1931
  name: "OBV",
1896
1932
  pane: "separate",
1897
1933
  paneHeightRatio: 0.16,
1898
- defaultInputs: { color: "#2962ff", width: 2, showValueLine: false },
1934
+ defaultInputs: { color: "#2962ff", width: 2, showValueLine: true },
1899
1935
  draw: (ctx, renderContext, inputs) => {
1900
1936
  const values = withCachedSeries("obv", renderContext.data, () => computeObvSeries(renderContext.data));
1901
1937
  return drawSeparateMultiSeries(
1902
1938
  ctx,
1903
1939
  renderContext,
1904
1940
  [{ values, color: inputs.color ?? "#2962ff", width: Number(inputs.width) || 2, label: "OBV" }],
1905
- { title: "OBV", format: formatCompactNumber, valueLines: inputs.showValueLine === true }
1941
+ { title: "OBV", format: formatCompactNumber, valueLines: inputs.showValueLine !== false }
1906
1942
  );
1907
1943
  }
1908
1944
  };
@@ -1911,7 +1947,7 @@ var BUILTIN_MFI_INDICATOR = {
1911
1947
  name: "MFI",
1912
1948
  pane: "separate",
1913
1949
  paneHeightRatio: 0.16,
1914
- defaultInputs: { length: 14, color: "#7e57c2", width: 2, showValueLine: false },
1950
+ defaultInputs: { length: 14, color: "#7e57c2", width: 2, showValueLine: true },
1915
1951
  draw: (ctx, renderContext, inputs) => {
1916
1952
  const length = clampIndicatorLength(inputs.length, 14);
1917
1953
  const values = withCachedSeries(
@@ -1930,7 +1966,7 @@ var BUILTIN_MFI_INDICATOR = {
1930
1966
  guideLines: [20, 80],
1931
1967
  axisTicks: [0, 20, 50, 80, 100],
1932
1968
  decimals: 2,
1933
- valueLines: inputs.showValueLine === true
1969
+ valueLines: inputs.showValueLine !== false
1934
1970
  }
1935
1971
  );
1936
1972
  }
@@ -1940,7 +1976,7 @@ var BUILTIN_CCI_INDICATOR = {
1940
1976
  name: "CCI",
1941
1977
  pane: "separate",
1942
1978
  paneHeightRatio: 0.16,
1943
- defaultInputs: { length: 20, color: "#2962ff", width: 2, showValueLine: false },
1979
+ defaultInputs: { length: 20, color: "#2962ff", width: 2, showValueLine: true },
1944
1980
  draw: (ctx, renderContext, inputs) => {
1945
1981
  const length = clampIndicatorLength(inputs.length, 20);
1946
1982
  const values = withCachedSeries(
@@ -1952,7 +1988,7 @@ var BUILTIN_CCI_INDICATOR = {
1952
1988
  ctx,
1953
1989
  renderContext,
1954
1990
  [{ values, color: inputs.color ?? "#2962ff", width: Number(inputs.width) || 2 }],
1955
- { title: `CCI ${length}`, guideLines: [-100, 0, 100], decimals: 2, valueLines: inputs.showValueLine === true }
1991
+ { title: `CCI ${length}`, guideLines: [-100, 0, 100], decimals: 2, valueLines: inputs.showValueLine !== false }
1956
1992
  );
1957
1993
  }
1958
1994
  };
@@ -1961,7 +1997,7 @@ var BUILTIN_WILLIAMSR_INDICATOR = {
1961
1997
  name: "Williams %R",
1962
1998
  pane: "separate",
1963
1999
  paneHeightRatio: 0.16,
1964
- defaultInputs: { length: 14, color: "#7e57c2", width: 2, showValueLine: false },
2000
+ defaultInputs: { length: 14, color: "#7e57c2", width: 2, showValueLine: true },
1965
2001
  draw: (ctx, renderContext, inputs) => {
1966
2002
  const length = clampIndicatorLength(inputs.length, 14);
1967
2003
  const values = withCachedSeries(
@@ -1980,7 +2016,7 @@ var BUILTIN_WILLIAMSR_INDICATOR = {
1980
2016
  guideLines: [-80, -20],
1981
2017
  axisTicks: [-100, -80, -50, -20, 0],
1982
2018
  decimals: 2,
1983
- valueLines: inputs.showValueLine === true
2019
+ valueLines: inputs.showValueLine !== false
1984
2020
  }
1985
2021
  );
1986
2022
  }
@@ -1990,7 +2026,7 @@ var BUILTIN_ROC_INDICATOR = {
1990
2026
  name: "ROC",
1991
2027
  pane: "separate",
1992
2028
  paneHeightRatio: 0.16,
1993
- defaultInputs: { length: 9, color: "#2962ff", width: 2, showValueLine: false },
2029
+ defaultInputs: { length: 9, color: "#2962ff", width: 2, showValueLine: true },
1994
2030
  draw: (ctx, renderContext, inputs) => {
1995
2031
  const length = clampIndicatorLength(inputs.length, 9);
1996
2032
  const values = withCachedSeries(
@@ -2002,7 +2038,7 @@ var BUILTIN_ROC_INDICATOR = {
2002
2038
  ctx,
2003
2039
  renderContext,
2004
2040
  [{ values, color: inputs.color ?? "#2962ff", width: Number(inputs.width) || 2 }],
2005
- { title: `ROC ${length}`, includeZero: true, guideLines: [0], decimals: 2, valueLines: inputs.showValueLine === true }
2041
+ { title: `ROC ${length}`, includeZero: true, guideLines: [0], decimals: 2, valueLines: inputs.showValueLine !== false }
2006
2042
  );
2007
2043
  }
2008
2044
  };
@@ -2011,7 +2047,7 @@ var BUILTIN_MOMENTUM_INDICATOR = {
2011
2047
  name: "Momentum",
2012
2048
  pane: "separate",
2013
2049
  paneHeightRatio: 0.16,
2014
- defaultInputs: { length: 10, color: "#2962ff", width: 2, showValueLine: false },
2050
+ defaultInputs: { length: 10, color: "#2962ff", width: 2, showValueLine: true },
2015
2051
  draw: (ctx, renderContext, inputs) => {
2016
2052
  const length = clampIndicatorLength(inputs.length, 10);
2017
2053
  const values = withCachedSeries(
@@ -2023,7 +2059,7 @@ var BUILTIN_MOMENTUM_INDICATOR = {
2023
2059
  ctx,
2024
2060
  renderContext,
2025
2061
  [{ values, color: inputs.color ?? "#2962ff", width: Number(inputs.width) || 2 }],
2026
- { title: `Mom ${length}`, includeZero: true, guideLines: [0], decimals: 2, valueLines: inputs.showValueLine === true }
2062
+ { title: `Mom ${length}`, includeZero: true, guideLines: [0], decimals: 2, valueLines: inputs.showValueLine !== false }
2027
2063
  );
2028
2064
  }
2029
2065
  };
@@ -2447,6 +2483,14 @@ function createChart(element, options = {}) {
2447
2483
  let watermarkImageReady = false;
2448
2484
  let drawState = null;
2449
2485
  let plotBottomForHit = 0;
2486
+ let paneLayoutFullChartHeight = 0;
2487
+ let paneLayoutInfos = [];
2488
+ let paneButtonRegions = [];
2489
+ let hoveredPaneId = null;
2490
+ let hoveredPaneButton = null;
2491
+ let paneDividerDrag = null;
2492
+ let indicatorPaneActionHandler = null;
2493
+ let indicatorPaneHeightChangeHandler = null;
2450
2494
  let orderDragState = null;
2451
2495
  let actionDragState = null;
2452
2496
  let pointerDownInfo = null;
@@ -3548,20 +3592,24 @@ function createChart(element, options = {}) {
3548
3592
  const watermark = { ...DEFAULT_WATERMARK_OPTIONS, ...mergedOptions.watermark ?? {} };
3549
3593
  const paneGap = 0;
3550
3594
  const separatePaneSpacing = 6;
3551
- const activeSeparateIndicators = indicators.filter((indicator) => indicator.visible).map((indicator) => ({ indicator, plugin: indicatorRegistry.get(indicator.type) })).filter(
3595
+ const collapsedPaneHeight = 26;
3596
+ const activeSeparateIndicators = indicators.map((indicator) => ({ indicator, plugin: indicatorRegistry.get(indicator.type) })).filter(
3552
3597
  (value) => value.plugin !== void 0 && (value.indicator.pane ?? value.plugin.pane ?? "overlay") === "separate"
3553
3598
  ).sort((a, b) => a.indicator.zIndex - b.indicator.zIndex);
3554
3599
  const overlayIndicatorsForScale = indicators.filter((indicator) => indicator.visible).map((indicator) => ({ indicator, plugin: indicatorRegistry.get(indicator.type) })).filter(
3555
3600
  (value) => value.plugin !== void 0 && (value.indicator.pane ?? value.plugin.pane ?? "overlay") === "overlay"
3556
3601
  );
3557
3602
  const separatePaneHeightDefaults = activeSeparateIndicators.map(({ indicator, plugin }) => {
3603
+ if (!indicator.visible) return collapsedPaneHeight;
3558
3604
  const ratio = Math.min(0.45, Math.max(0.08, indicator.paneHeightRatio ?? plugin.paneHeightRatio ?? 0.22));
3559
3605
  return Math.round(fullChartHeight * ratio);
3560
3606
  });
3561
3607
  const separatePaneDesiredTotal = separatePaneHeightDefaults.reduce((sum, value) => sum + value, 0) + Math.max(0, activeSeparateIndicators.length - 1) * separatePaneSpacing;
3562
3608
  const maxSeparatePaneTotal = Math.max(0, fullChartHeight - 140);
3563
3609
  const separatePaneScale = separatePaneDesiredTotal > 0 && separatePaneDesiredTotal > maxSeparatePaneTotal ? maxSeparatePaneTotal / separatePaneDesiredTotal : 1;
3564
- const separatePaneHeights = separatePaneHeightDefaults.map((value) => Math.max(48, Math.round(value * separatePaneScale)));
3610
+ const separatePaneHeights = separatePaneHeightDefaults.map(
3611
+ (value, index) => activeSeparateIndicators[index].indicator.visible ? Math.max(48, Math.round(value * separatePaneScale)) : collapsedPaneHeight
3612
+ );
3565
3613
  const separatePaneTotal = separatePaneHeights.reduce((sum, value) => sum + value, 0) + Math.max(0, activeSeparateIndicators.length - 1) * separatePaneSpacing;
3566
3614
  const pricePaneGap = activeSeparateIndicators.length > 0 ? paneGap : 0;
3567
3615
  const chartHeight = Math.max(120, fullChartHeight - separatePaneTotal - pricePaneGap);
@@ -4812,43 +4860,138 @@ function createChart(element, options = {}) {
4812
4860
  ctx.restore();
4813
4861
  }
4814
4862
  }
4863
+ paneLayoutFullChartHeight = fullChartHeight;
4864
+ paneLayoutInfos = [];
4865
+ paneButtonRegions = [];
4815
4866
  if (activeSeparateIndicators.length > 0) {
4816
4867
  const xFromIndex = (index) => chartLeft + (index + 0.5 - xStart) / xSpan * chartWidth;
4817
4868
  let paneTopCursor = chartBottom + paneGap;
4869
+ const paneButtonSize = 16;
4870
+ const paneButtonGap = 4;
4871
+ const drawPaneButtons = (indicatorId, indicatorType, indicatorVisible, startX, centerY) => {
4872
+ const actionList = [
4873
+ "visibility",
4874
+ "settings",
4875
+ "source",
4876
+ "remove"
4877
+ ];
4878
+ let buttonX = startX;
4879
+ for (const action of actionList) {
4880
+ const buttonY = centerY - paneButtonSize / 2;
4881
+ const hovered = hoveredPaneButton?.id === indicatorId && hoveredPaneButton.action === action;
4882
+ const iconColor = labels.indicatorTextColor;
4883
+ ctx.save();
4884
+ ctx.fillStyle = hovered ? "rgba(148,163,184,0.30)" : "rgba(148,163,184,0.12)";
4885
+ fillRoundedRect(Math.round(buttonX), Math.round(buttonY), paneButtonSize, paneButtonSize, 4);
4886
+ ctx.strokeStyle = iconColor;
4887
+ ctx.fillStyle = iconColor;
4888
+ ctx.globalAlpha = hovered ? 1 : 0.75;
4889
+ ctx.lineWidth = 1.2;
4890
+ ctx.setLineDash([]);
4891
+ const cx = buttonX + paneButtonSize / 2;
4892
+ const cy = centerY;
4893
+ if (action === "visibility") {
4894
+ ctx.beginPath();
4895
+ ctx.ellipse(cx, cy, 4.6, 3, 0, 0, Math.PI * 2);
4896
+ ctx.stroke();
4897
+ ctx.beginPath();
4898
+ ctx.arc(cx, cy, 1.3, 0, Math.PI * 2);
4899
+ ctx.fill();
4900
+ if (!indicatorVisible) {
4901
+ ctx.beginPath();
4902
+ ctx.moveTo(cx - 5.2, cy + 4.6);
4903
+ ctx.lineTo(cx + 5.2, cy - 4.6);
4904
+ ctx.stroke();
4905
+ }
4906
+ } else if (action === "settings") {
4907
+ ctx.beginPath();
4908
+ ctx.arc(cx, cy, 3.4, 0, Math.PI * 2);
4909
+ ctx.stroke();
4910
+ for (let toothIndex = 0; toothIndex < 8; toothIndex += 1) {
4911
+ const angle = Math.PI / 4 * toothIndex;
4912
+ ctx.beginPath();
4913
+ ctx.moveTo(cx + Math.cos(angle) * 3.4, cy + Math.sin(angle) * 3.4);
4914
+ ctx.lineTo(cx + Math.cos(angle) * 5.2, cy + Math.sin(angle) * 5.2);
4915
+ ctx.stroke();
4916
+ }
4917
+ ctx.beginPath();
4918
+ ctx.arc(cx, cy, 1.1, 0, Math.PI * 2);
4919
+ ctx.fill();
4920
+ } else if (action === "source") {
4921
+ const prevSourceFont = ctx.font;
4922
+ ctx.font = `bold 9px ${mergedOptions.fontFamily}`;
4923
+ drawText("{}", cx, cy + 0.5, "center", "middle", iconColor);
4924
+ ctx.font = prevSourceFont;
4925
+ } else {
4926
+ ctx.beginPath();
4927
+ ctx.moveTo(cx - 3.4, cy - 3.4);
4928
+ ctx.lineTo(cx + 3.4, cy + 3.4);
4929
+ ctx.moveTo(cx + 3.4, cy - 3.4);
4930
+ ctx.lineTo(cx - 3.4, cy + 3.4);
4931
+ ctx.stroke();
4932
+ }
4933
+ ctx.restore();
4934
+ paneButtonRegions.push({
4935
+ id: indicatorId,
4936
+ type: indicatorType,
4937
+ action,
4938
+ x: buttonX,
4939
+ y: buttonY,
4940
+ width: paneButtonSize,
4941
+ height: paneButtonSize
4942
+ });
4943
+ buttonX += paneButtonSize + paneButtonGap;
4944
+ }
4945
+ };
4818
4946
  activeSeparateIndicators.forEach(({ indicator, plugin }, paneIndex) => {
4819
4947
  const paneHeight = separatePaneHeights[paneIndex] ?? 80;
4820
4948
  const paneTop = paneTopCursor;
4821
4949
  const paneBottom = paneTop + paneHeight;
4822
4950
  paneTopCursor = paneBottom + separatePaneSpacing;
4823
- ctx.save();
4824
- ctx.beginPath();
4825
- ctx.rect(chartLeft + 1, paneTop + 1, Math.max(0, chartWidth - 2), Math.max(0, paneHeight - 2));
4826
- ctx.clip();
4827
- const paneInfo = plugin.draw(
4828
- ctx,
4829
- {
4830
- data,
4831
- startIndex,
4832
- endIndex,
4833
- xStart,
4834
- xSpan,
4835
- chartLeft,
4836
- chartRight,
4837
- chartTop: paneTop,
4838
- chartBottom: paneBottom,
4839
- chartWidth,
4840
- chartHeight: paneHeight,
4841
- xFromIndex,
4842
- yFromPrice: null,
4843
- getCandleDirectionByIndex,
4844
- getVolumeByIndex,
4845
- candleSpacing,
4846
- upColor: mergedOptions.upColor,
4847
- downColor: mergedOptions.downColor
4848
- },
4849
- indicator.inputs
4850
- );
4851
- ctx.restore();
4951
+ paneLayoutInfos.push({
4952
+ id: indicator.id,
4953
+ type: indicator.type,
4954
+ top: paneTop,
4955
+ bottom: paneBottom,
4956
+ visible: indicator.visible
4957
+ });
4958
+ let paneInfo;
4959
+ if (indicator.visible) {
4960
+ ctx.save();
4961
+ ctx.beginPath();
4962
+ ctx.rect(chartLeft + 1, paneTop + 1, Math.max(0, chartWidth - 2), Math.max(0, paneHeight - 2));
4963
+ ctx.clip();
4964
+ paneInfo = plugin.draw(
4965
+ ctx,
4966
+ {
4967
+ data,
4968
+ startIndex,
4969
+ endIndex,
4970
+ xStart,
4971
+ xSpan,
4972
+ chartLeft,
4973
+ chartRight,
4974
+ chartTop: paneTop,
4975
+ chartBottom: paneBottom,
4976
+ chartWidth,
4977
+ chartHeight: paneHeight,
4978
+ xFromIndex,
4979
+ yFromPrice: null,
4980
+ getCandleDirectionByIndex,
4981
+ getVolumeByIndex,
4982
+ candleSpacing,
4983
+ upColor: mergedOptions.upColor,
4984
+ downColor: mergedOptions.downColor,
4985
+ grid: {
4986
+ color: gridColor,
4987
+ opacity: gridOpacity,
4988
+ horizontalLines: grid.horizontalLines !== false
4989
+ }
4990
+ },
4991
+ indicator.inputs
4992
+ ) ?? void 0;
4993
+ ctx.restore();
4994
+ }
4852
4995
  ctx.save();
4853
4996
  ctx.strokeStyle = axis.lineColor;
4854
4997
  ctx.lineWidth = Math.max(1, axis.lineWidth);
@@ -4857,6 +5000,27 @@ function createChart(element, options = {}) {
4857
5000
  ctx.lineTo(crisp(chartRight), crisp(paneTop));
4858
5001
  ctx.stroke();
4859
5002
  ctx.restore();
5003
+ if (!indicator.visible) {
5004
+ if (labels.visible) {
5005
+ const prevFont = ctx.font;
5006
+ ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
5007
+ const hiddenText = `${plugin.name} (hidden)`;
5008
+ const rowCenterY = (paneTop + paneBottom) / 2;
5009
+ ctx.save();
5010
+ ctx.globalAlpha = 0.55;
5011
+ drawText(hiddenText, chartLeft + 10, rowCenterY, "left", "middle", labels.indicatorTextColor);
5012
+ ctx.restore();
5013
+ drawPaneButtons(
5014
+ indicator.id,
5015
+ indicator.type,
5016
+ false,
5017
+ chartLeft + 10 + Math.ceil(measureTextWidth(hiddenText)) + 10,
5018
+ rowCenterY
5019
+ );
5020
+ ctx.font = prevFont;
5021
+ }
5022
+ return;
5023
+ }
4860
5024
  const axisInfo = paneInfo?.axis;
4861
5025
  if (axisInfo && Number.isFinite(axisInfo.min) && Number.isFinite(axisInfo.max) && axisInfo.max !== axisInfo.min) {
4862
5026
  const paneRange = axisInfo.max - axisInfo.min;
@@ -4886,17 +5050,27 @@ function createChart(element, options = {}) {
4886
5050
  ctx.restore();
4887
5051
  if (labels.visible) {
4888
5052
  const prevFont = ctx.font;
4889
- ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
4890
- const legendTitle = paneInfo.title ?? plugin.name;
4891
- const legendValues = paneInfo.legendValues ?? [];
5053
+ const legendFontSize = Math.max(8, axis.fontSize);
5054
+ ctx.font = `${legendFontSize}px ${mergedOptions.fontFamily}`;
5055
+ const legendTitle = paneInfo?.title ?? plugin.name;
5056
+ const legendValues = paneInfo?.legendValues ?? [];
4892
5057
  const legendParts = [
4893
5058
  legendTitle,
4894
5059
  ...legendValues.map((value) => value.text ?? (value.value === void 0 ? "" : formatPaneValue(value.value))).filter(Boolean)
4895
5060
  ].filter(Boolean);
4896
- if (legendParts.length > 0) {
5061
+ if (hoveredPaneId === indicator.id) {
5062
+ drawText(legendTitle, chartLeft + 10, paneTop + 8, "left", "top", labels.indicatorTextColor);
5063
+ drawPaneButtons(
5064
+ indicator.id,
5065
+ indicator.type,
5066
+ true,
5067
+ chartLeft + 10 + Math.ceil(measureTextWidth(legendTitle)) + 10,
5068
+ paneTop + 8 + legendFontSize / 2
5069
+ );
5070
+ } else if (legendParts.length > 0) {
4897
5071
  drawText(legendParts.join(" "), chartLeft + 10, paneTop + 8, "left", "top", labels.indicatorTextColor);
4898
5072
  }
4899
- for (const label of paneInfo.valueLabels ?? []) {
5073
+ for (const label of paneInfo?.valueLabels ?? []) {
4900
5074
  if (!labels.showIndicatorValueLabels) {
4901
5075
  continue;
4902
5076
  }
@@ -6108,6 +6282,54 @@ function createChart(element, options = {}) {
6108
6282
  }
6109
6283
  return "outside";
6110
6284
  };
6285
+ const getPaneButtonHit = (x, y) => {
6286
+ for (const region of paneButtonRegions) {
6287
+ if (x >= region.x && x <= region.x + region.width && y >= region.y && y <= region.y + region.height) {
6288
+ return { id: region.id, type: region.type, action: region.action };
6289
+ }
6290
+ }
6291
+ return null;
6292
+ };
6293
+ const getPaneAt = (x, y) => {
6294
+ if (!drawState || x < drawState.chartLeft || x > drawState.chartRight) {
6295
+ return null;
6296
+ }
6297
+ for (const pane of paneLayoutInfos) {
6298
+ if (y >= pane.top && y <= pane.bottom) {
6299
+ return pane;
6300
+ }
6301
+ }
6302
+ return null;
6303
+ };
6304
+ const getPaneDividerHit = (x, y) => {
6305
+ if (!drawState || x < drawState.chartLeft || x > drawState.chartRight) {
6306
+ return null;
6307
+ }
6308
+ for (const pane of paneLayoutInfos) {
6309
+ if (pane.visible && Math.abs(y - pane.top) <= 4) {
6310
+ return { id: pane.id, heightPx: pane.bottom - pane.top };
6311
+ }
6312
+ }
6313
+ return null;
6314
+ };
6315
+ const handlePaneButtonAction = (hit) => {
6316
+ const indicator = indicators.find((entry) => entry.id === hit.id);
6317
+ if (!indicator) {
6318
+ return;
6319
+ }
6320
+ if (hit.action === "visibility") {
6321
+ indicator.visible = !indicator.visible;
6322
+ scheduleDraw();
6323
+ indicatorPaneActionHandler?.({ id: hit.id, type: hit.type, action: "visibility", visible: indicator.visible });
6324
+ return;
6325
+ }
6326
+ if (hit.action === "remove" && !indicatorPaneActionHandler) {
6327
+ indicators = indicators.filter((entry) => entry.id !== hit.id);
6328
+ scheduleDraw();
6329
+ return;
6330
+ }
6331
+ indicatorPaneActionHandler?.({ id: hit.id, type: hit.type, action: hit.action });
6332
+ };
6111
6333
  const handleDrawingToolPointerDown = (x, y) => {
6112
6334
  if (!activeDrawingTool || !drawState) {
6113
6335
  return false;
@@ -6587,6 +6809,26 @@ function createChart(element, options = {}) {
6587
6809
  setCrosshairPoint(null);
6588
6810
  return;
6589
6811
  }
6812
+ const paneButtonHit = getPaneButtonHit(point.x, point.y);
6813
+ if (paneButtonHit) {
6814
+ setCrosshairPoint(null);
6815
+ handlePaneButtonAction(paneButtonHit);
6816
+ return;
6817
+ }
6818
+ const paneDividerHit = getPaneDividerHit(point.x, point.y);
6819
+ if (paneDividerHit) {
6820
+ activePointerId = event.pointerId;
6821
+ paneDividerDrag = {
6822
+ id: paneDividerHit.id,
6823
+ startY: point.y,
6824
+ startHeightPx: paneDividerHit.heightPx,
6825
+ lastRatio: paneLayoutFullChartHeight > 0 ? paneDividerHit.heightPx / paneLayoutFullChartHeight : 0.2
6826
+ };
6827
+ capturePointer(event.pointerId);
6828
+ canvas.style.cursor = "row-resize";
6829
+ setCrosshairPoint(null);
6830
+ return;
6831
+ }
6590
6832
  }
6591
6833
  const region = getHitRegion(point.x, point.y);
6592
6834
  if (region === "outside") {
@@ -6711,6 +6953,32 @@ function createChart(element, options = {}) {
6711
6953
  return;
6712
6954
  }
6713
6955
  }
6956
+ if (paneDividerDrag) {
6957
+ if (activePointerId !== null && event.pointerId !== activePointerId) {
6958
+ return;
6959
+ }
6960
+ const fullHeight = paneLayoutFullChartHeight;
6961
+ if (fullHeight > 0) {
6962
+ const nextHeightPx = clamp(
6963
+ paneDividerDrag.startHeightPx + (paneDividerDrag.startY - point.y),
6964
+ 48,
6965
+ Math.round(fullHeight * 0.45)
6966
+ );
6967
+ const nextRatio = nextHeightPx / fullHeight;
6968
+ if (Math.abs(nextRatio - paneDividerDrag.lastRatio) > 5e-4) {
6969
+ paneDividerDrag.lastRatio = nextRatio;
6970
+ const targetIndicator = indicators.find((entry) => entry.id === paneDividerDrag?.id);
6971
+ if (targetIndicator) {
6972
+ targetIndicator.paneHeightRatio = nextRatio;
6973
+ indicatorPaneHeightChangeHandler?.({ id: paneDividerDrag.id, paneHeightRatio: nextRatio, dragging: true });
6974
+ scheduleDraw();
6975
+ }
6976
+ }
6977
+ }
6978
+ canvas.style.cursor = "row-resize";
6979
+ setCrosshairPoint(null);
6980
+ return;
6981
+ }
6714
6982
  if (orderDragState) {
6715
6983
  if (activePointerId !== null && event.pointerId !== activePointerId) {
6716
6984
  return;
@@ -6783,6 +7051,33 @@ function createChart(element, options = {}) {
6783
7051
  setCrosshairPoint(null);
6784
7052
  return;
6785
7053
  }
7054
+ const hoveredPane = getPaneAt(point.x, point.y);
7055
+ const nextHoveredPaneId = hoveredPane?.id ?? null;
7056
+ if (nextHoveredPaneId !== hoveredPaneId) {
7057
+ hoveredPaneId = nextHoveredPaneId;
7058
+ scheduleDraw();
7059
+ }
7060
+ const paneButtonHover = getPaneButtonHit(point.x, point.y);
7061
+ const nextHoveredButton = paneButtonHover ? { id: paneButtonHover.id, action: paneButtonHover.action } : null;
7062
+ if ((nextHoveredButton?.id ?? null) !== (hoveredPaneButton?.id ?? null) || nextHoveredButton?.action !== hoveredPaneButton?.action) {
7063
+ hoveredPaneButton = nextHoveredButton;
7064
+ scheduleDraw();
7065
+ }
7066
+ if (paneButtonHover) {
7067
+ const indicatorForTitle = indicators.find((entry) => entry.id === paneButtonHover.id);
7068
+ canvas.title = paneButtonHover.action === "visibility" ? indicatorForTitle?.visible === false ? "Show" : "Hide" : paneButtonHover.action === "settings" ? "Settings" : paneButtonHover.action === "source" ? "View source" : "Remove";
7069
+ canvas.style.cursor = "pointer";
7070
+ setCrosshairPoint(null);
7071
+ return;
7072
+ }
7073
+ if (canvas.title) {
7074
+ canvas.title = "";
7075
+ }
7076
+ if (getPaneDividerHit(point.x, point.y)) {
7077
+ canvas.style.cursor = "row-resize";
7078
+ setCrosshairPoint(null);
7079
+ return;
7080
+ }
6786
7081
  }
6787
7082
  if (!activeDrawingTool && getDrawingHit(point.x, point.y)) {
6788
7083
  const drawingHit = getDrawingHit(point.x, point.y);
@@ -6902,6 +7197,22 @@ function createChart(element, options = {}) {
6902
7197
  drawingDragState = null;
6903
7198
  emitDrawingsChange();
6904
7199
  }
7200
+ if (paneDividerDrag) {
7201
+ indicatorPaneHeightChangeHandler?.({
7202
+ id: paneDividerDrag.id,
7203
+ paneHeightRatio: paneDividerDrag.lastRatio,
7204
+ dragging: false
7205
+ });
7206
+ paneDividerDrag = null;
7207
+ }
7208
+ if (event?.type === "pointerleave" && (hoveredPaneId !== null || hoveredPaneButton !== null)) {
7209
+ hoveredPaneId = null;
7210
+ hoveredPaneButton = null;
7211
+ if (canvas.title) {
7212
+ canvas.title = "";
7213
+ }
7214
+ scheduleDraw();
7215
+ }
6905
7216
  const endedPlotDrag = isDragging && dragMode === "plot";
6906
7217
  isDragging = false;
6907
7218
  dragMode = null;
@@ -7247,6 +7558,12 @@ function createChart(element, options = {}) {
7247
7558
  const onCrosshairPriceAction = (handler) => {
7248
7559
  crosshairPriceActionHandler = handler;
7249
7560
  };
7561
+ const onIndicatorPaneAction = (handler) => {
7562
+ indicatorPaneActionHandler = handler;
7563
+ };
7564
+ const onIndicatorPaneHeightChange = (handler) => {
7565
+ indicatorPaneHeightChangeHandler = handler;
7566
+ };
7250
7567
  const setDoubleClickEnabled = (enabled) => {
7251
7568
  doubleClickEnabled = enabled;
7252
7569
  };
@@ -7452,6 +7769,8 @@ function createChart(element, options = {}) {
7452
7769
  onChartClick,
7453
7770
  onCrosshairMove,
7454
7771
  onCrosshairPriceAction,
7772
+ onIndicatorPaneAction,
7773
+ onIndicatorPaneHeightChange,
7455
7774
  zoomInX,
7456
7775
  zoomOutX,
7457
7776
  zoomInY,