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.
@@ -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;
@@ -824,6 +859,19 @@ var drawSeparateSeries = (ctx, renderContext, values, color, width, minOverride,
824
859
  break;
825
860
  }
826
861
  }
862
+ if (options.valueLine && latestValue !== null && latestValue >= minValue && latestValue <= maxValue) {
863
+ ctx.save();
864
+ ctx.lineWidth = 1;
865
+ ctx.setLineDash([3, 3]);
866
+ ctx.globalAlpha = 0.65;
867
+ ctx.strokeStyle = color;
868
+ const y = yFromValue(latestValue);
869
+ ctx.beginPath();
870
+ ctx.moveTo(renderContext.chartLeft, y);
871
+ ctx.lineTo(renderContext.chartRight, y);
872
+ ctx.stroke();
873
+ ctx.restore();
874
+ }
827
875
  const decimals = options.decimals ?? 2;
828
876
  const formatValue = (value) => value.toFixed(decimals);
829
877
  const axisTicks = options.axisTicks ?? guideLines;
@@ -1151,7 +1199,7 @@ var BUILTIN_STDDEV_INDICATOR = {
1151
1199
  name: "StdDev",
1152
1200
  pane: "separate",
1153
1201
  paneHeightRatio: 0.16,
1154
- defaultInputs: { length: 20, source: "close", color: "#f97316", width: 2 },
1202
+ defaultInputs: { length: 20, source: "close", color: "#f97316", width: 2, showValueLine: true },
1155
1203
  draw: (ctx, renderContext, inputs) => {
1156
1204
  const length = clampIndicatorLength(inputs.length, 20);
1157
1205
  const values = withCachedSeries(
@@ -1161,7 +1209,8 @@ var BUILTIN_STDDEV_INDICATOR = {
1161
1209
  );
1162
1210
  return drawSeparateSeries(ctx, renderContext, values, inputs.color ?? "#f97316", Number(inputs.width) || 2, void 0, void 0, void 0, {
1163
1211
  title: `StdDev ${length}`,
1164
- decimals: 2
1212
+ decimals: 2,
1213
+ valueLine: inputs.showValueLine !== false
1165
1214
  });
1166
1215
  }
1167
1216
  };
@@ -1170,13 +1219,14 @@ var BUILTIN_ATR_INDICATOR = {
1170
1219
  name: "ATR",
1171
1220
  pane: "separate",
1172
1221
  paneHeightRatio: 0.16,
1173
- defaultInputs: { length: 14, color: "#eab308", width: 2 },
1222
+ defaultInputs: { length: 14, color: "#eab308", width: 2, showValueLine: true },
1174
1223
  draw: (ctx, renderContext, inputs) => {
1175
1224
  const length = clampIndicatorLength(inputs.length, 14);
1176
1225
  const values = withCachedSeries(`atr|${length}`, renderContext.data, () => computeAtrSeries(renderContext.data, length));
1177
1226
  return drawSeparateSeries(ctx, renderContext, values, inputs.color ?? "#eab308", Number(inputs.width) || 2, void 0, void 0, void 0, {
1178
1227
  title: `ATR ${length}`,
1179
- decimals: 2
1228
+ decimals: 2,
1229
+ valueLine: inputs.showValueLine !== false
1180
1230
  });
1181
1231
  }
1182
1232
  };
@@ -1192,7 +1242,8 @@ var BUILTIN_RSI_INDICATOR = {
1192
1242
  showLegend: true,
1193
1243
  showValueLabel: true,
1194
1244
  showGuideLines: true,
1195
- showScaleLabels: true
1245
+ showScaleLabels: true,
1246
+ showValueLine: true
1196
1247
  },
1197
1248
  draw: (ctx, renderContext, inputs) => {
1198
1249
  const length = clampIndicatorLength(inputs.length, 14);
@@ -1215,7 +1266,8 @@ var BUILTIN_RSI_INDICATOR = {
1215
1266
  valueLabelColor: "#9E9E9E",
1216
1267
  valueLabelBackgroundColor: "#9E9E9E",
1217
1268
  valueLabelTextColor: "#0f172a",
1218
- showLegend: inputs.showLegend !== false
1269
+ showLegend: inputs.showLegend !== false,
1270
+ valueLine: inputs.showValueLine !== false
1219
1271
  }
1220
1272
  );
1221
1273
  }
@@ -1272,6 +1324,7 @@ var drawSeparateMultiSeries = (ctx, renderContext, seriesList, options = {}) =>
1272
1324
  const ratio = (value - minValue) / range;
1273
1325
  return renderContext.chartBottom - ratio * renderContext.chartHeight;
1274
1326
  };
1327
+ drawPaneHorizontalGrid(ctx, renderContext, minValue, maxValue);
1275
1328
  if (options.guideLines && options.guideLines.length > 0) {
1276
1329
  ctx.save();
1277
1330
  ctx.strokeStyle = "rgba(148,163,184,0.35)";
@@ -1351,6 +1404,24 @@ var drawSeparateMultiSeries = (ctx, renderContext, seriesList, options = {}) =>
1351
1404
  }
1352
1405
  return null;
1353
1406
  };
1407
+ if (options.valueLines) {
1408
+ ctx.save();
1409
+ ctx.lineWidth = 1;
1410
+ ctx.setLineDash([3, 3]);
1411
+ ctx.globalAlpha = 0.65;
1412
+ for (const spec of seriesList) {
1413
+ if (spec.histogram) continue;
1414
+ const latest = latestOf(spec.values);
1415
+ if (latest === null || latest < minValue || latest > maxValue) continue;
1416
+ const y = yFromValue(latest);
1417
+ ctx.strokeStyle = spec.color;
1418
+ ctx.beginPath();
1419
+ ctx.moveTo(renderContext.chartLeft, y);
1420
+ ctx.lineTo(renderContext.chartRight, y);
1421
+ ctx.stroke();
1422
+ }
1423
+ ctx.restore();
1424
+ }
1354
1425
  const paneInfo = {
1355
1426
  ...options.title ? { title: options.title } : {},
1356
1427
  axis: {
@@ -1704,7 +1775,8 @@ var BUILTIN_MACD_INDICATOR = {
1704
1775
  macdColor: "#2962ff",
1705
1776
  signalColor: "#ff6d00",
1706
1777
  histUpColor: "#26a69a",
1707
- histDownColor: "#ef5350"
1778
+ histDownColor: "#ef5350",
1779
+ showValueLine: true
1708
1780
  },
1709
1781
  draw: (ctx, renderContext, inputs) => {
1710
1782
  const fast = clampIndicatorLength(inputs.fast, 12);
@@ -1733,7 +1805,8 @@ var BUILTIN_MACD_INDICATOR = {
1733
1805
  includeZero: true,
1734
1806
  guideLines: [0],
1735
1807
  decimals: 2,
1736
- valueLabelSeriesIndex: 1
1808
+ valueLabelSeriesIndex: 1,
1809
+ valueLines: inputs.showValueLine !== false
1737
1810
  }
1738
1811
  );
1739
1812
  }
@@ -1743,7 +1816,7 @@ var BUILTIN_STOCHASTIC_INDICATOR = {
1743
1816
  name: "Stoch",
1744
1817
  pane: "separate",
1745
1818
  paneHeightRatio: 0.18,
1746
- defaultInputs: { kLength: 14, kSmoothing: 1, dLength: 3, kColor: "#2962ff", dColor: "#ff6d00" },
1819
+ defaultInputs: { kLength: 14, kSmoothing: 1, dLength: 3, kColor: "#2962ff", dColor: "#ff6d00", showValueLine: true },
1747
1820
  draw: (ctx, renderContext, inputs) => {
1748
1821
  const kLength = clampIndicatorLength(inputs.kLength, 14);
1749
1822
  const kSmoothing = clampIndicatorLength(inputs.kSmoothing, 1);
@@ -1766,7 +1839,8 @@ var BUILTIN_STOCHASTIC_INDICATOR = {
1766
1839
  maxOverride: 100,
1767
1840
  guideLines: [20, 80],
1768
1841
  axisTicks: [0, 20, 50, 80, 100],
1769
- decimals: 2
1842
+ decimals: 2,
1843
+ valueLines: inputs.showValueLine !== false
1770
1844
  }
1771
1845
  );
1772
1846
  }
@@ -1776,7 +1850,15 @@ var BUILTIN_STOCHRSI_INDICATOR = {
1776
1850
  name: "Stoch RSI",
1777
1851
  pane: "separate",
1778
1852
  paneHeightRatio: 0.18,
1779
- defaultInputs: { rsiLength: 14, stochLength: 14, kSmoothing: 3, dSmoothing: 3, kColor: "#2962ff", dColor: "#ff6d00" },
1853
+ defaultInputs: {
1854
+ rsiLength: 14,
1855
+ stochLength: 14,
1856
+ kSmoothing: 3,
1857
+ dSmoothing: 3,
1858
+ kColor: "#2962ff",
1859
+ dColor: "#ff6d00",
1860
+ showValueLine: true
1861
+ },
1780
1862
  draw: (ctx, renderContext, inputs) => {
1781
1863
  const rsiLength = clampIndicatorLength(inputs.rsiLength, 14);
1782
1864
  const stochLength = clampIndicatorLength(inputs.stochLength, 14);
@@ -1800,7 +1882,8 @@ var BUILTIN_STOCHRSI_INDICATOR = {
1800
1882
  maxOverride: 100,
1801
1883
  guideLines: [20, 80],
1802
1884
  axisTicks: [0, 20, 50, 80, 100],
1803
- decimals: 2
1885
+ decimals: 2,
1886
+ valueLines: inputs.showValueLine !== false
1804
1887
  }
1805
1888
  );
1806
1889
  }
@@ -1810,7 +1893,14 @@ var BUILTIN_ADX_INDICATOR = {
1810
1893
  name: "ADX/DMI",
1811
1894
  pane: "separate",
1812
1895
  paneHeightRatio: 0.18,
1813
- defaultInputs: { length: 14, adxColor: "#ff6d00", plusDiColor: "#26a69a", minusDiColor: "#ef5350", showDi: true },
1896
+ defaultInputs: {
1897
+ length: 14,
1898
+ adxColor: "#ff6d00",
1899
+ plusDiColor: "#26a69a",
1900
+ minusDiColor: "#ef5350",
1901
+ showDi: true,
1902
+ showValueLine: true
1903
+ },
1814
1904
  draw: (ctx, renderContext, inputs) => {
1815
1905
  const length = clampIndicatorLength(inputs.length, 14);
1816
1906
  const { adx, plusDi, minusDi } = withCachedComputation(
@@ -1831,7 +1921,8 @@ var BUILTIN_ADX_INDICATOR = {
1831
1921
  title: `ADX ${length}`,
1832
1922
  minOverride: 0,
1833
1923
  guideLines: [20],
1834
- decimals: 2
1924
+ decimals: 2,
1925
+ valueLines: inputs.showValueLine !== false
1835
1926
  });
1836
1927
  }
1837
1928
  };
@@ -1840,14 +1931,14 @@ var BUILTIN_OBV_INDICATOR = {
1840
1931
  name: "OBV",
1841
1932
  pane: "separate",
1842
1933
  paneHeightRatio: 0.16,
1843
- defaultInputs: { color: "#2962ff", width: 2 },
1934
+ defaultInputs: { color: "#2962ff", width: 2, showValueLine: true },
1844
1935
  draw: (ctx, renderContext, inputs) => {
1845
1936
  const values = withCachedSeries("obv", renderContext.data, () => computeObvSeries(renderContext.data));
1846
1937
  return drawSeparateMultiSeries(
1847
1938
  ctx,
1848
1939
  renderContext,
1849
1940
  [{ values, color: inputs.color ?? "#2962ff", width: Number(inputs.width) || 2, label: "OBV" }],
1850
- { title: "OBV", format: formatCompactNumber }
1941
+ { title: "OBV", format: formatCompactNumber, valueLines: inputs.showValueLine !== false }
1851
1942
  );
1852
1943
  }
1853
1944
  };
@@ -1856,7 +1947,7 @@ var BUILTIN_MFI_INDICATOR = {
1856
1947
  name: "MFI",
1857
1948
  pane: "separate",
1858
1949
  paneHeightRatio: 0.16,
1859
- defaultInputs: { length: 14, color: "#7e57c2", width: 2 },
1950
+ defaultInputs: { length: 14, color: "#7e57c2", width: 2, showValueLine: true },
1860
1951
  draw: (ctx, renderContext, inputs) => {
1861
1952
  const length = clampIndicatorLength(inputs.length, 14);
1862
1953
  const values = withCachedSeries(
@@ -1874,7 +1965,8 @@ var BUILTIN_MFI_INDICATOR = {
1874
1965
  maxOverride: 100,
1875
1966
  guideLines: [20, 80],
1876
1967
  axisTicks: [0, 20, 50, 80, 100],
1877
- decimals: 2
1968
+ decimals: 2,
1969
+ valueLines: inputs.showValueLine !== false
1878
1970
  }
1879
1971
  );
1880
1972
  }
@@ -1884,7 +1976,7 @@ var BUILTIN_CCI_INDICATOR = {
1884
1976
  name: "CCI",
1885
1977
  pane: "separate",
1886
1978
  paneHeightRatio: 0.16,
1887
- defaultInputs: { length: 20, color: "#2962ff", width: 2 },
1979
+ defaultInputs: { length: 20, color: "#2962ff", width: 2, showValueLine: true },
1888
1980
  draw: (ctx, renderContext, inputs) => {
1889
1981
  const length = clampIndicatorLength(inputs.length, 20);
1890
1982
  const values = withCachedSeries(
@@ -1896,7 +1988,7 @@ var BUILTIN_CCI_INDICATOR = {
1896
1988
  ctx,
1897
1989
  renderContext,
1898
1990
  [{ values, color: inputs.color ?? "#2962ff", width: Number(inputs.width) || 2 }],
1899
- { title: `CCI ${length}`, guideLines: [-100, 0, 100], decimals: 2 }
1991
+ { title: `CCI ${length}`, guideLines: [-100, 0, 100], decimals: 2, valueLines: inputs.showValueLine !== false }
1900
1992
  );
1901
1993
  }
1902
1994
  };
@@ -1905,7 +1997,7 @@ var BUILTIN_WILLIAMSR_INDICATOR = {
1905
1997
  name: "Williams %R",
1906
1998
  pane: "separate",
1907
1999
  paneHeightRatio: 0.16,
1908
- defaultInputs: { length: 14, color: "#7e57c2", width: 2 },
2000
+ defaultInputs: { length: 14, color: "#7e57c2", width: 2, showValueLine: true },
1909
2001
  draw: (ctx, renderContext, inputs) => {
1910
2002
  const length = clampIndicatorLength(inputs.length, 14);
1911
2003
  const values = withCachedSeries(
@@ -1923,7 +2015,8 @@ var BUILTIN_WILLIAMSR_INDICATOR = {
1923
2015
  maxOverride: 0,
1924
2016
  guideLines: [-80, -20],
1925
2017
  axisTicks: [-100, -80, -50, -20, 0],
1926
- decimals: 2
2018
+ decimals: 2,
2019
+ valueLines: inputs.showValueLine !== false
1927
2020
  }
1928
2021
  );
1929
2022
  }
@@ -1933,7 +2026,7 @@ var BUILTIN_ROC_INDICATOR = {
1933
2026
  name: "ROC",
1934
2027
  pane: "separate",
1935
2028
  paneHeightRatio: 0.16,
1936
- defaultInputs: { length: 9, color: "#2962ff", width: 2 },
2029
+ defaultInputs: { length: 9, color: "#2962ff", width: 2, showValueLine: true },
1937
2030
  draw: (ctx, renderContext, inputs) => {
1938
2031
  const length = clampIndicatorLength(inputs.length, 9);
1939
2032
  const values = withCachedSeries(
@@ -1945,7 +2038,7 @@ var BUILTIN_ROC_INDICATOR = {
1945
2038
  ctx,
1946
2039
  renderContext,
1947
2040
  [{ values, color: inputs.color ?? "#2962ff", width: Number(inputs.width) || 2 }],
1948
- { title: `ROC ${length}`, includeZero: true, guideLines: [0], decimals: 2 }
2041
+ { title: `ROC ${length}`, includeZero: true, guideLines: [0], decimals: 2, valueLines: inputs.showValueLine !== false }
1949
2042
  );
1950
2043
  }
1951
2044
  };
@@ -1954,7 +2047,7 @@ var BUILTIN_MOMENTUM_INDICATOR = {
1954
2047
  name: "Momentum",
1955
2048
  pane: "separate",
1956
2049
  paneHeightRatio: 0.16,
1957
- defaultInputs: { length: 10, color: "#2962ff", width: 2 },
2050
+ defaultInputs: { length: 10, color: "#2962ff", width: 2, showValueLine: true },
1958
2051
  draw: (ctx, renderContext, inputs) => {
1959
2052
  const length = clampIndicatorLength(inputs.length, 10);
1960
2053
  const values = withCachedSeries(
@@ -1966,7 +2059,7 @@ var BUILTIN_MOMENTUM_INDICATOR = {
1966
2059
  ctx,
1967
2060
  renderContext,
1968
2061
  [{ values, color: inputs.color ?? "#2962ff", width: Number(inputs.width) || 2 }],
1969
- { title: `Mom ${length}`, includeZero: true, guideLines: [0], decimals: 2 }
2062
+ { title: `Mom ${length}`, includeZero: true, guideLines: [0], decimals: 2, valueLines: inputs.showValueLine !== false }
1970
2063
  );
1971
2064
  }
1972
2065
  };
@@ -2390,6 +2483,14 @@ function createChart(element, options = {}) {
2390
2483
  let watermarkImageReady = false;
2391
2484
  let drawState = null;
2392
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;
2393
2494
  let orderDragState = null;
2394
2495
  let actionDragState = null;
2395
2496
  let pointerDownInfo = null;
@@ -3491,20 +3592,24 @@ function createChart(element, options = {}) {
3491
3592
  const watermark = { ...DEFAULT_WATERMARK_OPTIONS, ...mergedOptions.watermark ?? {} };
3492
3593
  const paneGap = 0;
3493
3594
  const separatePaneSpacing = 6;
3494
- 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(
3495
3597
  (value) => value.plugin !== void 0 && (value.indicator.pane ?? value.plugin.pane ?? "overlay") === "separate"
3496
3598
  ).sort((a, b) => a.indicator.zIndex - b.indicator.zIndex);
3497
3599
  const overlayIndicatorsForScale = indicators.filter((indicator) => indicator.visible).map((indicator) => ({ indicator, plugin: indicatorRegistry.get(indicator.type) })).filter(
3498
3600
  (value) => value.plugin !== void 0 && (value.indicator.pane ?? value.plugin.pane ?? "overlay") === "overlay"
3499
3601
  );
3500
3602
  const separatePaneHeightDefaults = activeSeparateIndicators.map(({ indicator, plugin }) => {
3603
+ if (!indicator.visible) return collapsedPaneHeight;
3501
3604
  const ratio = Math.min(0.45, Math.max(0.08, indicator.paneHeightRatio ?? plugin.paneHeightRatio ?? 0.22));
3502
3605
  return Math.round(fullChartHeight * ratio);
3503
3606
  });
3504
3607
  const separatePaneDesiredTotal = separatePaneHeightDefaults.reduce((sum, value) => sum + value, 0) + Math.max(0, activeSeparateIndicators.length - 1) * separatePaneSpacing;
3505
3608
  const maxSeparatePaneTotal = Math.max(0, fullChartHeight - 140);
3506
3609
  const separatePaneScale = separatePaneDesiredTotal > 0 && separatePaneDesiredTotal > maxSeparatePaneTotal ? maxSeparatePaneTotal / separatePaneDesiredTotal : 1;
3507
- 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
+ );
3508
3613
  const separatePaneTotal = separatePaneHeights.reduce((sum, value) => sum + value, 0) + Math.max(0, activeSeparateIndicators.length - 1) * separatePaneSpacing;
3509
3614
  const pricePaneGap = activeSeparateIndicators.length > 0 ? paneGap : 0;
3510
3615
  const chartHeight = Math.max(120, fullChartHeight - separatePaneTotal - pricePaneGap);
@@ -4755,43 +4860,138 @@ function createChart(element, options = {}) {
4755
4860
  ctx.restore();
4756
4861
  }
4757
4862
  }
4863
+ paneLayoutFullChartHeight = fullChartHeight;
4864
+ paneLayoutInfos = [];
4865
+ paneButtonRegions = [];
4758
4866
  if (activeSeparateIndicators.length > 0) {
4759
4867
  const xFromIndex = (index) => chartLeft + (index + 0.5 - xStart) / xSpan * chartWidth;
4760
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
+ };
4761
4946
  activeSeparateIndicators.forEach(({ indicator, plugin }, paneIndex) => {
4762
4947
  const paneHeight = separatePaneHeights[paneIndex] ?? 80;
4763
4948
  const paneTop = paneTopCursor;
4764
4949
  const paneBottom = paneTop + paneHeight;
4765
4950
  paneTopCursor = paneBottom + separatePaneSpacing;
4766
- ctx.save();
4767
- ctx.beginPath();
4768
- ctx.rect(chartLeft + 1, paneTop + 1, Math.max(0, chartWidth - 2), Math.max(0, paneHeight - 2));
4769
- ctx.clip();
4770
- const paneInfo = plugin.draw(
4771
- ctx,
4772
- {
4773
- data,
4774
- startIndex,
4775
- endIndex,
4776
- xStart,
4777
- xSpan,
4778
- chartLeft,
4779
- chartRight,
4780
- chartTop: paneTop,
4781
- chartBottom: paneBottom,
4782
- chartWidth,
4783
- chartHeight: paneHeight,
4784
- xFromIndex,
4785
- yFromPrice: null,
4786
- getCandleDirectionByIndex,
4787
- getVolumeByIndex,
4788
- candleSpacing,
4789
- upColor: mergedOptions.upColor,
4790
- downColor: mergedOptions.downColor
4791
- },
4792
- indicator.inputs
4793
- );
4794
- 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
+ }
4795
4995
  ctx.save();
4796
4996
  ctx.strokeStyle = axis.lineColor;
4797
4997
  ctx.lineWidth = Math.max(1, axis.lineWidth);
@@ -4800,6 +5000,27 @@ function createChart(element, options = {}) {
4800
5000
  ctx.lineTo(crisp(chartRight), crisp(paneTop));
4801
5001
  ctx.stroke();
4802
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
+ }
4803
5024
  const axisInfo = paneInfo?.axis;
4804
5025
  if (axisInfo && Number.isFinite(axisInfo.min) && Number.isFinite(axisInfo.max) && axisInfo.max !== axisInfo.min) {
4805
5026
  const paneRange = axisInfo.max - axisInfo.min;
@@ -4829,17 +5050,28 @@ function createChart(element, options = {}) {
4829
5050
  ctx.restore();
4830
5051
  if (labels.visible) {
4831
5052
  const prevFont = ctx.font;
4832
- ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
4833
- const legendTitle = paneInfo.title ?? plugin.name;
4834
- 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 ?? [];
4835
5057
  const legendParts = [
4836
5058
  legendTitle,
4837
5059
  ...legendValues.map((value) => value.text ?? (value.value === void 0 ? "" : formatPaneValue(value.value))).filter(Boolean)
4838
5060
  ].filter(Boolean);
5061
+ const legendText = legendParts.join(" ");
4839
5062
  if (legendParts.length > 0) {
4840
- drawText(legendParts.join(" "), chartLeft + 10, paneTop + 8, "left", "top", labels.indicatorTextColor);
5063
+ drawText(legendText, chartLeft + 10, paneTop + 8, "left", "top", labels.indicatorTextColor);
4841
5064
  }
4842
- for (const label of paneInfo.valueLabels ?? []) {
5065
+ if (hoveredPaneId === indicator.id) {
5066
+ drawPaneButtons(
5067
+ indicator.id,
5068
+ indicator.type,
5069
+ true,
5070
+ chartLeft + 10 + Math.ceil(measureTextWidth(legendText)) + 10,
5071
+ paneTop + 8 + legendFontSize / 2
5072
+ );
5073
+ }
5074
+ for (const label of paneInfo?.valueLabels ?? []) {
4843
5075
  if (!labels.showIndicatorValueLabels) {
4844
5076
  continue;
4845
5077
  }
@@ -6051,6 +6283,54 @@ function createChart(element, options = {}) {
6051
6283
  }
6052
6284
  return "outside";
6053
6285
  };
6286
+ const getPaneButtonHit = (x, y) => {
6287
+ for (const region of paneButtonRegions) {
6288
+ if (x >= region.x && x <= region.x + region.width && y >= region.y && y <= region.y + region.height) {
6289
+ return { id: region.id, type: region.type, action: region.action };
6290
+ }
6291
+ }
6292
+ return null;
6293
+ };
6294
+ const getPaneAt = (x, y) => {
6295
+ if (!drawState || x < drawState.chartLeft || x > drawState.chartRight) {
6296
+ return null;
6297
+ }
6298
+ for (const pane of paneLayoutInfos) {
6299
+ if (y >= pane.top && y <= pane.bottom) {
6300
+ return pane;
6301
+ }
6302
+ }
6303
+ return null;
6304
+ };
6305
+ const getPaneDividerHit = (x, y) => {
6306
+ if (!drawState || x < drawState.chartLeft || x > drawState.chartRight) {
6307
+ return null;
6308
+ }
6309
+ for (const pane of paneLayoutInfos) {
6310
+ if (pane.visible && Math.abs(y - pane.top) <= 4) {
6311
+ return { id: pane.id, heightPx: pane.bottom - pane.top };
6312
+ }
6313
+ }
6314
+ return null;
6315
+ };
6316
+ const handlePaneButtonAction = (hit) => {
6317
+ const indicator = indicators.find((entry) => entry.id === hit.id);
6318
+ if (!indicator) {
6319
+ return;
6320
+ }
6321
+ if (hit.action === "visibility") {
6322
+ indicator.visible = !indicator.visible;
6323
+ scheduleDraw();
6324
+ indicatorPaneActionHandler?.({ id: hit.id, type: hit.type, action: "visibility", visible: indicator.visible });
6325
+ return;
6326
+ }
6327
+ if (hit.action === "remove" && !indicatorPaneActionHandler) {
6328
+ indicators = indicators.filter((entry) => entry.id !== hit.id);
6329
+ scheduleDraw();
6330
+ return;
6331
+ }
6332
+ indicatorPaneActionHandler?.({ id: hit.id, type: hit.type, action: hit.action });
6333
+ };
6054
6334
  const handleDrawingToolPointerDown = (x, y) => {
6055
6335
  if (!activeDrawingTool || !drawState) {
6056
6336
  return false;
@@ -6530,6 +6810,26 @@ function createChart(element, options = {}) {
6530
6810
  setCrosshairPoint(null);
6531
6811
  return;
6532
6812
  }
6813
+ const paneButtonHit = getPaneButtonHit(point.x, point.y);
6814
+ if (paneButtonHit) {
6815
+ setCrosshairPoint(null);
6816
+ handlePaneButtonAction(paneButtonHit);
6817
+ return;
6818
+ }
6819
+ const paneDividerHit = getPaneDividerHit(point.x, point.y);
6820
+ if (paneDividerHit) {
6821
+ activePointerId = event.pointerId;
6822
+ paneDividerDrag = {
6823
+ id: paneDividerHit.id,
6824
+ startY: point.y,
6825
+ startHeightPx: paneDividerHit.heightPx,
6826
+ lastRatio: paneLayoutFullChartHeight > 0 ? paneDividerHit.heightPx / paneLayoutFullChartHeight : 0.2
6827
+ };
6828
+ capturePointer(event.pointerId);
6829
+ canvas.style.cursor = "row-resize";
6830
+ setCrosshairPoint(null);
6831
+ return;
6832
+ }
6533
6833
  }
6534
6834
  const region = getHitRegion(point.x, point.y);
6535
6835
  if (region === "outside") {
@@ -6654,6 +6954,32 @@ function createChart(element, options = {}) {
6654
6954
  return;
6655
6955
  }
6656
6956
  }
6957
+ if (paneDividerDrag) {
6958
+ if (activePointerId !== null && event.pointerId !== activePointerId) {
6959
+ return;
6960
+ }
6961
+ const fullHeight = paneLayoutFullChartHeight;
6962
+ if (fullHeight > 0) {
6963
+ const nextHeightPx = clamp(
6964
+ paneDividerDrag.startHeightPx + (paneDividerDrag.startY - point.y),
6965
+ 48,
6966
+ Math.round(fullHeight * 0.45)
6967
+ );
6968
+ const nextRatio = nextHeightPx / fullHeight;
6969
+ if (Math.abs(nextRatio - paneDividerDrag.lastRatio) > 5e-4) {
6970
+ paneDividerDrag.lastRatio = nextRatio;
6971
+ const targetIndicator = indicators.find((entry) => entry.id === paneDividerDrag?.id);
6972
+ if (targetIndicator) {
6973
+ targetIndicator.paneHeightRatio = nextRatio;
6974
+ indicatorPaneHeightChangeHandler?.({ id: paneDividerDrag.id, paneHeightRatio: nextRatio, dragging: true });
6975
+ scheduleDraw();
6976
+ }
6977
+ }
6978
+ }
6979
+ canvas.style.cursor = "row-resize";
6980
+ setCrosshairPoint(null);
6981
+ return;
6982
+ }
6657
6983
  if (orderDragState) {
6658
6984
  if (activePointerId !== null && event.pointerId !== activePointerId) {
6659
6985
  return;
@@ -6726,6 +7052,33 @@ function createChart(element, options = {}) {
6726
7052
  setCrosshairPoint(null);
6727
7053
  return;
6728
7054
  }
7055
+ const hoveredPane = getPaneAt(point.x, point.y);
7056
+ const nextHoveredPaneId = hoveredPane?.id ?? null;
7057
+ if (nextHoveredPaneId !== hoveredPaneId) {
7058
+ hoveredPaneId = nextHoveredPaneId;
7059
+ scheduleDraw();
7060
+ }
7061
+ const paneButtonHover = getPaneButtonHit(point.x, point.y);
7062
+ const nextHoveredButton = paneButtonHover ? { id: paneButtonHover.id, action: paneButtonHover.action } : null;
7063
+ if ((nextHoveredButton?.id ?? null) !== (hoveredPaneButton?.id ?? null) || nextHoveredButton?.action !== hoveredPaneButton?.action) {
7064
+ hoveredPaneButton = nextHoveredButton;
7065
+ scheduleDraw();
7066
+ }
7067
+ if (paneButtonHover) {
7068
+ const indicatorForTitle = indicators.find((entry) => entry.id === paneButtonHover.id);
7069
+ canvas.title = paneButtonHover.action === "visibility" ? indicatorForTitle?.visible === false ? "Show" : "Hide" : paneButtonHover.action === "settings" ? "Settings" : paneButtonHover.action === "source" ? "View source" : "Remove";
7070
+ canvas.style.cursor = "pointer";
7071
+ setCrosshairPoint(null);
7072
+ return;
7073
+ }
7074
+ if (canvas.title) {
7075
+ canvas.title = "";
7076
+ }
7077
+ if (getPaneDividerHit(point.x, point.y)) {
7078
+ canvas.style.cursor = "row-resize";
7079
+ setCrosshairPoint(null);
7080
+ return;
7081
+ }
6729
7082
  }
6730
7083
  if (!activeDrawingTool && getDrawingHit(point.x, point.y)) {
6731
7084
  const drawingHit = getDrawingHit(point.x, point.y);
@@ -6845,6 +7198,22 @@ function createChart(element, options = {}) {
6845
7198
  drawingDragState = null;
6846
7199
  emitDrawingsChange();
6847
7200
  }
7201
+ if (paneDividerDrag) {
7202
+ indicatorPaneHeightChangeHandler?.({
7203
+ id: paneDividerDrag.id,
7204
+ paneHeightRatio: paneDividerDrag.lastRatio,
7205
+ dragging: false
7206
+ });
7207
+ paneDividerDrag = null;
7208
+ }
7209
+ if (event?.type === "pointerleave" && (hoveredPaneId !== null || hoveredPaneButton !== null)) {
7210
+ hoveredPaneId = null;
7211
+ hoveredPaneButton = null;
7212
+ if (canvas.title) {
7213
+ canvas.title = "";
7214
+ }
7215
+ scheduleDraw();
7216
+ }
6848
7217
  const endedPlotDrag = isDragging && dragMode === "plot";
6849
7218
  isDragging = false;
6850
7219
  dragMode = null;
@@ -7190,6 +7559,12 @@ function createChart(element, options = {}) {
7190
7559
  const onCrosshairPriceAction = (handler) => {
7191
7560
  crosshairPriceActionHandler = handler;
7192
7561
  };
7562
+ const onIndicatorPaneAction = (handler) => {
7563
+ indicatorPaneActionHandler = handler;
7564
+ };
7565
+ const onIndicatorPaneHeightChange = (handler) => {
7566
+ indicatorPaneHeightChangeHandler = handler;
7567
+ };
7193
7568
  const setDoubleClickEnabled = (enabled) => {
7194
7569
  doubleClickEnabled = enabled;
7195
7570
  };
@@ -7395,6 +7770,8 @@ function createChart(element, options = {}) {
7395
7770
  onChartClick,
7396
7771
  onCrosshairMove,
7397
7772
  onCrosshairPriceAction,
7773
+ onIndicatorPaneAction,
7774
+ onIndicatorPaneHeightChange,
7398
7775
  zoomInX,
7399
7776
  zoomOutX,
7400
7777
  zoomInY,