acttrader-charts 1.4.0-beta.11 → 1.4.0-beta.13

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
@@ -223,6 +223,21 @@ chart.on('compareRemoved', ({ symbol }) => console.log('-', symbol));
223
223
  chart.on('compareError', ({ symbol, message }) => console.warn(symbol, message));
224
224
  ```
225
225
 
226
+ #### Saving and syncing compares
227
+
228
+ `getState().compares` is the ordered list of compare symbols, and `[]` once the
229
+ last one is removed (it is `undefined` only when no `compareDataLoader` is set).
230
+ Save the list as it is, including `[]`, so a removal reaches the trader's
231
+ other devices. `setState({ compares })` applies a saved list: `[]` clears every
232
+ compare, the current list is left as it is (no refetch), and a list applied
233
+ before the primary's first load waits as `loading` and is fetched with that
234
+ load instead of failing.
235
+
236
+ The % on each legend chip uses the same baseline as the lines: the compare's
237
+ close at the leftmost visible bar, measured at the last bar (or the hovered
238
+ bar). It updates as you scroll and zoom, so two screens showing different
239
+ ranges show different %.
240
+
226
241
  When any compare is active the Y-axis switches to percent (`+12.34%` /
227
242
  `-5.67%`); removing every compare returns it to absolute-price labels.
228
243
  The Compare button in the advanced toolbar opens an in-chart picker filtered
@@ -1595,6 +1610,22 @@ const chart = new ChartEngine({
1595
1610
  chart.setDrawingTool('icon'); // uses the last glyph picked
1596
1611
  ```
1597
1612
 
1613
+ #### Hosts with their own picker (native apps)
1614
+
1615
+ The panel above is part of the web drawing toolbar. A host that draws its own
1616
+ toolbar (the Android and iOS apps in native-UI mode) builds the picker from the
1617
+ catalog instead: the `icon` tool carries `glyphTabs`, each with a `kind`, a
1618
+ `label` and its `groups` (`id`, `name`, `tabIcon`, `glyphs`), the same list the
1619
+ web panel shows. On a tap, choose the glyph and then arm the tool:
1620
+
1621
+ ```ts
1622
+ chart.setIconGlyph('😍', 'sticker'); // kind: 'emoji' (default) | 'sticker' | 'icon'
1623
+ chart.setDrawingTool('icon');
1624
+ ```
1625
+
1626
+ Before `glyphTabs` and `setIconGlyph`, a host without the web toolbar had no
1627
+ list to show and every icon it placed was the default ⭐.
1628
+
1598
1629
  #### The picker
1599
1630
 
1600
1631
  Opening the **Icons & Emojis** group gives a panel, not a list of rows: a row per
@@ -323,10 +323,6 @@ interface ChartSettingsTemplate {
323
323
  }
324
324
  /** Mints a stable, collision-free id for a settings-template name. */
325
325
  declare function makeSettingsTemplateId(name: string, existingIds?: readonly string[]): string;
326
- /**
327
- * Deep-copies a settings snapshot one level past `canvasColors`, so a saved
328
- * template can never be mutated by later edits to the live chart (or vice versa).
329
- */
330
326
  declare function cloneSettings(s: ChartSettingsSnapshot): ChartSettingsSnapshot;
331
327
  /** Builds a template from a settings snapshot. */
332
328
  declare function captureSettingsTemplate(name: string, settings: ChartSettingsSnapshot, opts?: {
@@ -323,10 +323,6 @@ interface ChartSettingsTemplate {
323
323
  }
324
324
  /** Mints a stable, collision-free id for a settings-template name. */
325
325
  declare function makeSettingsTemplateId(name: string, existingIds?: readonly string[]): string;
326
- /**
327
- * Deep-copies a settings snapshot one level past `canvasColors`, so a saved
328
- * template can never be mutated by later edits to the live chart (or vice versa).
329
- */
330
326
  declare function cloneSettings(s: ChartSettingsSnapshot): ChartSettingsSnapshot;
331
327
  /** Builds a template from a settings snapshot. */
332
328
  declare function captureSettingsTemplate(name: string, settings: ChartSettingsSnapshot, opts?: {
package/dist/index.cjs CHANGED
@@ -27089,6 +27089,123 @@ var IndicatorSettingsDialog = class {
27089
27089
  }
27090
27090
  };
27091
27091
 
27092
+ // src/core/templateStore.ts
27093
+ function slugify(name) {
27094
+ return name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
27095
+ }
27096
+ function mintId(name, existingIds = [], fallback = "template") {
27097
+ const base = slugify(name) || fallback;
27098
+ if (!existingIds.includes(base)) return base;
27099
+ let n = 2;
27100
+ while (existingIds.includes(`${base}-${n}`)) n++;
27101
+ return `${base}-${n}`;
27102
+ }
27103
+ function upsertById(list, entry) {
27104
+ const idx = list.findIndex((e) => e.id === entry.id);
27105
+ if (idx === -1) return [...list, entry];
27106
+ const next = [...list];
27107
+ next[idx] = entry;
27108
+ return next;
27109
+ }
27110
+ function removeById(list, id) {
27111
+ return list.filter((e) => e.id !== id);
27112
+ }
27113
+ function findByIdOrName(list, idOrName) {
27114
+ const byId = list.find((e) => e.id === idOrName);
27115
+ if (byId) return byId;
27116
+ const slug = slugify(idOrName);
27117
+ return list.find((e) => slugify(e.name) === slug);
27118
+ }
27119
+
27120
+ // src/core/settingsTemplates.ts
27121
+ function makeSettingsTemplateId(name, existingIds = []) {
27122
+ return mintId(name, existingIds, "settings");
27123
+ }
27124
+ function cloneCanvasColors(c) {
27125
+ if (!c) return c;
27126
+ return {
27127
+ ...c,
27128
+ ...c.dark ? { dark: { ...c.dark } } : {},
27129
+ ...c.light ? { light: { ...c.light } } : {}
27130
+ };
27131
+ }
27132
+ function cloneSettings(s) {
27133
+ return {
27134
+ tradeDisplayFilter: s.tradeDisplayFilter,
27135
+ positionRenderStyle: s.positionRenderStyle,
27136
+ theme: s.theme,
27137
+ showTradeLevelsAlways: s.showTradeLevelsAlways,
27138
+ showPriceAxisCountdown: s.showPriceAxisCountdown,
27139
+ ...s.canvasColors ? {
27140
+ canvasColors: {
27141
+ ...s.canvasColors.dark ? { dark: { ...s.canvasColors.dark } } : {},
27142
+ ...s.canvasColors.light ? { light: { ...s.canvasColors.light } } : {}
27143
+ }
27144
+ } : {},
27145
+ // Each group is one level deep, so a spread copy is a full copy.
27146
+ ...s.statusLine ? { statusLine: { ...s.statusLine } } : {},
27147
+ ...s.scales ? { scales: { ...s.scales } } : {},
27148
+ ...s.canvas ? { canvas: { ...s.canvas } } : {},
27149
+ ...s.barColorSource ? { barColorSource: s.barColorSource } : {}
27150
+ };
27151
+ }
27152
+ function captureSettingsTemplate(name, settings, opts = {}) {
27153
+ return {
27154
+ id: opts.id ?? makeSettingsTemplateId(name, opts.existingIds ?? []),
27155
+ name: name.trim(),
27156
+ settings: cloneSettings(settings),
27157
+ ...opts.savedAt !== void 0 ? { savedAt: opts.savedAt } : {}
27158
+ };
27159
+ }
27160
+ function upsertSettingsTemplate(list, template) {
27161
+ return upsertById(list, template);
27162
+ }
27163
+ function removeSettingsTemplate(list, id) {
27164
+ return removeById(list, id);
27165
+ }
27166
+ function findSettingsTemplate(list, idOrName) {
27167
+ return findByIdOrName(list, idOrName);
27168
+ }
27169
+ function isGroup(v) {
27170
+ return typeof v === "object" && v !== null && !Array.isArray(v);
27171
+ }
27172
+ function sanitizeSettingsTemplates(input) {
27173
+ if (!Array.isArray(input)) return [];
27174
+ const out = [];
27175
+ for (const raw of input) {
27176
+ if (typeof raw !== "object" || raw === null) continue;
27177
+ const t = raw;
27178
+ if (typeof t.name !== "string" || t.name.trim() === "") continue;
27179
+ if (typeof t.settings !== "object" || t.settings === null) continue;
27180
+ const s = t.settings;
27181
+ out.push({
27182
+ id: typeof t.id === "string" && t.id !== "" ? t.id : makeSettingsTemplateId(
27183
+ t.name,
27184
+ out.map((x) => x.id)
27185
+ ),
27186
+ name: t.name.trim(),
27187
+ settings: {
27188
+ tradeDisplayFilter: s.tradeDisplayFilter ?? "all",
27189
+ positionRenderStyle: s.positionRenderStyle ?? "line",
27190
+ theme: s.theme === "light" ? "light" : "dark",
27191
+ showTradeLevelsAlways: s.showTradeLevelsAlways === true,
27192
+ showPriceAxisCountdown: s.showPriceAxisCountdown === true,
27193
+ ...typeof s.canvasColors === "object" && s.canvasColors !== null ? { canvasColors: s.canvasColors } : {},
27194
+ // Groups are carried through when present and dropped when not.
27195
+ // Absent is meaningful: it means "leave the chart as it is",
27196
+ // which is what an old template saved before these existed has
27197
+ // to mean if applying it is not to rearrange the chart.
27198
+ ...isGroup(s.statusLine) ? { statusLine: s.statusLine } : {},
27199
+ ...isGroup(s.scales) ? { scales: s.scales } : {},
27200
+ ...isGroup(s.canvas) ? { canvas: s.canvas } : {},
27201
+ ...s.barColorSource === "previousClose" || s.barColorSource === "open" ? { barColorSource: s.barColorSource } : {}
27202
+ },
27203
+ ...typeof t.savedAt === "number" ? { savedAt: t.savedAt } : {}
27204
+ });
27205
+ }
27206
+ return out;
27207
+ }
27208
+
27092
27209
  // src/ui/ChartSettingsDialog.ts
27093
27210
  var TAB_LABELS = {
27094
27211
  appearance: "Appearance",
@@ -27156,7 +27273,12 @@ var ChartSettingsDialog = class {
27156
27273
  display: "flex",
27157
27274
  flexDirection: "column",
27158
27275
  width: "540px",
27159
- minHeight: "340px",
27276
+ // One height for every tab. Sized to its content, the panel grew
27277
+ // and shrank as you switched tabs (Appearance tall, Status Line
27278
+ // short) and the footer jumped about; a long tab now scrolls
27279
+ // inside the content area instead.
27280
+ height: "min(620px, calc(100vh - 48px))",
27281
+ minHeight: "min(340px, calc(100vh - 48px))",
27160
27282
  maxWidth: "calc(100vw - 32px)",
27161
27283
  maxHeight: "calc(100vh - 48px)",
27162
27284
  background: theme.tooltip.background,
@@ -27193,7 +27315,7 @@ var ChartSettingsDialog = class {
27193
27315
  }
27194
27316
  show(current, onChange, onReset) {
27195
27317
  const wasVisible = this.isVisible;
27196
- this.current = { ...current };
27318
+ this.current = { ...current, canvasColors: cloneCanvasColors(current.canvasColors) };
27197
27319
  this.onChange = onChange;
27198
27320
  this.onReset = onReset ?? null;
27199
27321
  this.renderContent();
@@ -27253,7 +27375,7 @@ var ChartSettingsDialog = class {
27253
27375
  }
27254
27376
  // ── Private – emit ────────────────────────────────────────────────────────
27255
27377
  emit() {
27256
- this.onChange?.({ ...this.current });
27378
+ this.onChange?.({ ...this.current, canvasColors: cloneCanvasColors(this.current.canvasColors) });
27257
27379
  }
27258
27380
  // ── Private – panel theme ─────────────────────────────────────────────────
27259
27381
  applyPanelTheme() {
@@ -28033,9 +28155,11 @@ var ChartSettingsDialog = class {
28033
28155
  }
28034
28156
  ];
28035
28157
  const makeSwatch = (def) => this.makeColorSwatch(current[def.field] ?? def.fallback, (hex) => {
28036
- if (!this.current.canvasColors) this.current.canvasColors = {};
28037
- if (!this.current.canvasColors[themeKey]) this.current.canvasColors[themeKey] = {};
28038
- this.current.canvasColors[themeKey][def.field] = hex;
28158
+ const cc = this.current.canvasColors ?? {};
28159
+ this.current.canvasColors = {
28160
+ ...cc,
28161
+ [themeKey]: { ...cc[themeKey] ?? {}, [def.field]: hex }
28162
+ };
28039
28163
  this.emit();
28040
28164
  });
28041
28165
  for (const { label, s1, s2 } of rows) {
@@ -30734,6 +30858,25 @@ var CompareManager = class {
30734
30858
  });
30735
30859
  return;
30736
30860
  }
30861
+ const entry = this.createEntry(symbol);
30862
+ await this.fetchOne(entry, this.lastParams, this.generation);
30863
+ }
30864
+ /** True once the primary has a loaded range that `add()` can fetch against. */
30865
+ hasRange() {
30866
+ return this.lastParams !== null;
30867
+ }
30868
+ /**
30869
+ * Add a compare symbol as `loading` without fetching. Used when restoring a
30870
+ * saved list before the primary's first load: the next `refetchAll()` (run
30871
+ * when the primary range arrives) loads it. No-op for a duplicate, an empty
30872
+ * symbol, or past `maxCompares`.
30873
+ */
30874
+ addPending(symbol) {
30875
+ if (!symbol || this.entries.has(symbol)) return;
30876
+ if (this.entries.size >= this.maxCompares) return;
30877
+ this.createEntry(symbol);
30878
+ }
30879
+ createEntry(symbol) {
30737
30880
  const color = this.assignColor();
30738
30881
  const entry = {
30739
30882
  symbol,
@@ -30744,7 +30887,7 @@ var CompareManager = class {
30744
30887
  this.entries.set(symbol, entry);
30745
30888
  this.order.push(symbol);
30746
30889
  this.emitter.emit("compareAdded", { symbol, color });
30747
- await this.fetchOne(entry, this.lastParams, this.generation);
30890
+ return entry;
30748
30891
  }
30749
30892
  remove(symbol) {
30750
30893
  if (!this.entries.has(symbol)) return;
@@ -32015,34 +32158,6 @@ function formatMeasureDate(time, timeframe, timezone) {
32015
32158
  return `${date} ${pick(cp, "hour")}:${pick(cp, "minute")}`;
32016
32159
  }
32017
32160
 
32018
- // src/core/templateStore.ts
32019
- function slugify(name) {
32020
- return name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
32021
- }
32022
- function mintId(name, existingIds = [], fallback = "template") {
32023
- const base = slugify(name) || fallback;
32024
- if (!existingIds.includes(base)) return base;
32025
- let n = 2;
32026
- while (existingIds.includes(`${base}-${n}`)) n++;
32027
- return `${base}-${n}`;
32028
- }
32029
- function upsertById(list, entry) {
32030
- const idx = list.findIndex((e) => e.id === entry.id);
32031
- if (idx === -1) return [...list, entry];
32032
- const next = [...list];
32033
- next[idx] = entry;
32034
- return next;
32035
- }
32036
- function removeById(list, id) {
32037
- return list.filter((e) => e.id !== id);
32038
- }
32039
- function findByIdOrName(list, idOrName) {
32040
- const byId = list.find((e) => e.id === idOrName);
32041
- if (byId) return byId;
32042
- const slug = slugify(idOrName);
32043
- return list.find((e) => slugify(e.name) === slug);
32044
- }
32045
-
32046
32161
  // src/core/savedLayouts.ts
32047
32162
  function makeLayoutId(name, existingIds = []) {
32048
32163
  return mintId(name, existingIds, "layout");
@@ -32097,87 +32212,6 @@ function sanitizeLayouts(input) {
32097
32212
  return out;
32098
32213
  }
32099
32214
 
32100
- // src/core/settingsTemplates.ts
32101
- function makeSettingsTemplateId(name, existingIds = []) {
32102
- return mintId(name, existingIds, "settings");
32103
- }
32104
- function cloneSettings(s) {
32105
- return {
32106
- tradeDisplayFilter: s.tradeDisplayFilter,
32107
- positionRenderStyle: s.positionRenderStyle,
32108
- theme: s.theme,
32109
- showTradeLevelsAlways: s.showTradeLevelsAlways,
32110
- showPriceAxisCountdown: s.showPriceAxisCountdown,
32111
- ...s.canvasColors ? {
32112
- canvasColors: {
32113
- ...s.canvasColors.dark ? { dark: { ...s.canvasColors.dark } } : {},
32114
- ...s.canvasColors.light ? { light: { ...s.canvasColors.light } } : {}
32115
- }
32116
- } : {},
32117
- // Each group is one level deep, so a spread copy is a full copy.
32118
- ...s.statusLine ? { statusLine: { ...s.statusLine } } : {},
32119
- ...s.scales ? { scales: { ...s.scales } } : {},
32120
- ...s.canvas ? { canvas: { ...s.canvas } } : {},
32121
- ...s.barColorSource ? { barColorSource: s.barColorSource } : {}
32122
- };
32123
- }
32124
- function captureSettingsTemplate(name, settings, opts = {}) {
32125
- return {
32126
- id: opts.id ?? makeSettingsTemplateId(name, opts.existingIds ?? []),
32127
- name: name.trim(),
32128
- settings: cloneSettings(settings),
32129
- ...opts.savedAt !== void 0 ? { savedAt: opts.savedAt } : {}
32130
- };
32131
- }
32132
- function upsertSettingsTemplate(list, template) {
32133
- return upsertById(list, template);
32134
- }
32135
- function removeSettingsTemplate(list, id) {
32136
- return removeById(list, id);
32137
- }
32138
- function findSettingsTemplate(list, idOrName) {
32139
- return findByIdOrName(list, idOrName);
32140
- }
32141
- function isGroup(v) {
32142
- return typeof v === "object" && v !== null && !Array.isArray(v);
32143
- }
32144
- function sanitizeSettingsTemplates(input) {
32145
- if (!Array.isArray(input)) return [];
32146
- const out = [];
32147
- for (const raw of input) {
32148
- if (typeof raw !== "object" || raw === null) continue;
32149
- const t = raw;
32150
- if (typeof t.name !== "string" || t.name.trim() === "") continue;
32151
- if (typeof t.settings !== "object" || t.settings === null) continue;
32152
- const s = t.settings;
32153
- out.push({
32154
- id: typeof t.id === "string" && t.id !== "" ? t.id : makeSettingsTemplateId(
32155
- t.name,
32156
- out.map((x) => x.id)
32157
- ),
32158
- name: t.name.trim(),
32159
- settings: {
32160
- tradeDisplayFilter: s.tradeDisplayFilter ?? "all",
32161
- positionRenderStyle: s.positionRenderStyle ?? "line",
32162
- theme: s.theme === "light" ? "light" : "dark",
32163
- showTradeLevelsAlways: s.showTradeLevelsAlways === true,
32164
- showPriceAxisCountdown: s.showPriceAxisCountdown === true,
32165
- ...typeof s.canvasColors === "object" && s.canvasColors !== null ? { canvasColors: s.canvasColors } : {},
32166
- // Groups are carried through when present and dropped when not.
32167
- // Absent is meaningful: it means "leave the chart as it is",
32168
- // which is what an old template saved before these existed has
32169
- // to mean if applying it is not to rearrange the chart.
32170
- ...isGroup(s.statusLine) ? { statusLine: s.statusLine } : {},
32171
- ...isGroup(s.scales) ? { scales: s.scales } : {},
32172
- ...isGroup(s.canvas) ? { canvas: s.canvas } : {},
32173
- ...s.barColorSource === "previousClose" || s.barColorSource === "open" ? { barColorSource: s.barColorSource } : {}
32174
- },
32175
- ...typeof t.savedAt === "number" ? { savedAt: t.savedAt } : {}
32176
- });
32177
- }
32178
- return out;
32179
- }
32180
-
32181
32215
  // src/core/indicatorTemplates.ts
32182
32216
  function makeTemplateId(name, existingIds = []) {
32183
32217
  return mintId(name, existingIds);
@@ -37547,6 +37581,8 @@ var _ChartEngine = class _ChartEngine {
37547
37581
  this.valueTooltipEnabled = false;
37548
37582
  this.valueTooltip = null;
37549
37583
  this.compareLegend = null;
37584
+ /** Inputs the compare chips were last computed from (see syncCompareLegend). */
37585
+ this._compareLegendKey = "";
37550
37586
  this.comparePickerModal = null;
37551
37587
  this.compareDataLoader = null;
37552
37588
  /** Crosshair-hovered bar index used to drive live compare percent values. */
@@ -39326,7 +39362,7 @@ var _ChartEngine = class _ChartEngine {
39326
39362
  this.tradeDisplayFilter = config.tradeDisplayFilter;
39327
39363
  }
39328
39364
  if (config.canvasColors) {
39329
- this.settingsCanvasColors = config.canvasColors;
39365
+ this.settingsCanvasColors = cloneCanvasColors(config.canvasColors);
39330
39366
  }
39331
39367
  this.recomputeRenderStyle();
39332
39368
  this.showBidAskLines = config.showBidAskLines ?? false;
@@ -41851,7 +41887,7 @@ var _ChartEngine = class _ChartEngine {
41851
41887
  * like the dialog's own picks, and `stateChange` fires.
41852
41888
  */
41853
41889
  setCanvasColors(colors) {
41854
- this.settingsCanvasColors = colors ?? {};
41890
+ this.settingsCanvasColors = cloneCanvasColors(colors ?? {});
41855
41891
  this.setTheme(this.themeName);
41856
41892
  return this;
41857
41893
  }
@@ -42266,11 +42302,15 @@ var _ChartEngine = class _ChartEngine {
42266
42302
  })),
42267
42303
  positionRenderStyle: this.baseRenderStyle(),
42268
42304
  tradeDisplayFilter: this.tradeDisplayFilter,
42269
- canvasColors: Object.keys(this.settingsCanvasColors).length > 0 ? { ...this.settingsCanvasColors } : void 0,
42305
+ canvasColors: Object.keys(this.settingsCanvasColors).length > 0 ? cloneCanvasColors(this.settingsCanvasColors) : void 0,
42270
42306
  tfcEnabled: this.tfcActive,
42271
42307
  viewport: this.getViewportTimestamps(),
42272
42308
  priceScale: this.priceScaleFactor !== 1 || this.pricePanOffset !== 0 ? { factor: this.priceScaleFactor, panOffset: this.pricePanOffset } : void 0,
42273
- compares: this.compareManager && this.compareManager.isActive() ? this.compareManager.list().map((e) => e.symbol) : void 0
42309
+ // Always a list when compare is available — `[]` once the last one
42310
+ // is removed. Hosts merge the saved block over the stored one, so
42311
+ // leaving the key out kept the old list on the server and the
42312
+ // compares came back on the trader's other devices.
42313
+ compares: this.compareManager ? this.compareManager.list().map((e) => e.symbol) : void 0
42274
42314
  };
42275
42315
  }
42276
42316
  /**
@@ -42365,7 +42405,7 @@ var _ChartEngine = class _ChartEngine {
42365
42405
  this.tradeDisplayFilter = state.tradeDisplayFilter;
42366
42406
  }
42367
42407
  if (state.canvasColors) {
42368
- this.settingsCanvasColors = state.canvasColors;
42408
+ this.settingsCanvasColors = cloneCanvasColors(state.canvasColors);
42369
42409
  }
42370
42410
  if (state.tfcEnabled !== void 0 && this.tfcEnabled) {
42371
42411
  this.tfcActive = state.tfcEnabled;
@@ -42390,12 +42430,26 @@ var _ChartEngine = class _ChartEngine {
42390
42430
  };
42391
42431
  }
42392
42432
  }
42393
- if (state.compares && state.compares.length > 0 && this.compareManager) {
42394
- this.compareManager.clear();
42395
- for (const sym of state.compares) {
42396
- void this.compareManager.add(sym);
42433
+ if (Array.isArray(state.compares) && this.compareManager) {
42434
+ const cm = this.compareManager;
42435
+ const current = cm.list().map((e) => e.symbol);
42436
+ const same = current.length === state.compares.length && current.every((s, i) => s === state.compares[i]);
42437
+ if (!same) {
42438
+ cm.clear();
42439
+ for (const sym of state.compares) {
42440
+ if (cm.hasRange()) {
42441
+ void cm.add(sym).then(() => {
42442
+ this.recomputeIndicators();
42443
+ this.refreshCompareLegend();
42444
+ this.scheduleRender();
42445
+ });
42446
+ } else {
42447
+ cm.addPending(sym);
42448
+ }
42449
+ }
42450
+ this.recomputeIndicators();
42451
+ this.refreshCompareLegend();
42397
42452
  }
42398
- this.refreshCompareLegend();
42399
42453
  }
42400
42454
  } finally {
42401
42455
  this.isRestoringState = false;
@@ -42957,6 +43011,20 @@ var _ChartEngine = class _ChartEngine {
42957
43011
  console.warn("[acttrader-charts] setDraftOrderLabels() is deprecated and is a no-op. DraftOrderQtyInput has been removed.");
42958
43012
  return this;
42959
43013
  }
43014
+ /**
43015
+ * Choose the glyph the next `icon` drawing places — what a tap in the
43016
+ * Emojis / Stickers / Icons picker does. For hosts that draw their own
43017
+ * picker from the catalog's `glyphTabs` (the native apps): call this, then
43018
+ * `setDrawingTool('icon')`. `kind` sets the size and whether the drawing
43019
+ * colour applies (stickers are larger; icons take the colour). An empty
43020
+ * glyph is ignored.
43021
+ */
43022
+ setIconGlyph(glyph, kind = "emoji") {
43023
+ if (typeof glyph !== "string" || glyph.length === 0) return this;
43024
+ this.pendingIconGlyph = glyph;
43025
+ this.pendingIconKind = kind === "sticker" || kind === "icon" ? kind : "emoji";
43026
+ return this;
43027
+ }
42960
43028
  setDrawingTool(tool) {
42961
43029
  this.drawingManager.setActiveTool(tool);
42962
43030
  this.leftBar?.setActiveTool(tool);
@@ -44501,7 +44569,7 @@ var _ChartEngine = class _ChartEngine {
44501
44569
  theme: this.themeName,
44502
44570
  showTradeLevelsAlways: this.showTradeLevelsAlways,
44503
44571
  showPriceAxisCountdown: this.showPriceAxisCountdown,
44504
- canvasColors: { ...this.settingsCanvasColors },
44572
+ canvasColors: cloneCanvasColors(this.settingsCanvasColors),
44505
44573
  // The resolved sets, not the sparse ones. A template is a thing
44506
44574
  // the user means to reproduce exactly; saving the gaps would make
44507
44575
  // it reproduce whatever the *next* chart's defaults happen to be.
@@ -44525,7 +44593,7 @@ var _ChartEngine = class _ChartEngine {
44525
44593
  }
44526
44594
  this.positionRenderStyle = settings.positionRenderStyle;
44527
44595
  if (settings.canvasColors) {
44528
- this.settingsCanvasColors = settings.canvasColors;
44596
+ this.settingsCanvasColors = cloneCanvasColors(settings.canvasColors);
44529
44597
  }
44530
44598
  if (this.showTradeLevelsAlways !== settings.showTradeLevelsAlways) {
44531
44599
  this.showTradeLevelsAlways = settings.showTradeLevelsAlways;
@@ -45570,6 +45638,31 @@ var _ChartEngine = class _ChartEngine {
45570
45638
  });
45571
45639
  this.compareLegend.update(states);
45572
45640
  }
45641
+ /**
45642
+ * Called from render while comparing. The lines re-base to the leftmost
45643
+ * visible bar every frame; the chips used to be computed only on
45644
+ * add/remove/crosshair, so after a scroll, zoom, new bar or refetch they
45645
+ * showed a % from an older view. Recompute them whenever anything the
45646
+ * chip % depends on has changed.
45647
+ */
45648
+ syncCompareLegend(baselineIdx, entries) {
45649
+ const all = this.dataStore.all;
45650
+ const last = all[all.length - 1];
45651
+ const key = [
45652
+ baselineIdx,
45653
+ all.length,
45654
+ last?.time,
45655
+ last?.close,
45656
+ this.compareHoveredIndex,
45657
+ ...entries.map((e) => {
45658
+ const b = e.bars[e.bars.length - 1];
45659
+ return `${e.symbol}:${e.status}:${e.bars.length}:${b?.time}:${b?.close}`;
45660
+ })
45661
+ ].join("|");
45662
+ if (key === this._compareLegendKey) return;
45663
+ this._compareLegendKey = key;
45664
+ this.refreshCompareLegend();
45665
+ }
45573
45666
  /** Returns the index of the leftmost visible bar in the primary store,
45574
45667
  * used as the baseline anchor for percent-change normalization. */
45575
45668
  compareBaselineIndex(primaryBars) {
@@ -45683,6 +45776,7 @@ var _ChartEngine = class _ChartEngine {
45683
45776
  (e) => e.status === "ready" && e.bars.length > 0 ? buildComparePctSeries(e.bars, primaryTimes, baselineIdx ?? 0) : []
45684
45777
  );
45685
45778
  compareColors = entries.map((e) => e.color);
45779
+ this.syncCompareLegend(baselineIdx, entries);
45686
45780
  bars = transformBarsToPct(absBars, baseline);
45687
45781
  pctTargetRange = combinedPercentRange(
45688
45782
  absBars,