acttrader-charts 1.2.0-beta.8 → 1.3.0-beta.1

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/README.md CHANGED
@@ -892,6 +892,30 @@ Sub-pane heights are user-resizable by dragging the separator between panes.
892
892
 
893
893
  `fibRetracement` · `fibExtension` · `fibChannel` · `fibTimezone` · `fibCircles` · `fibSpiral` · `fibFan` · `fibProjection` · `gannFan` · `gannSquare` · `gannBox`
894
894
 
895
+ **Editable Fib Retracement ratios.** Select a Fib Retracement and open **LEVELS → Configure ▾** in the style popover: each row's ratio is an editable field alongside its visibility checkbox and color swatch. Type a new ratio and press Enter (or click away) and the line moves to it, keeping that row's color and visibility. The number of levels never changes — a value that isn't a finite number, or that another row already uses, reverts to the previous one.
896
+
897
+ Defaults come from the theme (`0` · `0.236` · `0.382` · `0.5` · `0.618` · `0.786` · `1`) and can be changed per theme without touching a drawing:
898
+
899
+ ```ts
900
+ new ChartEngine({
901
+ container,
902
+ themeOverrides: {
903
+ dark: {
904
+ drawing: {
905
+ fibRetracement: {
906
+ levels: {
907
+ '0.786': { visible: false, color: '#e8b84b' }, // hide a default ratio
908
+ '0.886': { visible: true, color: '#e8b84b' }, // and draw this one instead
909
+ },
910
+ },
911
+ },
912
+ },
913
+ },
914
+ });
915
+ ```
916
+
917
+ In a **per-drawing** style (`SerializedDrawing.style.levels`), a level may also be set to `null` — that removes a theme-supplied ratio for that one drawing, which is how a retyped ratio replaces its original rather than adding to it. Ratios are map keys, so they are written in canonical number form (`'0.5'`, not `'0.50'`).
918
+
895
919
  ### Shapes
896
920
 
897
921
  `rectangle` · `rotatedRectangle` · `ellipse` · `triangle` · `circle` · `arc` · `polyline` · `path` · `brush` · `highlighter`
@@ -1254,8 +1254,15 @@ interface DrawingLevelStyle {
1254
1254
  }
1255
1255
  /** Extends DrawingStyle for fib/multi-level drawings. */
1256
1256
  interface DrawingFibStyle extends DrawingStyle {
1257
- /** Map of level value (as string, e.g. '0', '0.236', '0.5', '1') → per-level style. */
1258
- levels?: Record<string, DrawingLevelStyle>;
1257
+ /**
1258
+ * Map of level value (as string, e.g. '0', '0.236', '0.5', '1') → per-level style.
1259
+ *
1260
+ * In a **per-instance** style, `null` marks a level supplied by the theme as
1261
+ * removed for this drawing — used when the user retypes a level's ratio in
1262
+ * the LEVELS → Configure panel, so the old ratio does not come back through
1263
+ * the theme merge. Resolved styles (from `resolveStyle`) never contain null.
1264
+ */
1265
+ levels?: Record<string, DrawingLevelStyle | null>;
1259
1266
  }
1260
1267
  /** Union of all drawing style shapes. */
1261
1268
  type AnyDrawingStyle = DrawingStyle | DrawingTextStyle | DrawingFibStyle;
@@ -1254,8 +1254,15 @@ interface DrawingLevelStyle {
1254
1254
  }
1255
1255
  /** Extends DrawingStyle for fib/multi-level drawings. */
1256
1256
  interface DrawingFibStyle extends DrawingStyle {
1257
- /** Map of level value (as string, e.g. '0', '0.236', '0.5', '1') → per-level style. */
1258
- levels?: Record<string, DrawingLevelStyle>;
1257
+ /**
1258
+ * Map of level value (as string, e.g. '0', '0.236', '0.5', '1') → per-level style.
1259
+ *
1260
+ * In a **per-instance** style, `null` marks a level supplied by the theme as
1261
+ * removed for this drawing — used when the user retypes a level's ratio in
1262
+ * the LEVELS → Configure panel, so the old ratio does not come back through
1263
+ * the theme merge. Resolved styles (from `resolveStyle`) never contain null.
1264
+ */
1265
+ levels?: Record<string, DrawingLevelStyle | null>;
1259
1266
  }
1260
1267
  /** Union of all drawing style shapes. */
1261
1268
  type AnyDrawingStyle = DrawingStyle | DrawingTextStyle | DrawingFibStyle;
package/dist/index.cjs CHANGED
@@ -318,6 +318,7 @@ function resolveDrawingThemeVars(drawing, resolve) {
318
318
  const levels = {};
319
319
  for (const lk of Object.keys(resolved.levels)) {
320
320
  const lv = resolved.levels[lk];
321
+ if (!lv) continue;
321
322
  levels[lk] = { ...lv, color: resolve(lv.color) };
322
323
  }
323
324
  resolved.levels = levels;
@@ -6573,6 +6574,21 @@ var MobileToolsSheet = class {
6573
6574
  };
6574
6575
 
6575
6576
  // src/ui/MobileHeader.ts
6577
+ var SERIES_OPTIONS = [
6578
+ { value: "candlestick", label: "Candlestick" },
6579
+ { value: "hollow", label: "Hollow Candles" },
6580
+ { value: "ohlc", label: "OHLC Bars" },
6581
+ { value: "line", label: "Line" },
6582
+ { value: "area", label: "Area" },
6583
+ { value: "heikinashi", label: "Heikin Ashi" },
6584
+ { value: "volumecandles", label: "Volume Candles" },
6585
+ { value: "linemarkers", label: "Line with Markers" },
6586
+ { value: "step", label: "Step Line" },
6587
+ { value: "hlcarea", label: "HLC Area" },
6588
+ { value: "baseline", label: "Baseline" },
6589
+ { value: "columns", label: "Columns" },
6590
+ { value: "highlow", label: "High Low" }
6591
+ ];
6576
6592
  var TF_PILLS3 = ["1m", "5m", "15m", "1h", "4h", "1D", "1W"];
6577
6593
  var TF_LABEL3 = {
6578
6594
  "1m": "1m",
@@ -6617,14 +6633,21 @@ var IC_SETTINGS2 = (sz = 16) => lucide4(
6617
6633
  1.75
6618
6634
  );
6619
6635
  var MobileHeader = class {
6620
- constructor(onTimeframeChange, toolsController, onAskAiClick, onSettingsClick, currentTf, theme, fontFamily) {
6636
+ constructor(onTimeframeChange, toolsController, onAskAiClick, onSettingsClick, onSeriesChange, currentTf, currentSeries, theme, fontFamily) {
6621
6637
  this.toolsBtn = null;
6622
6638
  this.tfBtns = /* @__PURE__ */ new Map();
6623
6639
  this.toolsActive = false;
6640
+ this.seriesBtn = null;
6641
+ this.seriesMenu = null;
6642
+ this.seriesMenuOpen = false;
6643
+ this.onDocPointerDown = null;
6644
+ this.onWinReposition = null;
6624
6645
  this.onTimeframeChange = onTimeframeChange;
6625
6646
  this.onAskAiClick = onAskAiClick;
6626
6647
  this.onSettingsClick = onSettingsClick;
6648
+ this.onSeriesChange = onSeriesChange;
6627
6649
  this.currentTf = currentTf;
6650
+ this.currentSeries = currentSeries;
6628
6651
  this.theme = theme;
6629
6652
  this.fontFamily = fontFamily ?? MONO_FONT2;
6630
6653
  this.sheet = toolsController ? new MobileToolsSheet(theme, toolsController, this.fontFamily) : null;
@@ -6635,9 +6658,11 @@ var MobileHeader = class {
6635
6658
  }
6636
6659
  // ── Build ──────────────────────────────────────────────────────────────────
6637
6660
  build() {
6661
+ this.closeSeriesMenu();
6638
6662
  this.el.innerHTML = "";
6639
6663
  this.tfBtns.clear();
6640
6664
  this.toolsBtn = null;
6665
+ this.seriesBtn = null;
6641
6666
  Object.assign(this.el.style, {
6642
6667
  display: "flex",
6643
6668
  alignItems: "center",
@@ -6691,6 +6716,41 @@ var MobileHeader = class {
6691
6716
  });
6692
6717
  this.el.appendChild(divider);
6693
6718
  }
6719
+ if (this.onSeriesChange) {
6720
+ const btn = document.createElement("button");
6721
+ btn.setAttribute("data-testid", "m-chart-type-btn");
6722
+ btn.title = "Chart type";
6723
+ btn.innerHTML = this.seriesIconHtml(this.currentSeries);
6724
+ Object.assign(btn.style, {
6725
+ display: "flex",
6726
+ alignItems: "center",
6727
+ justifyContent: "center",
6728
+ padding: "6px",
6729
+ borderRadius: "4px",
6730
+ border: "none",
6731
+ whiteSpace: "nowrap",
6732
+ flexShrink: "0",
6733
+ cursor: "pointer",
6734
+ outline: "none",
6735
+ background: "transparent",
6736
+ color: this.theme.axisText,
6737
+ transition: "color 0.15s, background 0.15s"
6738
+ });
6739
+ btn.addEventListener("click", (e) => {
6740
+ e.stopPropagation();
6741
+ this.toggleSeriesMenu();
6742
+ });
6743
+ this.seriesBtn = btn;
6744
+ this.el.appendChild(btn);
6745
+ const d2 = document.createElement("div");
6746
+ Object.assign(d2.style, {
6747
+ width: "1px",
6748
+ height: "20px",
6749
+ background: this.theme.axisBorder,
6750
+ flexShrink: "0"
6751
+ });
6752
+ this.el.appendChild(d2);
6753
+ }
6694
6754
  for (const tf of TF_PILLS3) {
6695
6755
  const btn = document.createElement("button");
6696
6756
  btn.setAttribute("data-testid", `m-tf-${TF_LABEL3[tf] ?? tf}`);
@@ -6791,6 +6851,135 @@ var MobileHeader = class {
6791
6851
  btn.style.color = active ? t.ui.accent : t.topBar.btnColor;
6792
6852
  }
6793
6853
  }
6854
+ // ── Chart-type (series) picker ───────────────────────────────────────────────
6855
+ /** Current-series icon, sized for the header button (mirrors AdvancedToolbar). */
6856
+ seriesIconHtml(series) {
6857
+ const raw = ICON_SERIES[series] ?? ICON_SERIES.candlestick;
6858
+ return raw.replace(/width="\d+" height="\d+"/, 'width="18" height="18"');
6859
+ }
6860
+ toggleSeriesMenu() {
6861
+ if (this.seriesMenuOpen) this.closeSeriesMenu();
6862
+ else this.openSeriesMenu();
6863
+ }
6864
+ openSeriesMenu() {
6865
+ if (this.seriesMenuOpen || !this.seriesBtn || typeof document === "undefined") return;
6866
+ this.seriesMenu = this.buildSeriesMenu();
6867
+ document.body.appendChild(this.seriesMenu);
6868
+ this.seriesMenuOpen = true;
6869
+ this.positionSeriesMenu();
6870
+ this.styleSeriesBtnActive(true);
6871
+ this.onDocPointerDown = (e) => {
6872
+ const t = e.target;
6873
+ if (this.seriesMenu?.contains(t) || this.seriesBtn?.contains(t)) return;
6874
+ this.closeSeriesMenu();
6875
+ };
6876
+ this.onWinReposition = () => this.closeSeriesMenu();
6877
+ setTimeout(() => {
6878
+ if (!this.seriesMenuOpen) return;
6879
+ document.addEventListener("pointerdown", this.onDocPointerDown, true);
6880
+ window.addEventListener("resize", this.onWinReposition, true);
6881
+ window.addEventListener("scroll", this.onWinReposition, true);
6882
+ this.el.addEventListener("scroll", this.onWinReposition, true);
6883
+ }, 0);
6884
+ }
6885
+ closeSeriesMenu() {
6886
+ if (this.onDocPointerDown) {
6887
+ document.removeEventListener("pointerdown", this.onDocPointerDown, true);
6888
+ this.onDocPointerDown = null;
6889
+ }
6890
+ if (this.onWinReposition) {
6891
+ window.removeEventListener("resize", this.onWinReposition, true);
6892
+ window.removeEventListener("scroll", this.onWinReposition, true);
6893
+ this.el.removeEventListener("scroll", this.onWinReposition, true);
6894
+ this.onWinReposition = null;
6895
+ }
6896
+ this.seriesMenu?.remove();
6897
+ this.seriesMenu = null;
6898
+ this.seriesMenuOpen = false;
6899
+ this.styleSeriesBtnActive(false);
6900
+ }
6901
+ buildSeriesMenu() {
6902
+ const t = this.theme;
6903
+ const menu = document.createElement("div");
6904
+ menu.setAttribute("data-testid", "m-chart-type-menu");
6905
+ Object.assign(menu.style, {
6906
+ position: "fixed",
6907
+ zIndex: "2147483000",
6908
+ minWidth: "184px",
6909
+ maxHeight: "60vh",
6910
+ overflowY: "auto",
6911
+ padding: "4px",
6912
+ borderRadius: "8px",
6913
+ border: `1px solid ${t.axisBorder}`,
6914
+ background: t.background,
6915
+ boxShadow: "0 8px 28px rgba(0,0,0,0.35)",
6916
+ fontFamily: this.fontFamily,
6917
+ WebkitOverflowScrolling: "touch"
6918
+ });
6919
+ for (const opt of SERIES_OPTIONS) {
6920
+ const active = opt.value === this.currentSeries;
6921
+ const row = document.createElement("button");
6922
+ row.type = "button";
6923
+ row.setAttribute("data-testid", `m-chart-type-${opt.value}`);
6924
+ Object.assign(row.style, {
6925
+ display: "flex",
6926
+ alignItems: "center",
6927
+ gap: "10px",
6928
+ width: "100%",
6929
+ padding: "9px 10px",
6930
+ borderRadius: "6px",
6931
+ border: "none",
6932
+ cursor: "pointer",
6933
+ outline: "none",
6934
+ textAlign: "left",
6935
+ fontSize: "13px",
6936
+ fontFamily: this.fontFamily,
6937
+ background: active ? t.ui.accentBg : "transparent",
6938
+ color: active ? t.ui.accent : t.axisText
6939
+ });
6940
+ const icon = (ICON_SERIES[opt.value] ?? "").replace(/width="\d+" height="\d+"/, 'width="16" height="16"');
6941
+ const check = active ? ICON_CHECKMARK.replace(/width="\d+" height="\d+"/, 'width="16" height="16"') : "";
6942
+ row.innerHTML = `<span style="display:flex;flex-shrink:0;">${icon}</span><span style="flex:1;">${opt.label}</span><span style="display:flex;flex-shrink:0;width:16px;">${check}</span>`;
6943
+ row.addEventListener("mouseenter", () => {
6944
+ if (opt.value !== this.currentSeries) row.style.background = t.ui.accentBg;
6945
+ });
6946
+ row.addEventListener("mouseleave", () => {
6947
+ if (opt.value !== this.currentSeries) row.style.background = "transparent";
6948
+ });
6949
+ row.addEventListener("click", (e) => {
6950
+ e.stopPropagation();
6951
+ this.selectSeries(opt.value);
6952
+ });
6953
+ menu.appendChild(row);
6954
+ }
6955
+ return menu;
6956
+ }
6957
+ positionSeriesMenu() {
6958
+ if (!this.seriesMenu || !this.seriesBtn) return;
6959
+ const r = this.seriesBtn.getBoundingClientRect();
6960
+ const m = this.seriesMenu;
6961
+ m.style.top = `${r.bottom + 6}px`;
6962
+ m.style.left = `${r.left}px`;
6963
+ const mr = m.getBoundingClientRect();
6964
+ if (mr.right > window.innerWidth - 8) {
6965
+ m.style.left = `${Math.max(8, window.innerWidth - 8 - mr.width)}px`;
6966
+ }
6967
+ if (mr.bottom > window.innerHeight - 8) {
6968
+ m.style.top = `${Math.max(8, r.top - 6 - mr.height)}px`;
6969
+ }
6970
+ }
6971
+ selectSeries(series) {
6972
+ this.closeSeriesMenu();
6973
+ if (series === this.currentSeries) return;
6974
+ this.currentSeries = series;
6975
+ if (this.seriesBtn) this.seriesBtn.innerHTML = this.seriesIconHtml(series);
6976
+ this.onSeriesChange?.(series);
6977
+ }
6978
+ styleSeriesBtnActive(active) {
6979
+ if (!this.seriesBtn) return;
6980
+ this.seriesBtn.style.color = active ? this.theme.ui.accent : this.theme.axisText;
6981
+ this.seriesBtn.style.background = active ? this.theme.ui.accentBg : "transparent";
6982
+ }
6794
6983
  // ── Public surface (drop-in parity with CompactToolbar) ──────────────────────
6795
6984
  setTimeframe(tf) {
6796
6985
  this.currentTf = tf;
@@ -6808,10 +6997,12 @@ var MobileHeader = class {
6808
6997
  this.sheet?.setTheme(theme);
6809
6998
  }
6810
6999
  destroy() {
7000
+ this.closeSeriesMenu();
6811
7001
  this.sheet?.destroy();
6812
7002
  this.el.remove();
6813
7003
  this.tfBtns.clear();
6814
7004
  this.toolsBtn = null;
7005
+ this.seriesBtn = null;
6815
7006
  }
6816
7007
  // ── Forwarded to the "Draw & Indicators" sheet so its toggles stay in sync ───
6817
7008
  /**
@@ -6827,15 +7018,24 @@ var MobileHeader = class {
6827
7018
  this.sheet?.syncVolume(show);
6828
7019
  }
6829
7020
  // ── No-op stubs — controls this variant intentionally does not render ────────
6830
- setSeries(_series) {
7021
+ /** Engine → header sync: reflect an external series change (restored layout
7022
+ * state, keyboard shortcut, etc.) on the chart-type button. */
7023
+ setSeries(series) {
7024
+ this.currentSeries = series;
7025
+ if (this.seriesBtn) this.seriesBtn.innerHTML = this.seriesIconHtml(series);
7026
+ if (this.seriesMenuOpen) {
7027
+ this.closeSeriesMenu();
7028
+ this.openSeriesMenu();
7029
+ }
6831
7030
  }
6832
7031
  setIndicatorOptions(_options) {
6833
7032
  }
6834
7033
  closeDropdown() {
6835
7034
  this.sheet?.close();
7035
+ this.closeSeriesMenu();
6836
7036
  }
6837
7037
  get hasOpenDropdown() {
6838
- return this.sheet?.isOpen ?? false;
7038
+ return (this.sheet?.isOpen ?? false) || this.seriesMenuOpen;
6839
7039
  }
6840
7040
  setFullscreen(_active) {
6841
7041
  }
@@ -8531,10 +8731,19 @@ function resolveStyle(type, instanceStyle, theme) {
8531
8731
  const themeLevels = themeEntry.levels;
8532
8732
  const instLevels = instanceStyle?.levels;
8533
8733
  if (themeLevels || instLevels) {
8534
- const out = { ...themeLevels ?? {} };
8734
+ const out = {};
8735
+ for (const k of Object.keys(themeLevels ?? {})) {
8736
+ const lvl = themeLevels?.[k];
8737
+ if (lvl) out[k] = lvl;
8738
+ }
8535
8739
  if (instLevels) {
8536
8740
  for (const k of Object.keys(instLevels)) {
8537
- out[k] = { ...out[k] ?? { visible: true, color: "#888888" }, ...instLevels[k] };
8741
+ const inst = instLevels[k];
8742
+ if (inst === null) {
8743
+ delete out[k];
8744
+ continue;
8745
+ }
8746
+ out[k] = { ...out[k] ?? { visible: true, color: "#888888" }, ...inst };
8538
8747
  }
8539
8748
  }
8540
8749
  merged.levels = out;
@@ -9155,6 +9364,12 @@ var DisjointChannel = class {
9155
9364
  var FIB_LEVELS = [0, 0.236, 0.382, 0.5, 0.618, 0.786, 1];
9156
9365
  var FIB_FALLBACK_COLORS = ["#ef5350", "#e8b84b", "#26a69a", "#2196f3", "#26a69a", "#e8b84b", "#ef5350"];
9157
9366
  var HIT_TOLERANCE9 = 6;
9367
+ function fibLevelEntries(levels) {
9368
+ const fallback = () => FIB_LEVELS.map((value) => ({ key: String(value), value }));
9369
+ if (!levels) return fallback();
9370
+ const entries = Object.keys(levels).filter((key) => levels[key] !== null).map((key) => ({ key, value: Number(key) })).filter((e) => Number.isFinite(e.value));
9371
+ return entries.length ? entries.sort((a, b) => a.value - b.value) : fallback();
9372
+ }
9158
9373
  var FibRetracement = class {
9159
9374
  constructor(first) {
9160
9375
  this.type = "fibRetracement";
@@ -9180,14 +9395,16 @@ var FibRetracement = class {
9180
9395
  ctx.save();
9181
9396
  ctx.font = "10px 'Inter', system-ui, sans-serif";
9182
9397
  ctx.lineWidth = baseLineWidth;
9183
- for (let i = 0; i < FIB_LEVELS.length; i++) {
9184
- const level = FIB_LEVELS[i];
9185
- const key = String(level);
9398
+ const entries = fibLevelEntries(s.levels);
9399
+ const drawn = [];
9400
+ for (let i = 0; i < entries.length; i++) {
9401
+ const { key, value: level } = entries[i];
9186
9402
  const lvl = s.levels?.[key];
9187
9403
  if (lvl?.visible === false) continue;
9404
+ drawn.push(level);
9188
9405
  const price = highPrice - range * level;
9189
9406
  const y = scale.yToPixel(price, priceRange, chartH);
9190
- const baseColor = lvl?.color ?? s.strokeColor ?? FIB_FALLBACK_COLORS[i];
9407
+ const baseColor = lvl?.color ?? s.strokeColor ?? FIB_FALLBACK_COLORS[i % FIB_FALLBACK_COLORS.length];
9191
9408
  const color = hexWithAlpha(baseColor, isSelected ? 0.8 : 0.6);
9192
9409
  ctx.strokeStyle = color;
9193
9410
  applyLineStyle(ctx, isSelected ? "solid" : s.lineStyle, baseLineWidth);
@@ -9201,6 +9418,7 @@ var FibRetracement = class {
9201
9418
  ctx.fillStyle = color;
9202
9419
  ctx.fillText(lbl, xMin + 4, y - 3);
9203
9420
  }
9421
+ this.visibleLevels = drawn;
9204
9422
  for (const [ax, ay] of [[x1, y1], [x22, y22]]) {
9205
9423
  ctx.beginPath();
9206
9424
  ctx.arc(ax, ay, isSelected ? 4 : 3, 0, Math.PI * 2);
@@ -9214,7 +9432,7 @@ var FibRetracement = class {
9214
9432
  const highPrice = Math.max(this.points[0].price, this.points[1].price);
9215
9433
  const lowPrice = Math.min(this.points[0].price, this.points[1].price);
9216
9434
  const range = highPrice - lowPrice;
9217
- for (const level of FIB_LEVELS) {
9435
+ for (const level of this.visibleLevels ?? FIB_LEVELS) {
9218
9436
  const price = highPrice - range * level;
9219
9437
  const lineY = scale.yToPixel(price, priceRange, chartH);
9220
9438
  if (Math.abs(y - lineY) <= HIT_TOLERANCE9) return true;
@@ -14800,9 +15018,9 @@ var DrawingManager = class {
14800
15018
  updateDrawingStyle(id, patch) {
14801
15019
  const drawing = this.getDrawingById(id);
14802
15020
  if (!drawing) return null;
15021
+ const existingLevels = drawing.style?.levels;
14803
15022
  drawing.style = { ...drawing.style ?? {}, ...patch };
14804
15023
  const patchLevels = patch.levels;
14805
- const existingLevels = drawing.style.levels;
14806
15024
  if (patchLevels && existingLevels) {
14807
15025
  drawing.style.levels = {
14808
15026
  ...existingLevels,
@@ -16854,6 +17072,23 @@ var POPOVER_CONTROLS = {
16854
17072
  var LINE_WIDTHS = [1, 2, 3, 4];
16855
17073
  var FONT_SIZES = [10, 11, 12, 13, 14, 16, 18, 20, 24];
16856
17074
  var SWATCH_PX = 24;
17075
+ var EDITABLE_LEVEL_TYPES = /* @__PURE__ */ new Set(["fibRetracement"]);
17076
+ function mergeLevels(themeLevels, instLevels) {
17077
+ const out = {};
17078
+ for (const k of Object.keys(themeLevels ?? {})) {
17079
+ const lvl = themeLevels?.[k];
17080
+ if (lvl) out[k] = lvl;
17081
+ }
17082
+ for (const k of Object.keys(instLevels ?? {})) {
17083
+ const lvl = instLevels?.[k];
17084
+ if (lvl === null) delete out[k];
17085
+ else if (lvl) out[k] = { ...out[k] ?? { visible: true, color: "#888888" }, ...lvl };
17086
+ }
17087
+ return out;
17088
+ }
17089
+ function levelKey(value) {
17090
+ return String(value);
17091
+ }
16857
17092
  var DrawingStylePopover = class {
16858
17093
  constructor(container, theme, onChange) {
16859
17094
  this.currentDrawing = null;
@@ -16998,7 +17233,9 @@ var DrawingStylePopover = class {
16998
17233
  case "fibLevels":
16999
17234
  groups.push(this.group(
17000
17235
  "Levels",
17001
- this.buildFibLevelsButton({ ...themeEntry.levels ?? {}, ...style.levels ?? {} })
17236
+ this.buildFibLevelsButton(
17237
+ mergeLevels(themeEntry.levels, style.levels)
17238
+ )
17002
17239
  ));
17003
17240
  break;
17004
17241
  }
@@ -17198,6 +17435,65 @@ var DrawingStylePopover = class {
17198
17435
  });
17199
17436
  return wrap;
17200
17437
  }
17438
+ /** Static ratio text — used for level sets whose ratios are fixed. */
17439
+ buildLevelRatioLabel(key) {
17440
+ const label = document.createElement("span");
17441
+ label.textContent = key;
17442
+ Object.assign(label.style, { fontSize: "11px", color: this.theme.tooltip.text });
17443
+ return label;
17444
+ }
17445
+ /**
17446
+ * Editable ratio field (TradingView-style): the user retypes a level's ratio
17447
+ * and the line moves, keeping its color and visibility. Commits on Enter or
17448
+ * blur; reverts when the value isn't a finite number or already belongs to
17449
+ * another row, so the level count never changes.
17450
+ */
17451
+ buildLevelRatioInput(initialKey, taken, onCommit) {
17452
+ let current = initialKey;
17453
+ const input = document.createElement("input");
17454
+ input.type = "number";
17455
+ input.step = "0.001";
17456
+ input.value = current;
17457
+ input.title = "Level ratio \u2014 press Enter to apply";
17458
+ Object.assign(input.style, {
17459
+ width: "58px",
17460
+ padding: "2px 4px",
17461
+ fontSize: "11px",
17462
+ fontFamily: "inherit",
17463
+ color: this.theme.tooltip.text,
17464
+ background: this.theme.background,
17465
+ border: `1px solid ${this.theme.axisBorder}`,
17466
+ borderRadius: "3px",
17467
+ outline: "none"
17468
+ });
17469
+ input.addEventListener("focus", () => {
17470
+ input.style.borderColor = this.theme.ui.accent;
17471
+ });
17472
+ input.addEventListener("blur", () => {
17473
+ input.style.borderColor = this.theme.axisBorder;
17474
+ });
17475
+ input.addEventListener("keydown", (e) => {
17476
+ e.stopPropagation();
17477
+ if (e.key === "Enter") input.blur();
17478
+ });
17479
+ const commit = () => {
17480
+ const raw = Number(input.value);
17481
+ const next = Number.isFinite(raw) ? levelKey(raw) : null;
17482
+ if (next === null || next !== current && taken.has(next)) {
17483
+ input.value = current;
17484
+ return;
17485
+ }
17486
+ if (next === current) {
17487
+ input.value = current;
17488
+ return;
17489
+ }
17490
+ current = next;
17491
+ input.value = next;
17492
+ onCommit(next);
17493
+ };
17494
+ input.addEventListener("change", commit);
17495
+ return input;
17496
+ }
17201
17497
  openFibPanel(anchor, levels) {
17202
17498
  this.closeFibPanel();
17203
17499
  const panel = document.createElement("div");
@@ -17219,23 +17515,37 @@ var DrawingStylePopover = class {
17219
17515
  alignItems: "center"
17220
17516
  });
17221
17517
  const keys = Object.keys(levels).sort((a, b) => Number(a) - Number(b));
17518
+ const editable = this.currentDrawing !== null && EDITABLE_LEVEL_TYPES.has(this.currentDrawing.type);
17519
+ const taken = new Set(keys);
17222
17520
  for (const k of keys) {
17223
17521
  const level = levels[k];
17522
+ let key = k;
17523
+ let color = level.color;
17224
17524
  const cb = document.createElement("input");
17225
17525
  cb.type = "checkbox";
17226
17526
  cb.checked = level.visible !== false;
17227
17527
  cb.style.accentColor = this.theme.ui.accent;
17228
17528
  cb.style.cursor = "pointer";
17229
- const label = document.createElement("span");
17230
- label.textContent = k;
17231
- Object.assign(label.style, { fontSize: "11px", color: this.theme.tooltip.text });
17232
- const swatch = this.makeColorSwatch(level.color, (hex) => {
17233
- this.emit({ levels: { [k]: { visible: cb.checked, color: hex } } });
17529
+ const swatch = this.makeColorSwatch(color, (hex) => {
17530
+ color = hex;
17531
+ this.emit({ levels: { [key]: { visible: cb.checked, color: hex } } });
17234
17532
  });
17235
17533
  cb.addEventListener("change", () => {
17236
- this.emit({ levels: { [k]: { visible: cb.checked, color: level.color } } });
17534
+ this.emit({ levels: { [key]: { visible: cb.checked, color } } });
17237
17535
  });
17238
- panel.append(cb, label, swatch);
17536
+ panel.append(cb, editable ? this.buildLevelRatioInput(
17537
+ k,
17538
+ taken,
17539
+ (nextKey) => {
17540
+ this.emit({ levels: {
17541
+ [key]: null,
17542
+ [nextKey]: { visible: cb.checked, color }
17543
+ } });
17544
+ taken.delete(key);
17545
+ taken.add(nextKey);
17546
+ key = nextKey;
17547
+ }
17548
+ ) : this.buildLevelRatioLabel(k), swatch);
17239
17549
  }
17240
17550
  const anchorRect = anchor.getBoundingClientRect();
17241
17551
  const cRect = this.container.getBoundingClientRect();
@@ -23615,7 +23925,9 @@ var _ChartEngine = class _ChartEngine {
23615
23925
  toolsController,
23616
23926
  config.onAskAiClick ?? null,
23617
23927
  this.settingsEnabled ? () => this.openChartSettings() : null,
23928
+ (s) => this.setSeries(s),
23618
23929
  this.timeframe,
23930
+ this.series,
23619
23931
  this.theme,
23620
23932
  this.uiConfig.topBar?.dropBtnFontFamily
23621
23933
  );