hyperprop-charting-library 0.1.128 → 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;
@@ -850,6 +885,19 @@ var drawSeparateSeries = (ctx, renderContext, values, color, width, minOverride,
850
885
  break;
851
886
  }
852
887
  }
888
+ if (options.valueLine && latestValue !== null && latestValue >= minValue && latestValue <= maxValue) {
889
+ ctx.save();
890
+ ctx.lineWidth = 1;
891
+ ctx.setLineDash([3, 3]);
892
+ ctx.globalAlpha = 0.65;
893
+ ctx.strokeStyle = color;
894
+ const y = yFromValue(latestValue);
895
+ ctx.beginPath();
896
+ ctx.moveTo(renderContext.chartLeft, y);
897
+ ctx.lineTo(renderContext.chartRight, y);
898
+ ctx.stroke();
899
+ ctx.restore();
900
+ }
853
901
  const decimals = options.decimals ?? 2;
854
902
  const formatValue = (value) => value.toFixed(decimals);
855
903
  const axisTicks = options.axisTicks ?? guideLines;
@@ -1177,7 +1225,7 @@ var BUILTIN_STDDEV_INDICATOR = {
1177
1225
  name: "StdDev",
1178
1226
  pane: "separate",
1179
1227
  paneHeightRatio: 0.16,
1180
- defaultInputs: { length: 20, source: "close", color: "#f97316", width: 2 },
1228
+ defaultInputs: { length: 20, source: "close", color: "#f97316", width: 2, showValueLine: true },
1181
1229
  draw: (ctx, renderContext, inputs) => {
1182
1230
  const length = clampIndicatorLength(inputs.length, 20);
1183
1231
  const values = withCachedSeries(
@@ -1187,7 +1235,8 @@ var BUILTIN_STDDEV_INDICATOR = {
1187
1235
  );
1188
1236
  return drawSeparateSeries(ctx, renderContext, values, inputs.color ?? "#f97316", Number(inputs.width) || 2, void 0, void 0, void 0, {
1189
1237
  title: `StdDev ${length}`,
1190
- decimals: 2
1238
+ decimals: 2,
1239
+ valueLine: inputs.showValueLine !== false
1191
1240
  });
1192
1241
  }
1193
1242
  };
@@ -1196,13 +1245,14 @@ var BUILTIN_ATR_INDICATOR = {
1196
1245
  name: "ATR",
1197
1246
  pane: "separate",
1198
1247
  paneHeightRatio: 0.16,
1199
- defaultInputs: { length: 14, color: "#eab308", width: 2 },
1248
+ defaultInputs: { length: 14, color: "#eab308", width: 2, showValueLine: true },
1200
1249
  draw: (ctx, renderContext, inputs) => {
1201
1250
  const length = clampIndicatorLength(inputs.length, 14);
1202
1251
  const values = withCachedSeries(`atr|${length}`, renderContext.data, () => computeAtrSeries(renderContext.data, length));
1203
1252
  return drawSeparateSeries(ctx, renderContext, values, inputs.color ?? "#eab308", Number(inputs.width) || 2, void 0, void 0, void 0, {
1204
1253
  title: `ATR ${length}`,
1205
- decimals: 2
1254
+ decimals: 2,
1255
+ valueLine: inputs.showValueLine !== false
1206
1256
  });
1207
1257
  }
1208
1258
  };
@@ -1218,7 +1268,8 @@ var BUILTIN_RSI_INDICATOR = {
1218
1268
  showLegend: true,
1219
1269
  showValueLabel: true,
1220
1270
  showGuideLines: true,
1221
- showScaleLabels: true
1271
+ showScaleLabels: true,
1272
+ showValueLine: true
1222
1273
  },
1223
1274
  draw: (ctx, renderContext, inputs) => {
1224
1275
  const length = clampIndicatorLength(inputs.length, 14);
@@ -1241,7 +1292,8 @@ var BUILTIN_RSI_INDICATOR = {
1241
1292
  valueLabelColor: "#9E9E9E",
1242
1293
  valueLabelBackgroundColor: "#9E9E9E",
1243
1294
  valueLabelTextColor: "#0f172a",
1244
- showLegend: inputs.showLegend !== false
1295
+ showLegend: inputs.showLegend !== false,
1296
+ valueLine: inputs.showValueLine !== false
1245
1297
  }
1246
1298
  );
1247
1299
  }
@@ -1298,6 +1350,7 @@ var drawSeparateMultiSeries = (ctx, renderContext, seriesList, options = {}) =>
1298
1350
  const ratio = (value - minValue) / range;
1299
1351
  return renderContext.chartBottom - ratio * renderContext.chartHeight;
1300
1352
  };
1353
+ drawPaneHorizontalGrid(ctx, renderContext, minValue, maxValue);
1301
1354
  if (options.guideLines && options.guideLines.length > 0) {
1302
1355
  ctx.save();
1303
1356
  ctx.strokeStyle = "rgba(148,163,184,0.35)";
@@ -1377,6 +1430,24 @@ var drawSeparateMultiSeries = (ctx, renderContext, seriesList, options = {}) =>
1377
1430
  }
1378
1431
  return null;
1379
1432
  };
1433
+ if (options.valueLines) {
1434
+ ctx.save();
1435
+ ctx.lineWidth = 1;
1436
+ ctx.setLineDash([3, 3]);
1437
+ ctx.globalAlpha = 0.65;
1438
+ for (const spec of seriesList) {
1439
+ if (spec.histogram) continue;
1440
+ const latest = latestOf(spec.values);
1441
+ if (latest === null || latest < minValue || latest > maxValue) continue;
1442
+ const y = yFromValue(latest);
1443
+ ctx.strokeStyle = spec.color;
1444
+ ctx.beginPath();
1445
+ ctx.moveTo(renderContext.chartLeft, y);
1446
+ ctx.lineTo(renderContext.chartRight, y);
1447
+ ctx.stroke();
1448
+ }
1449
+ ctx.restore();
1450
+ }
1380
1451
  const paneInfo = {
1381
1452
  ...options.title ? { title: options.title } : {},
1382
1453
  axis: {
@@ -1730,7 +1801,8 @@ var BUILTIN_MACD_INDICATOR = {
1730
1801
  macdColor: "#2962ff",
1731
1802
  signalColor: "#ff6d00",
1732
1803
  histUpColor: "#26a69a",
1733
- histDownColor: "#ef5350"
1804
+ histDownColor: "#ef5350",
1805
+ showValueLine: true
1734
1806
  },
1735
1807
  draw: (ctx, renderContext, inputs) => {
1736
1808
  const fast = clampIndicatorLength(inputs.fast, 12);
@@ -1759,7 +1831,8 @@ var BUILTIN_MACD_INDICATOR = {
1759
1831
  includeZero: true,
1760
1832
  guideLines: [0],
1761
1833
  decimals: 2,
1762
- valueLabelSeriesIndex: 1
1834
+ valueLabelSeriesIndex: 1,
1835
+ valueLines: inputs.showValueLine !== false
1763
1836
  }
1764
1837
  );
1765
1838
  }
@@ -1769,7 +1842,7 @@ var BUILTIN_STOCHASTIC_INDICATOR = {
1769
1842
  name: "Stoch",
1770
1843
  pane: "separate",
1771
1844
  paneHeightRatio: 0.18,
1772
- defaultInputs: { kLength: 14, kSmoothing: 1, dLength: 3, kColor: "#2962ff", dColor: "#ff6d00" },
1845
+ defaultInputs: { kLength: 14, kSmoothing: 1, dLength: 3, kColor: "#2962ff", dColor: "#ff6d00", showValueLine: true },
1773
1846
  draw: (ctx, renderContext, inputs) => {
1774
1847
  const kLength = clampIndicatorLength(inputs.kLength, 14);
1775
1848
  const kSmoothing = clampIndicatorLength(inputs.kSmoothing, 1);
@@ -1792,7 +1865,8 @@ var BUILTIN_STOCHASTIC_INDICATOR = {
1792
1865
  maxOverride: 100,
1793
1866
  guideLines: [20, 80],
1794
1867
  axisTicks: [0, 20, 50, 80, 100],
1795
- decimals: 2
1868
+ decimals: 2,
1869
+ valueLines: inputs.showValueLine !== false
1796
1870
  }
1797
1871
  );
1798
1872
  }
@@ -1802,7 +1876,15 @@ var BUILTIN_STOCHRSI_INDICATOR = {
1802
1876
  name: "Stoch RSI",
1803
1877
  pane: "separate",
1804
1878
  paneHeightRatio: 0.18,
1805
- defaultInputs: { rsiLength: 14, stochLength: 14, kSmoothing: 3, dSmoothing: 3, kColor: "#2962ff", dColor: "#ff6d00" },
1879
+ defaultInputs: {
1880
+ rsiLength: 14,
1881
+ stochLength: 14,
1882
+ kSmoothing: 3,
1883
+ dSmoothing: 3,
1884
+ kColor: "#2962ff",
1885
+ dColor: "#ff6d00",
1886
+ showValueLine: true
1887
+ },
1806
1888
  draw: (ctx, renderContext, inputs) => {
1807
1889
  const rsiLength = clampIndicatorLength(inputs.rsiLength, 14);
1808
1890
  const stochLength = clampIndicatorLength(inputs.stochLength, 14);
@@ -1826,7 +1908,8 @@ var BUILTIN_STOCHRSI_INDICATOR = {
1826
1908
  maxOverride: 100,
1827
1909
  guideLines: [20, 80],
1828
1910
  axisTicks: [0, 20, 50, 80, 100],
1829
- decimals: 2
1911
+ decimals: 2,
1912
+ valueLines: inputs.showValueLine !== false
1830
1913
  }
1831
1914
  );
1832
1915
  }
@@ -1836,7 +1919,14 @@ var BUILTIN_ADX_INDICATOR = {
1836
1919
  name: "ADX/DMI",
1837
1920
  pane: "separate",
1838
1921
  paneHeightRatio: 0.18,
1839
- defaultInputs: { length: 14, adxColor: "#ff6d00", plusDiColor: "#26a69a", minusDiColor: "#ef5350", showDi: true },
1922
+ defaultInputs: {
1923
+ length: 14,
1924
+ adxColor: "#ff6d00",
1925
+ plusDiColor: "#26a69a",
1926
+ minusDiColor: "#ef5350",
1927
+ showDi: true,
1928
+ showValueLine: true
1929
+ },
1840
1930
  draw: (ctx, renderContext, inputs) => {
1841
1931
  const length = clampIndicatorLength(inputs.length, 14);
1842
1932
  const { adx, plusDi, minusDi } = withCachedComputation(
@@ -1857,7 +1947,8 @@ var BUILTIN_ADX_INDICATOR = {
1857
1947
  title: `ADX ${length}`,
1858
1948
  minOverride: 0,
1859
1949
  guideLines: [20],
1860
- decimals: 2
1950
+ decimals: 2,
1951
+ valueLines: inputs.showValueLine !== false
1861
1952
  });
1862
1953
  }
1863
1954
  };
@@ -1866,14 +1957,14 @@ var BUILTIN_OBV_INDICATOR = {
1866
1957
  name: "OBV",
1867
1958
  pane: "separate",
1868
1959
  paneHeightRatio: 0.16,
1869
- defaultInputs: { color: "#2962ff", width: 2 },
1960
+ defaultInputs: { color: "#2962ff", width: 2, showValueLine: true },
1870
1961
  draw: (ctx, renderContext, inputs) => {
1871
1962
  const values = withCachedSeries("obv", renderContext.data, () => computeObvSeries(renderContext.data));
1872
1963
  return drawSeparateMultiSeries(
1873
1964
  ctx,
1874
1965
  renderContext,
1875
1966
  [{ values, color: inputs.color ?? "#2962ff", width: Number(inputs.width) || 2, label: "OBV" }],
1876
- { title: "OBV", format: formatCompactNumber }
1967
+ { title: "OBV", format: formatCompactNumber, valueLines: inputs.showValueLine !== false }
1877
1968
  );
1878
1969
  }
1879
1970
  };
@@ -1882,7 +1973,7 @@ var BUILTIN_MFI_INDICATOR = {
1882
1973
  name: "MFI",
1883
1974
  pane: "separate",
1884
1975
  paneHeightRatio: 0.16,
1885
- defaultInputs: { length: 14, color: "#7e57c2", width: 2 },
1976
+ defaultInputs: { length: 14, color: "#7e57c2", width: 2, showValueLine: true },
1886
1977
  draw: (ctx, renderContext, inputs) => {
1887
1978
  const length = clampIndicatorLength(inputs.length, 14);
1888
1979
  const values = withCachedSeries(
@@ -1900,7 +1991,8 @@ var BUILTIN_MFI_INDICATOR = {
1900
1991
  maxOverride: 100,
1901
1992
  guideLines: [20, 80],
1902
1993
  axisTicks: [0, 20, 50, 80, 100],
1903
- decimals: 2
1994
+ decimals: 2,
1995
+ valueLines: inputs.showValueLine !== false
1904
1996
  }
1905
1997
  );
1906
1998
  }
@@ -1910,7 +2002,7 @@ var BUILTIN_CCI_INDICATOR = {
1910
2002
  name: "CCI",
1911
2003
  pane: "separate",
1912
2004
  paneHeightRatio: 0.16,
1913
- defaultInputs: { length: 20, color: "#2962ff", width: 2 },
2005
+ defaultInputs: { length: 20, color: "#2962ff", width: 2, showValueLine: true },
1914
2006
  draw: (ctx, renderContext, inputs) => {
1915
2007
  const length = clampIndicatorLength(inputs.length, 20);
1916
2008
  const values = withCachedSeries(
@@ -1922,7 +2014,7 @@ var BUILTIN_CCI_INDICATOR = {
1922
2014
  ctx,
1923
2015
  renderContext,
1924
2016
  [{ values, color: inputs.color ?? "#2962ff", width: Number(inputs.width) || 2 }],
1925
- { title: `CCI ${length}`, guideLines: [-100, 0, 100], decimals: 2 }
2017
+ { title: `CCI ${length}`, guideLines: [-100, 0, 100], decimals: 2, valueLines: inputs.showValueLine !== false }
1926
2018
  );
1927
2019
  }
1928
2020
  };
@@ -1931,7 +2023,7 @@ var BUILTIN_WILLIAMSR_INDICATOR = {
1931
2023
  name: "Williams %R",
1932
2024
  pane: "separate",
1933
2025
  paneHeightRatio: 0.16,
1934
- defaultInputs: { length: 14, color: "#7e57c2", width: 2 },
2026
+ defaultInputs: { length: 14, color: "#7e57c2", width: 2, showValueLine: true },
1935
2027
  draw: (ctx, renderContext, inputs) => {
1936
2028
  const length = clampIndicatorLength(inputs.length, 14);
1937
2029
  const values = withCachedSeries(
@@ -1949,7 +2041,8 @@ var BUILTIN_WILLIAMSR_INDICATOR = {
1949
2041
  maxOverride: 0,
1950
2042
  guideLines: [-80, -20],
1951
2043
  axisTicks: [-100, -80, -50, -20, 0],
1952
- decimals: 2
2044
+ decimals: 2,
2045
+ valueLines: inputs.showValueLine !== false
1953
2046
  }
1954
2047
  );
1955
2048
  }
@@ -1959,7 +2052,7 @@ var BUILTIN_ROC_INDICATOR = {
1959
2052
  name: "ROC",
1960
2053
  pane: "separate",
1961
2054
  paneHeightRatio: 0.16,
1962
- defaultInputs: { length: 9, color: "#2962ff", width: 2 },
2055
+ defaultInputs: { length: 9, color: "#2962ff", width: 2, showValueLine: true },
1963
2056
  draw: (ctx, renderContext, inputs) => {
1964
2057
  const length = clampIndicatorLength(inputs.length, 9);
1965
2058
  const values = withCachedSeries(
@@ -1971,7 +2064,7 @@ var BUILTIN_ROC_INDICATOR = {
1971
2064
  ctx,
1972
2065
  renderContext,
1973
2066
  [{ values, color: inputs.color ?? "#2962ff", width: Number(inputs.width) || 2 }],
1974
- { title: `ROC ${length}`, includeZero: true, guideLines: [0], decimals: 2 }
2067
+ { title: `ROC ${length}`, includeZero: true, guideLines: [0], decimals: 2, valueLines: inputs.showValueLine !== false }
1975
2068
  );
1976
2069
  }
1977
2070
  };
@@ -1980,7 +2073,7 @@ var BUILTIN_MOMENTUM_INDICATOR = {
1980
2073
  name: "Momentum",
1981
2074
  pane: "separate",
1982
2075
  paneHeightRatio: 0.16,
1983
- defaultInputs: { length: 10, color: "#2962ff", width: 2 },
2076
+ defaultInputs: { length: 10, color: "#2962ff", width: 2, showValueLine: true },
1984
2077
  draw: (ctx, renderContext, inputs) => {
1985
2078
  const length = clampIndicatorLength(inputs.length, 10);
1986
2079
  const values = withCachedSeries(
@@ -1992,7 +2085,7 @@ var BUILTIN_MOMENTUM_INDICATOR = {
1992
2085
  ctx,
1993
2086
  renderContext,
1994
2087
  [{ values, color: inputs.color ?? "#2962ff", width: Number(inputs.width) || 2 }],
1995
- { title: `Mom ${length}`, includeZero: true, guideLines: [0], decimals: 2 }
2088
+ { title: `Mom ${length}`, includeZero: true, guideLines: [0], decimals: 2, valueLines: inputs.showValueLine !== false }
1996
2089
  );
1997
2090
  }
1998
2091
  };
@@ -2416,6 +2509,14 @@ function createChart(element, options = {}) {
2416
2509
  let watermarkImageReady = false;
2417
2510
  let drawState = null;
2418
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;
2419
2520
  let orderDragState = null;
2420
2521
  let actionDragState = null;
2421
2522
  let pointerDownInfo = null;
@@ -3517,20 +3618,24 @@ function createChart(element, options = {}) {
3517
3618
  const watermark = { ...DEFAULT_WATERMARK_OPTIONS, ...mergedOptions.watermark ?? {} };
3518
3619
  const paneGap = 0;
3519
3620
  const separatePaneSpacing = 6;
3520
- 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(
3521
3623
  (value) => value.plugin !== void 0 && (value.indicator.pane ?? value.plugin.pane ?? "overlay") === "separate"
3522
3624
  ).sort((a, b) => a.indicator.zIndex - b.indicator.zIndex);
3523
3625
  const overlayIndicatorsForScale = indicators.filter((indicator) => indicator.visible).map((indicator) => ({ indicator, plugin: indicatorRegistry.get(indicator.type) })).filter(
3524
3626
  (value) => value.plugin !== void 0 && (value.indicator.pane ?? value.plugin.pane ?? "overlay") === "overlay"
3525
3627
  );
3526
3628
  const separatePaneHeightDefaults = activeSeparateIndicators.map(({ indicator, plugin }) => {
3629
+ if (!indicator.visible) return collapsedPaneHeight;
3527
3630
  const ratio = Math.min(0.45, Math.max(0.08, indicator.paneHeightRatio ?? plugin.paneHeightRatio ?? 0.22));
3528
3631
  return Math.round(fullChartHeight * ratio);
3529
3632
  });
3530
3633
  const separatePaneDesiredTotal = separatePaneHeightDefaults.reduce((sum, value) => sum + value, 0) + Math.max(0, activeSeparateIndicators.length - 1) * separatePaneSpacing;
3531
3634
  const maxSeparatePaneTotal = Math.max(0, fullChartHeight - 140);
3532
3635
  const separatePaneScale = separatePaneDesiredTotal > 0 && separatePaneDesiredTotal > maxSeparatePaneTotal ? maxSeparatePaneTotal / separatePaneDesiredTotal : 1;
3533
- 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
+ );
3534
3639
  const separatePaneTotal = separatePaneHeights.reduce((sum, value) => sum + value, 0) + Math.max(0, activeSeparateIndicators.length - 1) * separatePaneSpacing;
3535
3640
  const pricePaneGap = activeSeparateIndicators.length > 0 ? paneGap : 0;
3536
3641
  const chartHeight = Math.max(120, fullChartHeight - separatePaneTotal - pricePaneGap);
@@ -4781,43 +4886,138 @@ function createChart(element, options = {}) {
4781
4886
  ctx.restore();
4782
4887
  }
4783
4888
  }
4889
+ paneLayoutFullChartHeight = fullChartHeight;
4890
+ paneLayoutInfos = [];
4891
+ paneButtonRegions = [];
4784
4892
  if (activeSeparateIndicators.length > 0) {
4785
4893
  const xFromIndex = (index) => chartLeft + (index + 0.5 - xStart) / xSpan * chartWidth;
4786
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
+ };
4787
4972
  activeSeparateIndicators.forEach(({ indicator, plugin }, paneIndex) => {
4788
4973
  const paneHeight = separatePaneHeights[paneIndex] ?? 80;
4789
4974
  const paneTop = paneTopCursor;
4790
4975
  const paneBottom = paneTop + paneHeight;
4791
4976
  paneTopCursor = paneBottom + separatePaneSpacing;
4792
- ctx.save();
4793
- ctx.beginPath();
4794
- ctx.rect(chartLeft + 1, paneTop + 1, Math.max(0, chartWidth - 2), Math.max(0, paneHeight - 2));
4795
- ctx.clip();
4796
- const paneInfo = plugin.draw(
4797
- ctx,
4798
- {
4799
- data,
4800
- startIndex,
4801
- endIndex,
4802
- xStart,
4803
- xSpan,
4804
- chartLeft,
4805
- chartRight,
4806
- chartTop: paneTop,
4807
- chartBottom: paneBottom,
4808
- chartWidth,
4809
- chartHeight: paneHeight,
4810
- xFromIndex,
4811
- yFromPrice: null,
4812
- getCandleDirectionByIndex,
4813
- getVolumeByIndex,
4814
- candleSpacing,
4815
- upColor: mergedOptions.upColor,
4816
- downColor: mergedOptions.downColor
4817
- },
4818
- indicator.inputs
4819
- );
4820
- 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
+ }
4821
5021
  ctx.save();
4822
5022
  ctx.strokeStyle = axis.lineColor;
4823
5023
  ctx.lineWidth = Math.max(1, axis.lineWidth);
@@ -4826,6 +5026,27 @@ function createChart(element, options = {}) {
4826
5026
  ctx.lineTo(crisp(chartRight), crisp(paneTop));
4827
5027
  ctx.stroke();
4828
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
+ }
4829
5050
  const axisInfo = paneInfo?.axis;
4830
5051
  if (axisInfo && Number.isFinite(axisInfo.min) && Number.isFinite(axisInfo.max) && axisInfo.max !== axisInfo.min) {
4831
5052
  const paneRange = axisInfo.max - axisInfo.min;
@@ -4855,17 +5076,28 @@ function createChart(element, options = {}) {
4855
5076
  ctx.restore();
4856
5077
  if (labels.visible) {
4857
5078
  const prevFont = ctx.font;
4858
- ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
4859
- const legendTitle = paneInfo.title ?? plugin.name;
4860
- 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 ?? [];
4861
5083
  const legendParts = [
4862
5084
  legendTitle,
4863
5085
  ...legendValues.map((value) => value.text ?? (value.value === void 0 ? "" : formatPaneValue(value.value))).filter(Boolean)
4864
5086
  ].filter(Boolean);
5087
+ const legendText = legendParts.join(" ");
4865
5088
  if (legendParts.length > 0) {
4866
- drawText(legendParts.join(" "), chartLeft + 10, paneTop + 8, "left", "top", labels.indicatorTextColor);
5089
+ drawText(legendText, chartLeft + 10, paneTop + 8, "left", "top", labels.indicatorTextColor);
4867
5090
  }
4868
- 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 ?? []) {
4869
5101
  if (!labels.showIndicatorValueLabels) {
4870
5102
  continue;
4871
5103
  }
@@ -6077,6 +6309,54 @@ function createChart(element, options = {}) {
6077
6309
  }
6078
6310
  return "outside";
6079
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
+ };
6080
6360
  const handleDrawingToolPointerDown = (x, y) => {
6081
6361
  if (!activeDrawingTool || !drawState) {
6082
6362
  return false;
@@ -6556,6 +6836,26 @@ function createChart(element, options = {}) {
6556
6836
  setCrosshairPoint(null);
6557
6837
  return;
6558
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
+ }
6559
6859
  }
6560
6860
  const region = getHitRegion(point.x, point.y);
6561
6861
  if (region === "outside") {
@@ -6680,6 +6980,32 @@ function createChart(element, options = {}) {
6680
6980
  return;
6681
6981
  }
6682
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
+ }
6683
7009
  if (orderDragState) {
6684
7010
  if (activePointerId !== null && event.pointerId !== activePointerId) {
6685
7011
  return;
@@ -6752,6 +7078,33 @@ function createChart(element, options = {}) {
6752
7078
  setCrosshairPoint(null);
6753
7079
  return;
6754
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
+ }
6755
7108
  }
6756
7109
  if (!activeDrawingTool && getDrawingHit(point.x, point.y)) {
6757
7110
  const drawingHit = getDrawingHit(point.x, point.y);
@@ -6871,6 +7224,22 @@ function createChart(element, options = {}) {
6871
7224
  drawingDragState = null;
6872
7225
  emitDrawingsChange();
6873
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
+ }
6874
7243
  const endedPlotDrag = isDragging && dragMode === "plot";
6875
7244
  isDragging = false;
6876
7245
  dragMode = null;
@@ -7216,6 +7585,12 @@ function createChart(element, options = {}) {
7216
7585
  const onCrosshairPriceAction = (handler) => {
7217
7586
  crosshairPriceActionHandler = handler;
7218
7587
  };
7588
+ const onIndicatorPaneAction = (handler) => {
7589
+ indicatorPaneActionHandler = handler;
7590
+ };
7591
+ const onIndicatorPaneHeightChange = (handler) => {
7592
+ indicatorPaneHeightChangeHandler = handler;
7593
+ };
7219
7594
  const setDoubleClickEnabled = (enabled) => {
7220
7595
  doubleClickEnabled = enabled;
7221
7596
  };
@@ -7421,6 +7796,8 @@ function createChart(element, options = {}) {
7421
7796
  onChartClick,
7422
7797
  onCrosshairMove,
7423
7798
  onCrosshairPriceAction,
7799
+ onIndicatorPaneAction,
7800
+ onIndicatorPaneHeightChange,
7424
7801
  zoomInX,
7425
7802
  zoomOutX,
7426
7803
  zoomInY,