acttrader-charts 1.2.0-beta.7 → 1.2.0-beta.9

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;
@@ -8531,10 +8532,19 @@ function resolveStyle(type, instanceStyle, theme) {
8531
8532
  const themeLevels = themeEntry.levels;
8532
8533
  const instLevels = instanceStyle?.levels;
8533
8534
  if (themeLevels || instLevels) {
8534
- const out = { ...themeLevels ?? {} };
8535
+ const out = {};
8536
+ for (const k of Object.keys(themeLevels ?? {})) {
8537
+ const lvl = themeLevels?.[k];
8538
+ if (lvl) out[k] = lvl;
8539
+ }
8535
8540
  if (instLevels) {
8536
8541
  for (const k of Object.keys(instLevels)) {
8537
- out[k] = { ...out[k] ?? { visible: true, color: "#888888" }, ...instLevels[k] };
8542
+ const inst = instLevels[k];
8543
+ if (inst === null) {
8544
+ delete out[k];
8545
+ continue;
8546
+ }
8547
+ out[k] = { ...out[k] ?? { visible: true, color: "#888888" }, ...inst };
8538
8548
  }
8539
8549
  }
8540
8550
  merged.levels = out;
@@ -9155,6 +9165,12 @@ var DisjointChannel = class {
9155
9165
  var FIB_LEVELS = [0, 0.236, 0.382, 0.5, 0.618, 0.786, 1];
9156
9166
  var FIB_FALLBACK_COLORS = ["#ef5350", "#e8b84b", "#26a69a", "#2196f3", "#26a69a", "#e8b84b", "#ef5350"];
9157
9167
  var HIT_TOLERANCE9 = 6;
9168
+ function fibLevelEntries(levels) {
9169
+ const fallback = () => FIB_LEVELS.map((value) => ({ key: String(value), value }));
9170
+ if (!levels) return fallback();
9171
+ const entries = Object.keys(levels).filter((key) => levels[key] !== null).map((key) => ({ key, value: Number(key) })).filter((e) => Number.isFinite(e.value));
9172
+ return entries.length ? entries.sort((a, b) => a.value - b.value) : fallback();
9173
+ }
9158
9174
  var FibRetracement = class {
9159
9175
  constructor(first) {
9160
9176
  this.type = "fibRetracement";
@@ -9180,14 +9196,16 @@ var FibRetracement = class {
9180
9196
  ctx.save();
9181
9197
  ctx.font = "10px 'Inter', system-ui, sans-serif";
9182
9198
  ctx.lineWidth = baseLineWidth;
9183
- for (let i = 0; i < FIB_LEVELS.length; i++) {
9184
- const level = FIB_LEVELS[i];
9185
- const key = String(level);
9199
+ const entries = fibLevelEntries(s.levels);
9200
+ const drawn = [];
9201
+ for (let i = 0; i < entries.length; i++) {
9202
+ const { key, value: level } = entries[i];
9186
9203
  const lvl = s.levels?.[key];
9187
9204
  if (lvl?.visible === false) continue;
9205
+ drawn.push(level);
9188
9206
  const price = highPrice - range * level;
9189
9207
  const y = scale.yToPixel(price, priceRange, chartH);
9190
- const baseColor = lvl?.color ?? s.strokeColor ?? FIB_FALLBACK_COLORS[i];
9208
+ const baseColor = lvl?.color ?? s.strokeColor ?? FIB_FALLBACK_COLORS[i % FIB_FALLBACK_COLORS.length];
9191
9209
  const color = hexWithAlpha(baseColor, isSelected ? 0.8 : 0.6);
9192
9210
  ctx.strokeStyle = color;
9193
9211
  applyLineStyle(ctx, isSelected ? "solid" : s.lineStyle, baseLineWidth);
@@ -9201,6 +9219,7 @@ var FibRetracement = class {
9201
9219
  ctx.fillStyle = color;
9202
9220
  ctx.fillText(lbl, xMin + 4, y - 3);
9203
9221
  }
9222
+ this.visibleLevels = drawn;
9204
9223
  for (const [ax, ay] of [[x1, y1], [x22, y22]]) {
9205
9224
  ctx.beginPath();
9206
9225
  ctx.arc(ax, ay, isSelected ? 4 : 3, 0, Math.PI * 2);
@@ -9214,7 +9233,7 @@ var FibRetracement = class {
9214
9233
  const highPrice = Math.max(this.points[0].price, this.points[1].price);
9215
9234
  const lowPrice = Math.min(this.points[0].price, this.points[1].price);
9216
9235
  const range = highPrice - lowPrice;
9217
- for (const level of FIB_LEVELS) {
9236
+ for (const level of this.visibleLevels ?? FIB_LEVELS) {
9218
9237
  const price = highPrice - range * level;
9219
9238
  const lineY = scale.yToPixel(price, priceRange, chartH);
9220
9239
  if (Math.abs(y - lineY) <= HIT_TOLERANCE9) return true;
@@ -14800,9 +14819,9 @@ var DrawingManager = class {
14800
14819
  updateDrawingStyle(id, patch) {
14801
14820
  const drawing = this.getDrawingById(id);
14802
14821
  if (!drawing) return null;
14822
+ const existingLevels = drawing.style?.levels;
14803
14823
  drawing.style = { ...drawing.style ?? {}, ...patch };
14804
14824
  const patchLevels = patch.levels;
14805
- const existingLevels = drawing.style.levels;
14806
14825
  if (patchLevels && existingLevels) {
14807
14826
  drawing.style.levels = {
14808
14827
  ...existingLevels,
@@ -16854,6 +16873,23 @@ var POPOVER_CONTROLS = {
16854
16873
  var LINE_WIDTHS = [1, 2, 3, 4];
16855
16874
  var FONT_SIZES = [10, 11, 12, 13, 14, 16, 18, 20, 24];
16856
16875
  var SWATCH_PX = 24;
16876
+ var EDITABLE_LEVEL_TYPES = /* @__PURE__ */ new Set(["fibRetracement"]);
16877
+ function mergeLevels(themeLevels, instLevels) {
16878
+ const out = {};
16879
+ for (const k of Object.keys(themeLevels ?? {})) {
16880
+ const lvl = themeLevels?.[k];
16881
+ if (lvl) out[k] = lvl;
16882
+ }
16883
+ for (const k of Object.keys(instLevels ?? {})) {
16884
+ const lvl = instLevels?.[k];
16885
+ if (lvl === null) delete out[k];
16886
+ else if (lvl) out[k] = { ...out[k] ?? { visible: true, color: "#888888" }, ...lvl };
16887
+ }
16888
+ return out;
16889
+ }
16890
+ function levelKey(value) {
16891
+ return String(value);
16892
+ }
16857
16893
  var DrawingStylePopover = class {
16858
16894
  constructor(container, theme, onChange) {
16859
16895
  this.currentDrawing = null;
@@ -16998,7 +17034,9 @@ var DrawingStylePopover = class {
16998
17034
  case "fibLevels":
16999
17035
  groups.push(this.group(
17000
17036
  "Levels",
17001
- this.buildFibLevelsButton({ ...themeEntry.levels ?? {}, ...style.levels ?? {} })
17037
+ this.buildFibLevelsButton(
17038
+ mergeLevels(themeEntry.levels, style.levels)
17039
+ )
17002
17040
  ));
17003
17041
  break;
17004
17042
  }
@@ -17198,6 +17236,65 @@ var DrawingStylePopover = class {
17198
17236
  });
17199
17237
  return wrap;
17200
17238
  }
17239
+ /** Static ratio text — used for level sets whose ratios are fixed. */
17240
+ buildLevelRatioLabel(key) {
17241
+ const label = document.createElement("span");
17242
+ label.textContent = key;
17243
+ Object.assign(label.style, { fontSize: "11px", color: this.theme.tooltip.text });
17244
+ return label;
17245
+ }
17246
+ /**
17247
+ * Editable ratio field (TradingView-style): the user retypes a level's ratio
17248
+ * and the line moves, keeping its color and visibility. Commits on Enter or
17249
+ * blur; reverts when the value isn't a finite number or already belongs to
17250
+ * another row, so the level count never changes.
17251
+ */
17252
+ buildLevelRatioInput(initialKey, taken, onCommit) {
17253
+ let current = initialKey;
17254
+ const input = document.createElement("input");
17255
+ input.type = "number";
17256
+ input.step = "0.001";
17257
+ input.value = current;
17258
+ input.title = "Level ratio \u2014 press Enter to apply";
17259
+ Object.assign(input.style, {
17260
+ width: "58px",
17261
+ padding: "2px 4px",
17262
+ fontSize: "11px",
17263
+ fontFamily: "inherit",
17264
+ color: this.theme.tooltip.text,
17265
+ background: this.theme.background,
17266
+ border: `1px solid ${this.theme.axisBorder}`,
17267
+ borderRadius: "3px",
17268
+ outline: "none"
17269
+ });
17270
+ input.addEventListener("focus", () => {
17271
+ input.style.borderColor = this.theme.ui.accent;
17272
+ });
17273
+ input.addEventListener("blur", () => {
17274
+ input.style.borderColor = this.theme.axisBorder;
17275
+ });
17276
+ input.addEventListener("keydown", (e) => {
17277
+ e.stopPropagation();
17278
+ if (e.key === "Enter") input.blur();
17279
+ });
17280
+ const commit = () => {
17281
+ const raw = Number(input.value);
17282
+ const next = Number.isFinite(raw) ? levelKey(raw) : null;
17283
+ if (next === null || next !== current && taken.has(next)) {
17284
+ input.value = current;
17285
+ return;
17286
+ }
17287
+ if (next === current) {
17288
+ input.value = current;
17289
+ return;
17290
+ }
17291
+ current = next;
17292
+ input.value = next;
17293
+ onCommit(next);
17294
+ };
17295
+ input.addEventListener("change", commit);
17296
+ return input;
17297
+ }
17201
17298
  openFibPanel(anchor, levels) {
17202
17299
  this.closeFibPanel();
17203
17300
  const panel = document.createElement("div");
@@ -17219,23 +17316,37 @@ var DrawingStylePopover = class {
17219
17316
  alignItems: "center"
17220
17317
  });
17221
17318
  const keys = Object.keys(levels).sort((a, b) => Number(a) - Number(b));
17319
+ const editable = this.currentDrawing !== null && EDITABLE_LEVEL_TYPES.has(this.currentDrawing.type);
17320
+ const taken = new Set(keys);
17222
17321
  for (const k of keys) {
17223
17322
  const level = levels[k];
17323
+ let key = k;
17324
+ let color = level.color;
17224
17325
  const cb = document.createElement("input");
17225
17326
  cb.type = "checkbox";
17226
17327
  cb.checked = level.visible !== false;
17227
17328
  cb.style.accentColor = this.theme.ui.accent;
17228
17329
  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 } } });
17330
+ const swatch = this.makeColorSwatch(color, (hex) => {
17331
+ color = hex;
17332
+ this.emit({ levels: { [key]: { visible: cb.checked, color: hex } } });
17234
17333
  });
17235
17334
  cb.addEventListener("change", () => {
17236
- this.emit({ levels: { [k]: { visible: cb.checked, color: level.color } } });
17335
+ this.emit({ levels: { [key]: { visible: cb.checked, color } } });
17237
17336
  });
17238
- panel.append(cb, label, swatch);
17337
+ panel.append(cb, editable ? this.buildLevelRatioInput(
17338
+ k,
17339
+ taken,
17340
+ (nextKey) => {
17341
+ this.emit({ levels: {
17342
+ [key]: null,
17343
+ [nextKey]: { visible: cb.checked, color }
17344
+ } });
17345
+ taken.delete(key);
17346
+ taken.add(nextKey);
17347
+ key = nextKey;
17348
+ }
17349
+ ) : this.buildLevelRatioLabel(k), swatch);
17239
17350
  }
17240
17351
  const anchorRect = anchor.getBoundingClientRect();
17241
17352
  const cRect = this.container.getBoundingClientRect();