hyperprop-charting-library 0.1.129 → 0.1.130

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
@@ -785,6 +785,40 @@ var fillBetweenSeries = (ctx, renderContext, upper, lower, color, opacity) => {
785
785
  flushRun(renderContext.endIndex + 1);
786
786
  ctx.restore();
787
787
  };
788
+ var drawPaneHorizontalGrid = (ctx, renderContext, minValue, maxValue) => {
789
+ const grid = renderContext.grid;
790
+ if (!grid || !grid.horizontalLines) return;
791
+ const range = maxValue - minValue;
792
+ if (!(range > 0) || !Number.isFinite(range)) return;
793
+ const targetCount = Math.max(2, Math.round(renderContext.chartHeight / 42));
794
+ const targetStep = range / targetCount;
795
+ if (!(targetStep > 0) || !Number.isFinite(targetStep)) return;
796
+ const base = Math.pow(10, Math.floor(Math.log10(targetStep)));
797
+ const frac = targetStep / base;
798
+ const step = (frac <= 1 ? 1 : frac <= 2 ? 2 : frac <= 2.5 ? 2.5 : frac <= 5 ? 5 : 10) * base;
799
+ ctx.save();
800
+ ctx.globalAlpha = Math.min(1, Math.max(0, grid.opacity));
801
+ ctx.strokeStyle = grid.color;
802
+ ctx.lineWidth = 1;
803
+ ctx.setLineDash([]);
804
+ ctx.beginPath();
805
+ const firstTick = Math.ceil(minValue / step) * step;
806
+ const edgeInset = renderContext.chartHeight * 0.04;
807
+ for (let i = 0; i < 64; i += 1) {
808
+ const value = firstTick + i * step;
809
+ if (value > maxValue) break;
810
+ const ratio = (value - minValue) / range;
811
+ const y = renderContext.chartBottom - ratio * renderContext.chartHeight;
812
+ if (y > renderContext.chartBottom - edgeInset || y < renderContext.chartBottom - renderContext.chartHeight + edgeInset) {
813
+ continue;
814
+ }
815
+ const crispY = Math.round(y) + 0.5;
816
+ ctx.moveTo(renderContext.chartLeft, crispY);
817
+ ctx.lineTo(renderContext.chartRight, crispY);
818
+ }
819
+ ctx.stroke();
820
+ ctx.restore();
821
+ };
788
822
  var drawSeparateSeries = (ctx, renderContext, values, color, width, minOverride, maxOverride, guideLines, options = {}) => {
789
823
  const visible = [];
790
824
  for (let index = renderContext.startIndex; index <= renderContext.endIndex; index += 1) {
@@ -796,6 +830,7 @@ var drawSeparateSeries = (ctx, renderContext, values, color, width, minOverride,
796
830
  if (visible.length === 0) return void 0;
797
831
  const minValue = minOverride ?? Math.min(...visible);
798
832
  const maxValue = maxOverride ?? Math.max(...visible);
833
+ drawPaneHorizontalGrid(ctx, renderContext, minValue, maxValue);
799
834
  const range = maxValue - minValue || 1;
800
835
  const yFromValue = (value) => {
801
836
  const ratio = (value - minValue) / range;
@@ -1190,7 +1225,7 @@ var BUILTIN_STDDEV_INDICATOR = {
1190
1225
  name: "StdDev",
1191
1226
  pane: "separate",
1192
1227
  paneHeightRatio: 0.16,
1193
- defaultInputs: { length: 20, source: "close", color: "#f97316", width: 2, showValueLine: false },
1228
+ defaultInputs: { length: 20, source: "close", color: "#f97316", width: 2, showValueLine: true },
1194
1229
  draw: (ctx, renderContext, inputs) => {
1195
1230
  const length = clampIndicatorLength(inputs.length, 20);
1196
1231
  const values = withCachedSeries(
@@ -1201,7 +1236,7 @@ var BUILTIN_STDDEV_INDICATOR = {
1201
1236
  return drawSeparateSeries(ctx, renderContext, values, inputs.color ?? "#f97316", Number(inputs.width) || 2, void 0, void 0, void 0, {
1202
1237
  title: `StdDev ${length}`,
1203
1238
  decimals: 2,
1204
- valueLine: inputs.showValueLine === true
1239
+ valueLine: inputs.showValueLine !== false
1205
1240
  });
1206
1241
  }
1207
1242
  };
@@ -1210,14 +1245,14 @@ var BUILTIN_ATR_INDICATOR = {
1210
1245
  name: "ATR",
1211
1246
  pane: "separate",
1212
1247
  paneHeightRatio: 0.16,
1213
- defaultInputs: { length: 14, color: "#eab308", width: 2, showValueLine: false },
1248
+ defaultInputs: { length: 14, color: "#eab308", width: 2, showValueLine: true },
1214
1249
  draw: (ctx, renderContext, inputs) => {
1215
1250
  const length = clampIndicatorLength(inputs.length, 14);
1216
1251
  const values = withCachedSeries(`atr|${length}`, renderContext.data, () => computeAtrSeries(renderContext.data, length));
1217
1252
  return drawSeparateSeries(ctx, renderContext, values, inputs.color ?? "#eab308", Number(inputs.width) || 2, void 0, void 0, void 0, {
1218
1253
  title: `ATR ${length}`,
1219
1254
  decimals: 2,
1220
- valueLine: inputs.showValueLine === true
1255
+ valueLine: inputs.showValueLine !== false
1221
1256
  });
1222
1257
  }
1223
1258
  };
@@ -1234,7 +1269,7 @@ var BUILTIN_RSI_INDICATOR = {
1234
1269
  showValueLabel: true,
1235
1270
  showGuideLines: true,
1236
1271
  showScaleLabels: true,
1237
- showValueLine: false
1272
+ showValueLine: true
1238
1273
  },
1239
1274
  draw: (ctx, renderContext, inputs) => {
1240
1275
  const length = clampIndicatorLength(inputs.length, 14);
@@ -1258,7 +1293,7 @@ var BUILTIN_RSI_INDICATOR = {
1258
1293
  valueLabelBackgroundColor: "#9E9E9E",
1259
1294
  valueLabelTextColor: "#0f172a",
1260
1295
  showLegend: inputs.showLegend !== false,
1261
- valueLine: inputs.showValueLine === true
1296
+ valueLine: inputs.showValueLine !== false
1262
1297
  }
1263
1298
  );
1264
1299
  }
@@ -1315,6 +1350,7 @@ var drawSeparateMultiSeries = (ctx, renderContext, seriesList, options = {}) =>
1315
1350
  const ratio = (value - minValue) / range;
1316
1351
  return renderContext.chartBottom - ratio * renderContext.chartHeight;
1317
1352
  };
1353
+ drawPaneHorizontalGrid(ctx, renderContext, minValue, maxValue);
1318
1354
  if (options.guideLines && options.guideLines.length > 0) {
1319
1355
  ctx.save();
1320
1356
  ctx.strokeStyle = "rgba(148,163,184,0.35)";
@@ -1766,7 +1802,7 @@ var BUILTIN_MACD_INDICATOR = {
1766
1802
  signalColor: "#ff6d00",
1767
1803
  histUpColor: "#26a69a",
1768
1804
  histDownColor: "#ef5350",
1769
- showValueLine: false
1805
+ showValueLine: true
1770
1806
  },
1771
1807
  draw: (ctx, renderContext, inputs) => {
1772
1808
  const fast = clampIndicatorLength(inputs.fast, 12);
@@ -1796,7 +1832,7 @@ var BUILTIN_MACD_INDICATOR = {
1796
1832
  guideLines: [0],
1797
1833
  decimals: 2,
1798
1834
  valueLabelSeriesIndex: 1,
1799
- valueLines: inputs.showValueLine === true
1835
+ valueLines: inputs.showValueLine !== false
1800
1836
  }
1801
1837
  );
1802
1838
  }
@@ -1806,7 +1842,7 @@ var BUILTIN_STOCHASTIC_INDICATOR = {
1806
1842
  name: "Stoch",
1807
1843
  pane: "separate",
1808
1844
  paneHeightRatio: 0.18,
1809
- defaultInputs: { kLength: 14, kSmoothing: 1, dLength: 3, kColor: "#2962ff", dColor: "#ff6d00", showValueLine: false },
1845
+ defaultInputs: { kLength: 14, kSmoothing: 1, dLength: 3, kColor: "#2962ff", dColor: "#ff6d00", showValueLine: true },
1810
1846
  draw: (ctx, renderContext, inputs) => {
1811
1847
  const kLength = clampIndicatorLength(inputs.kLength, 14);
1812
1848
  const kSmoothing = clampIndicatorLength(inputs.kSmoothing, 1);
@@ -1830,7 +1866,7 @@ var BUILTIN_STOCHASTIC_INDICATOR = {
1830
1866
  guideLines: [20, 80],
1831
1867
  axisTicks: [0, 20, 50, 80, 100],
1832
1868
  decimals: 2,
1833
- valueLines: inputs.showValueLine === true
1869
+ valueLines: inputs.showValueLine !== false
1834
1870
  }
1835
1871
  );
1836
1872
  }
@@ -1847,7 +1883,7 @@ var BUILTIN_STOCHRSI_INDICATOR = {
1847
1883
  dSmoothing: 3,
1848
1884
  kColor: "#2962ff",
1849
1885
  dColor: "#ff6d00",
1850
- showValueLine: false
1886
+ showValueLine: true
1851
1887
  },
1852
1888
  draw: (ctx, renderContext, inputs) => {
1853
1889
  const rsiLength = clampIndicatorLength(inputs.rsiLength, 14);
@@ -1873,7 +1909,7 @@ var BUILTIN_STOCHRSI_INDICATOR = {
1873
1909
  guideLines: [20, 80],
1874
1910
  axisTicks: [0, 20, 50, 80, 100],
1875
1911
  decimals: 2,
1876
- valueLines: inputs.showValueLine === true
1912
+ valueLines: inputs.showValueLine !== false
1877
1913
  }
1878
1914
  );
1879
1915
  }
@@ -1889,7 +1925,7 @@ var BUILTIN_ADX_INDICATOR = {
1889
1925
  plusDiColor: "#26a69a",
1890
1926
  minusDiColor: "#ef5350",
1891
1927
  showDi: true,
1892
- showValueLine: false
1928
+ showValueLine: true
1893
1929
  },
1894
1930
  draw: (ctx, renderContext, inputs) => {
1895
1931
  const length = clampIndicatorLength(inputs.length, 14);
@@ -1912,7 +1948,7 @@ var BUILTIN_ADX_INDICATOR = {
1912
1948
  minOverride: 0,
1913
1949
  guideLines: [20],
1914
1950
  decimals: 2,
1915
- valueLines: inputs.showValueLine === true
1951
+ valueLines: inputs.showValueLine !== false
1916
1952
  });
1917
1953
  }
1918
1954
  };
@@ -1921,14 +1957,14 @@ var BUILTIN_OBV_INDICATOR = {
1921
1957
  name: "OBV",
1922
1958
  pane: "separate",
1923
1959
  paneHeightRatio: 0.16,
1924
- defaultInputs: { color: "#2962ff", width: 2, showValueLine: false },
1960
+ defaultInputs: { color: "#2962ff", width: 2, showValueLine: true },
1925
1961
  draw: (ctx, renderContext, inputs) => {
1926
1962
  const values = withCachedSeries("obv", renderContext.data, () => computeObvSeries(renderContext.data));
1927
1963
  return drawSeparateMultiSeries(
1928
1964
  ctx,
1929
1965
  renderContext,
1930
1966
  [{ values, color: inputs.color ?? "#2962ff", width: Number(inputs.width) || 2, label: "OBV" }],
1931
- { title: "OBV", format: formatCompactNumber, valueLines: inputs.showValueLine === true }
1967
+ { title: "OBV", format: formatCompactNumber, valueLines: inputs.showValueLine !== false }
1932
1968
  );
1933
1969
  }
1934
1970
  };
@@ -1937,7 +1973,7 @@ var BUILTIN_MFI_INDICATOR = {
1937
1973
  name: "MFI",
1938
1974
  pane: "separate",
1939
1975
  paneHeightRatio: 0.16,
1940
- defaultInputs: { length: 14, color: "#7e57c2", width: 2, showValueLine: false },
1976
+ defaultInputs: { length: 14, color: "#7e57c2", width: 2, showValueLine: true },
1941
1977
  draw: (ctx, renderContext, inputs) => {
1942
1978
  const length = clampIndicatorLength(inputs.length, 14);
1943
1979
  const values = withCachedSeries(
@@ -1956,7 +1992,7 @@ var BUILTIN_MFI_INDICATOR = {
1956
1992
  guideLines: [20, 80],
1957
1993
  axisTicks: [0, 20, 50, 80, 100],
1958
1994
  decimals: 2,
1959
- valueLines: inputs.showValueLine === true
1995
+ valueLines: inputs.showValueLine !== false
1960
1996
  }
1961
1997
  );
1962
1998
  }
@@ -1966,7 +2002,7 @@ var BUILTIN_CCI_INDICATOR = {
1966
2002
  name: "CCI",
1967
2003
  pane: "separate",
1968
2004
  paneHeightRatio: 0.16,
1969
- defaultInputs: { length: 20, color: "#2962ff", width: 2, showValueLine: false },
2005
+ defaultInputs: { length: 20, color: "#2962ff", width: 2, showValueLine: true },
1970
2006
  draw: (ctx, renderContext, inputs) => {
1971
2007
  const length = clampIndicatorLength(inputs.length, 20);
1972
2008
  const values = withCachedSeries(
@@ -1978,7 +2014,7 @@ var BUILTIN_CCI_INDICATOR = {
1978
2014
  ctx,
1979
2015
  renderContext,
1980
2016
  [{ values, color: inputs.color ?? "#2962ff", width: Number(inputs.width) || 2 }],
1981
- { title: `CCI ${length}`, guideLines: [-100, 0, 100], decimals: 2, valueLines: inputs.showValueLine === true }
2017
+ { title: `CCI ${length}`, guideLines: [-100, 0, 100], decimals: 2, valueLines: inputs.showValueLine !== false }
1982
2018
  );
1983
2019
  }
1984
2020
  };
@@ -1987,7 +2023,7 @@ var BUILTIN_WILLIAMSR_INDICATOR = {
1987
2023
  name: "Williams %R",
1988
2024
  pane: "separate",
1989
2025
  paneHeightRatio: 0.16,
1990
- defaultInputs: { length: 14, color: "#7e57c2", width: 2, showValueLine: false },
2026
+ defaultInputs: { length: 14, color: "#7e57c2", width: 2, showValueLine: true },
1991
2027
  draw: (ctx, renderContext, inputs) => {
1992
2028
  const length = clampIndicatorLength(inputs.length, 14);
1993
2029
  const values = withCachedSeries(
@@ -2006,7 +2042,7 @@ var BUILTIN_WILLIAMSR_INDICATOR = {
2006
2042
  guideLines: [-80, -20],
2007
2043
  axisTicks: [-100, -80, -50, -20, 0],
2008
2044
  decimals: 2,
2009
- valueLines: inputs.showValueLine === true
2045
+ valueLines: inputs.showValueLine !== false
2010
2046
  }
2011
2047
  );
2012
2048
  }
@@ -2016,7 +2052,7 @@ var BUILTIN_ROC_INDICATOR = {
2016
2052
  name: "ROC",
2017
2053
  pane: "separate",
2018
2054
  paneHeightRatio: 0.16,
2019
- defaultInputs: { length: 9, color: "#2962ff", width: 2, showValueLine: false },
2055
+ defaultInputs: { length: 9, color: "#2962ff", width: 2, showValueLine: true },
2020
2056
  draw: (ctx, renderContext, inputs) => {
2021
2057
  const length = clampIndicatorLength(inputs.length, 9);
2022
2058
  const values = withCachedSeries(
@@ -2028,7 +2064,7 @@ var BUILTIN_ROC_INDICATOR = {
2028
2064
  ctx,
2029
2065
  renderContext,
2030
2066
  [{ values, color: inputs.color ?? "#2962ff", width: Number(inputs.width) || 2 }],
2031
- { title: `ROC ${length}`, includeZero: true, guideLines: [0], decimals: 2, valueLines: inputs.showValueLine === true }
2067
+ { title: `ROC ${length}`, includeZero: true, guideLines: [0], decimals: 2, valueLines: inputs.showValueLine !== false }
2032
2068
  );
2033
2069
  }
2034
2070
  };
@@ -2037,7 +2073,7 @@ var BUILTIN_MOMENTUM_INDICATOR = {
2037
2073
  name: "Momentum",
2038
2074
  pane: "separate",
2039
2075
  paneHeightRatio: 0.16,
2040
- defaultInputs: { length: 10, color: "#2962ff", width: 2, showValueLine: false },
2076
+ defaultInputs: { length: 10, color: "#2962ff", width: 2, showValueLine: true },
2041
2077
  draw: (ctx, renderContext, inputs) => {
2042
2078
  const length = clampIndicatorLength(inputs.length, 10);
2043
2079
  const values = withCachedSeries(
@@ -2049,7 +2085,7 @@ var BUILTIN_MOMENTUM_INDICATOR = {
2049
2085
  ctx,
2050
2086
  renderContext,
2051
2087
  [{ values, color: inputs.color ?? "#2962ff", width: Number(inputs.width) || 2 }],
2052
- { title: `Mom ${length}`, includeZero: true, guideLines: [0], decimals: 2, valueLines: inputs.showValueLine === true }
2088
+ { title: `Mom ${length}`, includeZero: true, guideLines: [0], decimals: 2, valueLines: inputs.showValueLine !== false }
2053
2089
  );
2054
2090
  }
2055
2091
  };
@@ -2473,6 +2509,14 @@ function createChart(element, options = {}) {
2473
2509
  let watermarkImageReady = false;
2474
2510
  let drawState = null;
2475
2511
  let plotBottomForHit = 0;
2512
+ let paneLayoutFullChartHeight = 0;
2513
+ let paneLayoutInfos = [];
2514
+ let paneButtonRegions = [];
2515
+ let hoveredPaneId = null;
2516
+ let hoveredPaneButton = null;
2517
+ let paneDividerDrag = null;
2518
+ let indicatorPaneActionHandler = null;
2519
+ let indicatorPaneHeightChangeHandler = null;
2476
2520
  let orderDragState = null;
2477
2521
  let actionDragState = null;
2478
2522
  let pointerDownInfo = null;
@@ -3574,20 +3618,24 @@ function createChart(element, options = {}) {
3574
3618
  const watermark = { ...DEFAULT_WATERMARK_OPTIONS, ...mergedOptions.watermark ?? {} };
3575
3619
  const paneGap = 0;
3576
3620
  const separatePaneSpacing = 6;
3577
- const activeSeparateIndicators = indicators.filter((indicator) => indicator.visible).map((indicator) => ({ indicator, plugin: indicatorRegistry.get(indicator.type) })).filter(
3621
+ const collapsedPaneHeight = 26;
3622
+ const activeSeparateIndicators = indicators.map((indicator) => ({ indicator, plugin: indicatorRegistry.get(indicator.type) })).filter(
3578
3623
  (value) => value.plugin !== void 0 && (value.indicator.pane ?? value.plugin.pane ?? "overlay") === "separate"
3579
3624
  ).sort((a, b) => a.indicator.zIndex - b.indicator.zIndex);
3580
3625
  const overlayIndicatorsForScale = indicators.filter((indicator) => indicator.visible).map((indicator) => ({ indicator, plugin: indicatorRegistry.get(indicator.type) })).filter(
3581
3626
  (value) => value.plugin !== void 0 && (value.indicator.pane ?? value.plugin.pane ?? "overlay") === "overlay"
3582
3627
  );
3583
3628
  const separatePaneHeightDefaults = activeSeparateIndicators.map(({ indicator, plugin }) => {
3629
+ if (!indicator.visible) return collapsedPaneHeight;
3584
3630
  const ratio = Math.min(0.45, Math.max(0.08, indicator.paneHeightRatio ?? plugin.paneHeightRatio ?? 0.22));
3585
3631
  return Math.round(fullChartHeight * ratio);
3586
3632
  });
3587
3633
  const separatePaneDesiredTotal = separatePaneHeightDefaults.reduce((sum, value) => sum + value, 0) + Math.max(0, activeSeparateIndicators.length - 1) * separatePaneSpacing;
3588
3634
  const maxSeparatePaneTotal = Math.max(0, fullChartHeight - 140);
3589
3635
  const separatePaneScale = separatePaneDesiredTotal > 0 && separatePaneDesiredTotal > maxSeparatePaneTotal ? maxSeparatePaneTotal / separatePaneDesiredTotal : 1;
3590
- const separatePaneHeights = separatePaneHeightDefaults.map((value) => Math.max(48, Math.round(value * separatePaneScale)));
3636
+ const separatePaneHeights = separatePaneHeightDefaults.map(
3637
+ (value, index) => activeSeparateIndicators[index].indicator.visible ? Math.max(48, Math.round(value * separatePaneScale)) : collapsedPaneHeight
3638
+ );
3591
3639
  const separatePaneTotal = separatePaneHeights.reduce((sum, value) => sum + value, 0) + Math.max(0, activeSeparateIndicators.length - 1) * separatePaneSpacing;
3592
3640
  const pricePaneGap = activeSeparateIndicators.length > 0 ? paneGap : 0;
3593
3641
  const chartHeight = Math.max(120, fullChartHeight - separatePaneTotal - pricePaneGap);
@@ -4838,43 +4886,138 @@ function createChart(element, options = {}) {
4838
4886
  ctx.restore();
4839
4887
  }
4840
4888
  }
4889
+ paneLayoutFullChartHeight = fullChartHeight;
4890
+ paneLayoutInfos = [];
4891
+ paneButtonRegions = [];
4841
4892
  if (activeSeparateIndicators.length > 0) {
4842
4893
  const xFromIndex = (index) => chartLeft + (index + 0.5 - xStart) / xSpan * chartWidth;
4843
4894
  let paneTopCursor = chartBottom + paneGap;
4895
+ const paneButtonSize = 16;
4896
+ const paneButtonGap = 4;
4897
+ const drawPaneButtons = (indicatorId, indicatorType, indicatorVisible, startX, centerY) => {
4898
+ const actionList = [
4899
+ "visibility",
4900
+ "settings",
4901
+ "source",
4902
+ "remove"
4903
+ ];
4904
+ let buttonX = startX;
4905
+ for (const action of actionList) {
4906
+ const buttonY = centerY - paneButtonSize / 2;
4907
+ const hovered = hoveredPaneButton?.id === indicatorId && hoveredPaneButton.action === action;
4908
+ const iconColor = labels.indicatorTextColor;
4909
+ ctx.save();
4910
+ ctx.fillStyle = hovered ? "rgba(148,163,184,0.30)" : "rgba(148,163,184,0.12)";
4911
+ fillRoundedRect(Math.round(buttonX), Math.round(buttonY), paneButtonSize, paneButtonSize, 4);
4912
+ ctx.strokeStyle = iconColor;
4913
+ ctx.fillStyle = iconColor;
4914
+ ctx.globalAlpha = hovered ? 1 : 0.75;
4915
+ ctx.lineWidth = 1.2;
4916
+ ctx.setLineDash([]);
4917
+ const cx = buttonX + paneButtonSize / 2;
4918
+ const cy = centerY;
4919
+ if (action === "visibility") {
4920
+ ctx.beginPath();
4921
+ ctx.ellipse(cx, cy, 4.6, 3, 0, 0, Math.PI * 2);
4922
+ ctx.stroke();
4923
+ ctx.beginPath();
4924
+ ctx.arc(cx, cy, 1.3, 0, Math.PI * 2);
4925
+ ctx.fill();
4926
+ if (!indicatorVisible) {
4927
+ ctx.beginPath();
4928
+ ctx.moveTo(cx - 5.2, cy + 4.6);
4929
+ ctx.lineTo(cx + 5.2, cy - 4.6);
4930
+ ctx.stroke();
4931
+ }
4932
+ } else if (action === "settings") {
4933
+ ctx.beginPath();
4934
+ ctx.arc(cx, cy, 3.4, 0, Math.PI * 2);
4935
+ ctx.stroke();
4936
+ for (let toothIndex = 0; toothIndex < 8; toothIndex += 1) {
4937
+ const angle = Math.PI / 4 * toothIndex;
4938
+ ctx.beginPath();
4939
+ ctx.moveTo(cx + Math.cos(angle) * 3.4, cy + Math.sin(angle) * 3.4);
4940
+ ctx.lineTo(cx + Math.cos(angle) * 5.2, cy + Math.sin(angle) * 5.2);
4941
+ ctx.stroke();
4942
+ }
4943
+ ctx.beginPath();
4944
+ ctx.arc(cx, cy, 1.1, 0, Math.PI * 2);
4945
+ ctx.fill();
4946
+ } else if (action === "source") {
4947
+ const prevSourceFont = ctx.font;
4948
+ ctx.font = `bold 9px ${mergedOptions.fontFamily}`;
4949
+ drawText("{}", cx, cy + 0.5, "center", "middle", iconColor);
4950
+ ctx.font = prevSourceFont;
4951
+ } else {
4952
+ ctx.beginPath();
4953
+ ctx.moveTo(cx - 3.4, cy - 3.4);
4954
+ ctx.lineTo(cx + 3.4, cy + 3.4);
4955
+ ctx.moveTo(cx + 3.4, cy - 3.4);
4956
+ ctx.lineTo(cx - 3.4, cy + 3.4);
4957
+ ctx.stroke();
4958
+ }
4959
+ ctx.restore();
4960
+ paneButtonRegions.push({
4961
+ id: indicatorId,
4962
+ type: indicatorType,
4963
+ action,
4964
+ x: buttonX,
4965
+ y: buttonY,
4966
+ width: paneButtonSize,
4967
+ height: paneButtonSize
4968
+ });
4969
+ buttonX += paneButtonSize + paneButtonGap;
4970
+ }
4971
+ };
4844
4972
  activeSeparateIndicators.forEach(({ indicator, plugin }, paneIndex) => {
4845
4973
  const paneHeight = separatePaneHeights[paneIndex] ?? 80;
4846
4974
  const paneTop = paneTopCursor;
4847
4975
  const paneBottom = paneTop + paneHeight;
4848
4976
  paneTopCursor = paneBottom + separatePaneSpacing;
4849
- ctx.save();
4850
- ctx.beginPath();
4851
- ctx.rect(chartLeft + 1, paneTop + 1, Math.max(0, chartWidth - 2), Math.max(0, paneHeight - 2));
4852
- ctx.clip();
4853
- const paneInfo = plugin.draw(
4854
- ctx,
4855
- {
4856
- data,
4857
- startIndex,
4858
- endIndex,
4859
- xStart,
4860
- xSpan,
4861
- chartLeft,
4862
- chartRight,
4863
- chartTop: paneTop,
4864
- chartBottom: paneBottom,
4865
- chartWidth,
4866
- chartHeight: paneHeight,
4867
- xFromIndex,
4868
- yFromPrice: null,
4869
- getCandleDirectionByIndex,
4870
- getVolumeByIndex,
4871
- candleSpacing,
4872
- upColor: mergedOptions.upColor,
4873
- downColor: mergedOptions.downColor
4874
- },
4875
- indicator.inputs
4876
- );
4877
- ctx.restore();
4977
+ paneLayoutInfos.push({
4978
+ id: indicator.id,
4979
+ type: indicator.type,
4980
+ top: paneTop,
4981
+ bottom: paneBottom,
4982
+ visible: indicator.visible
4983
+ });
4984
+ let paneInfo;
4985
+ if (indicator.visible) {
4986
+ ctx.save();
4987
+ ctx.beginPath();
4988
+ ctx.rect(chartLeft + 1, paneTop + 1, Math.max(0, chartWidth - 2), Math.max(0, paneHeight - 2));
4989
+ ctx.clip();
4990
+ paneInfo = plugin.draw(
4991
+ ctx,
4992
+ {
4993
+ data,
4994
+ startIndex,
4995
+ endIndex,
4996
+ xStart,
4997
+ xSpan,
4998
+ chartLeft,
4999
+ chartRight,
5000
+ chartTop: paneTop,
5001
+ chartBottom: paneBottom,
5002
+ chartWidth,
5003
+ chartHeight: paneHeight,
5004
+ xFromIndex,
5005
+ yFromPrice: null,
5006
+ getCandleDirectionByIndex,
5007
+ getVolumeByIndex,
5008
+ candleSpacing,
5009
+ upColor: mergedOptions.upColor,
5010
+ downColor: mergedOptions.downColor,
5011
+ grid: {
5012
+ color: gridColor,
5013
+ opacity: gridOpacity,
5014
+ horizontalLines: grid.horizontalLines !== false
5015
+ }
5016
+ },
5017
+ indicator.inputs
5018
+ ) ?? void 0;
5019
+ ctx.restore();
5020
+ }
4878
5021
  ctx.save();
4879
5022
  ctx.strokeStyle = axis.lineColor;
4880
5023
  ctx.lineWidth = Math.max(1, axis.lineWidth);
@@ -4883,6 +5026,27 @@ function createChart(element, options = {}) {
4883
5026
  ctx.lineTo(crisp(chartRight), crisp(paneTop));
4884
5027
  ctx.stroke();
4885
5028
  ctx.restore();
5029
+ if (!indicator.visible) {
5030
+ if (labels.visible) {
5031
+ const prevFont = ctx.font;
5032
+ ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
5033
+ const hiddenText = `${plugin.name} (hidden)`;
5034
+ const rowCenterY = (paneTop + paneBottom) / 2;
5035
+ ctx.save();
5036
+ ctx.globalAlpha = 0.55;
5037
+ drawText(hiddenText, chartLeft + 10, rowCenterY, "left", "middle", labels.indicatorTextColor);
5038
+ ctx.restore();
5039
+ drawPaneButtons(
5040
+ indicator.id,
5041
+ indicator.type,
5042
+ false,
5043
+ chartLeft + 10 + Math.ceil(measureTextWidth(hiddenText)) + 10,
5044
+ rowCenterY
5045
+ );
5046
+ ctx.font = prevFont;
5047
+ }
5048
+ return;
5049
+ }
4886
5050
  const axisInfo = paneInfo?.axis;
4887
5051
  if (axisInfo && Number.isFinite(axisInfo.min) && Number.isFinite(axisInfo.max) && axisInfo.max !== axisInfo.min) {
4888
5052
  const paneRange = axisInfo.max - axisInfo.min;
@@ -4912,17 +5076,28 @@ function createChart(element, options = {}) {
4912
5076
  ctx.restore();
4913
5077
  if (labels.visible) {
4914
5078
  const prevFont = ctx.font;
4915
- ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
4916
- const legendTitle = paneInfo.title ?? plugin.name;
4917
- const legendValues = paneInfo.legendValues ?? [];
5079
+ const legendFontSize = Math.max(8, axis.fontSize);
5080
+ ctx.font = `${legendFontSize}px ${mergedOptions.fontFamily}`;
5081
+ const legendTitle = paneInfo?.title ?? plugin.name;
5082
+ const legendValues = paneInfo?.legendValues ?? [];
4918
5083
  const legendParts = [
4919
5084
  legendTitle,
4920
5085
  ...legendValues.map((value) => value.text ?? (value.value === void 0 ? "" : formatPaneValue(value.value))).filter(Boolean)
4921
5086
  ].filter(Boolean);
5087
+ const legendText = legendParts.join(" ");
4922
5088
  if (legendParts.length > 0) {
4923
- drawText(legendParts.join(" "), chartLeft + 10, paneTop + 8, "left", "top", labels.indicatorTextColor);
5089
+ drawText(legendText, chartLeft + 10, paneTop + 8, "left", "top", labels.indicatorTextColor);
4924
5090
  }
4925
- for (const label of paneInfo.valueLabels ?? []) {
5091
+ if (hoveredPaneId === indicator.id) {
5092
+ drawPaneButtons(
5093
+ indicator.id,
5094
+ indicator.type,
5095
+ true,
5096
+ chartLeft + 10 + Math.ceil(measureTextWidth(legendText)) + 10,
5097
+ paneTop + 8 + legendFontSize / 2
5098
+ );
5099
+ }
5100
+ for (const label of paneInfo?.valueLabels ?? []) {
4926
5101
  if (!labels.showIndicatorValueLabels) {
4927
5102
  continue;
4928
5103
  }
@@ -6134,6 +6309,54 @@ function createChart(element, options = {}) {
6134
6309
  }
6135
6310
  return "outside";
6136
6311
  };
6312
+ const getPaneButtonHit = (x, y) => {
6313
+ for (const region of paneButtonRegions) {
6314
+ if (x >= region.x && x <= region.x + region.width && y >= region.y && y <= region.y + region.height) {
6315
+ return { id: region.id, type: region.type, action: region.action };
6316
+ }
6317
+ }
6318
+ return null;
6319
+ };
6320
+ const getPaneAt = (x, y) => {
6321
+ if (!drawState || x < drawState.chartLeft || x > drawState.chartRight) {
6322
+ return null;
6323
+ }
6324
+ for (const pane of paneLayoutInfos) {
6325
+ if (y >= pane.top && y <= pane.bottom) {
6326
+ return pane;
6327
+ }
6328
+ }
6329
+ return null;
6330
+ };
6331
+ const getPaneDividerHit = (x, y) => {
6332
+ if (!drawState || x < drawState.chartLeft || x > drawState.chartRight) {
6333
+ return null;
6334
+ }
6335
+ for (const pane of paneLayoutInfos) {
6336
+ if (pane.visible && Math.abs(y - pane.top) <= 4) {
6337
+ return { id: pane.id, heightPx: pane.bottom - pane.top };
6338
+ }
6339
+ }
6340
+ return null;
6341
+ };
6342
+ const handlePaneButtonAction = (hit) => {
6343
+ const indicator = indicators.find((entry) => entry.id === hit.id);
6344
+ if (!indicator) {
6345
+ return;
6346
+ }
6347
+ if (hit.action === "visibility") {
6348
+ indicator.visible = !indicator.visible;
6349
+ scheduleDraw();
6350
+ indicatorPaneActionHandler?.({ id: hit.id, type: hit.type, action: "visibility", visible: indicator.visible });
6351
+ return;
6352
+ }
6353
+ if (hit.action === "remove" && !indicatorPaneActionHandler) {
6354
+ indicators = indicators.filter((entry) => entry.id !== hit.id);
6355
+ scheduleDraw();
6356
+ return;
6357
+ }
6358
+ indicatorPaneActionHandler?.({ id: hit.id, type: hit.type, action: hit.action });
6359
+ };
6137
6360
  const handleDrawingToolPointerDown = (x, y) => {
6138
6361
  if (!activeDrawingTool || !drawState) {
6139
6362
  return false;
@@ -6613,6 +6836,26 @@ function createChart(element, options = {}) {
6613
6836
  setCrosshairPoint(null);
6614
6837
  return;
6615
6838
  }
6839
+ const paneButtonHit = getPaneButtonHit(point.x, point.y);
6840
+ if (paneButtonHit) {
6841
+ setCrosshairPoint(null);
6842
+ handlePaneButtonAction(paneButtonHit);
6843
+ return;
6844
+ }
6845
+ const paneDividerHit = getPaneDividerHit(point.x, point.y);
6846
+ if (paneDividerHit) {
6847
+ activePointerId = event.pointerId;
6848
+ paneDividerDrag = {
6849
+ id: paneDividerHit.id,
6850
+ startY: point.y,
6851
+ startHeightPx: paneDividerHit.heightPx,
6852
+ lastRatio: paneLayoutFullChartHeight > 0 ? paneDividerHit.heightPx / paneLayoutFullChartHeight : 0.2
6853
+ };
6854
+ capturePointer(event.pointerId);
6855
+ canvas.style.cursor = "row-resize";
6856
+ setCrosshairPoint(null);
6857
+ return;
6858
+ }
6616
6859
  }
6617
6860
  const region = getHitRegion(point.x, point.y);
6618
6861
  if (region === "outside") {
@@ -6737,6 +6980,32 @@ function createChart(element, options = {}) {
6737
6980
  return;
6738
6981
  }
6739
6982
  }
6983
+ if (paneDividerDrag) {
6984
+ if (activePointerId !== null && event.pointerId !== activePointerId) {
6985
+ return;
6986
+ }
6987
+ const fullHeight = paneLayoutFullChartHeight;
6988
+ if (fullHeight > 0) {
6989
+ const nextHeightPx = clamp(
6990
+ paneDividerDrag.startHeightPx + (paneDividerDrag.startY - point.y),
6991
+ 48,
6992
+ Math.round(fullHeight * 0.45)
6993
+ );
6994
+ const nextRatio = nextHeightPx / fullHeight;
6995
+ if (Math.abs(nextRatio - paneDividerDrag.lastRatio) > 5e-4) {
6996
+ paneDividerDrag.lastRatio = nextRatio;
6997
+ const targetIndicator = indicators.find((entry) => entry.id === paneDividerDrag?.id);
6998
+ if (targetIndicator) {
6999
+ targetIndicator.paneHeightRatio = nextRatio;
7000
+ indicatorPaneHeightChangeHandler?.({ id: paneDividerDrag.id, paneHeightRatio: nextRatio, dragging: true });
7001
+ scheduleDraw();
7002
+ }
7003
+ }
7004
+ }
7005
+ canvas.style.cursor = "row-resize";
7006
+ setCrosshairPoint(null);
7007
+ return;
7008
+ }
6740
7009
  if (orderDragState) {
6741
7010
  if (activePointerId !== null && event.pointerId !== activePointerId) {
6742
7011
  return;
@@ -6809,6 +7078,33 @@ function createChart(element, options = {}) {
6809
7078
  setCrosshairPoint(null);
6810
7079
  return;
6811
7080
  }
7081
+ const hoveredPane = getPaneAt(point.x, point.y);
7082
+ const nextHoveredPaneId = hoveredPane?.id ?? null;
7083
+ if (nextHoveredPaneId !== hoveredPaneId) {
7084
+ hoveredPaneId = nextHoveredPaneId;
7085
+ scheduleDraw();
7086
+ }
7087
+ const paneButtonHover = getPaneButtonHit(point.x, point.y);
7088
+ const nextHoveredButton = paneButtonHover ? { id: paneButtonHover.id, action: paneButtonHover.action } : null;
7089
+ if ((nextHoveredButton?.id ?? null) !== (hoveredPaneButton?.id ?? null) || nextHoveredButton?.action !== hoveredPaneButton?.action) {
7090
+ hoveredPaneButton = nextHoveredButton;
7091
+ scheduleDraw();
7092
+ }
7093
+ if (paneButtonHover) {
7094
+ const indicatorForTitle = indicators.find((entry) => entry.id === paneButtonHover.id);
7095
+ canvas.title = paneButtonHover.action === "visibility" ? indicatorForTitle?.visible === false ? "Show" : "Hide" : paneButtonHover.action === "settings" ? "Settings" : paneButtonHover.action === "source" ? "View source" : "Remove";
7096
+ canvas.style.cursor = "pointer";
7097
+ setCrosshairPoint(null);
7098
+ return;
7099
+ }
7100
+ if (canvas.title) {
7101
+ canvas.title = "";
7102
+ }
7103
+ if (getPaneDividerHit(point.x, point.y)) {
7104
+ canvas.style.cursor = "row-resize";
7105
+ setCrosshairPoint(null);
7106
+ return;
7107
+ }
6812
7108
  }
6813
7109
  if (!activeDrawingTool && getDrawingHit(point.x, point.y)) {
6814
7110
  const drawingHit = getDrawingHit(point.x, point.y);
@@ -6928,6 +7224,22 @@ function createChart(element, options = {}) {
6928
7224
  drawingDragState = null;
6929
7225
  emitDrawingsChange();
6930
7226
  }
7227
+ if (paneDividerDrag) {
7228
+ indicatorPaneHeightChangeHandler?.({
7229
+ id: paneDividerDrag.id,
7230
+ paneHeightRatio: paneDividerDrag.lastRatio,
7231
+ dragging: false
7232
+ });
7233
+ paneDividerDrag = null;
7234
+ }
7235
+ if (event?.type === "pointerleave" && (hoveredPaneId !== null || hoveredPaneButton !== null)) {
7236
+ hoveredPaneId = null;
7237
+ hoveredPaneButton = null;
7238
+ if (canvas.title) {
7239
+ canvas.title = "";
7240
+ }
7241
+ scheduleDraw();
7242
+ }
6931
7243
  const endedPlotDrag = isDragging && dragMode === "plot";
6932
7244
  isDragging = false;
6933
7245
  dragMode = null;
@@ -7273,6 +7585,12 @@ function createChart(element, options = {}) {
7273
7585
  const onCrosshairPriceAction = (handler) => {
7274
7586
  crosshairPriceActionHandler = handler;
7275
7587
  };
7588
+ const onIndicatorPaneAction = (handler) => {
7589
+ indicatorPaneActionHandler = handler;
7590
+ };
7591
+ const onIndicatorPaneHeightChange = (handler) => {
7592
+ indicatorPaneHeightChangeHandler = handler;
7593
+ };
7276
7594
  const setDoubleClickEnabled = (enabled) => {
7277
7595
  doubleClickEnabled = enabled;
7278
7596
  };
@@ -7478,6 +7796,8 @@ function createChart(element, options = {}) {
7478
7796
  onChartClick,
7479
7797
  onCrosshairMove,
7480
7798
  onCrosshairPriceAction,
7799
+ onIndicatorPaneAction,
7800
+ onIndicatorPaneHeightChange,
7481
7801
  zoomInX,
7482
7802
  zoomOutX,
7483
7803
  zoomInY,