@dnax/ui 0.0.66 → 0.0.69

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.
@@ -186,6 +186,10 @@ import {
186
186
  ISO_DATE,
187
187
  isoAddDays,
188
188
  shiftFormulaRefs,
189
+ functionQueryAt,
190
+ rankSuggestions,
191
+ functionCallAt,
192
+ signatureArgs,
189
193
  csvEscape,
190
194
  deriveCols,
191
195
  parseCsv,
@@ -194,6 +198,7 @@ import { checkValidation, type ArkCheck } from "../lib/spreadsheetValidation"
194
198
  import { normalizeCellOptions } from "../lib/spreadsheetOptions"
195
199
  import { inLockRect, lockRects, prefillForRow } from "../lib/spreadsheetZones"
196
200
  import { canAddRows, canRemoveRows, clampRows } from "../lib/spreadsheetRows"
201
+ import { createHistory } from "../lib/history"
197
202
  import {
198
203
  DEFAULT_ROW_KEY,
199
204
  diffDocuments,
@@ -349,6 +354,9 @@ const I18N = {
349
354
  sheet: "Sheet",
350
355
  fxPlaceholder: "Value or formula (=SUM(A1:A3)…)",
351
356
  fxNoSel: "Select a cell…",
357
+ fxSyntax: "Syntax error",
358
+ fxEmpty: "empty",
359
+ fxColumn: "column",
352
360
  cut: "Cut",
353
361
  copy: "Copy",
354
362
  paste: "Paste",
@@ -427,6 +435,9 @@ const I18N = {
427
435
  sheet: "Feuille",
428
436
  fxPlaceholder: "Valeur ou formule (=SOMME(A1:A3)…)",
429
437
  fxNoSel: "Sélectionnez une cellule…",
438
+ fxSyntax: "Erreur de syntaxe",
439
+ fxEmpty: "vide",
440
+ fxColumn: "colonne",
430
441
  cut: "Couper",
431
442
  copy: "Copier",
432
443
  paste: "Coller",
@@ -1069,11 +1080,13 @@ const coerceValue = (col: QSpreadsheetColumn | undefined, text: unknown, old: an
1069
1080
  const raw = String(text ?? "")
1070
1081
  const t = raw.trim()
1071
1082
  if (t === "") return null
1072
- // Formule A1 : conservée brute dans la cellule (booléen/choix exclus)
1083
+ // Formule A1 : conservée brute dans la cellule (booléen/choix exclus) —
1084
+ // `t` (déjà trimé) évite qu'un `" =…"` ne soit traité comme une formule par
1085
+ // `isFormulaRaw` tout en échouant au parsing du moteur.
1073
1086
  if (t.startsWith("=")) {
1074
1087
  if (col?.type === "boolean" || col?.type === "select" || col?.type === "multiselect")
1075
1088
  return old
1076
- return raw
1089
+ return t
1077
1090
  }
1078
1091
  if (col?.type === "number") {
1079
1092
  const n = Number(raw)
@@ -1136,12 +1149,14 @@ const clearCell = (row: number, column: string) => {
1136
1149
  if (isCellLocked(row, column)) return
1137
1150
  const old = state.value[row]?.[column]
1138
1151
  if (isBlankValue(old) || (Array.isArray(old) && old.length === 0)) return
1152
+ pushHistory()
1139
1153
  setCellValue(row, column, old, null)
1140
1154
  }
1141
1155
 
1142
1156
  const toggleBoolean = (row: number, column: string) => {
1143
1157
  if (isCellLocked(row, column)) return
1144
1158
  const old = !!state.value[row]?.[column]
1159
+ pushHistory()
1145
1160
  setCellValue(row, column, old, !old)
1146
1161
  }
1147
1162
 
@@ -1246,9 +1261,7 @@ interface Snapshot {
1246
1261
  merges: MergeRange[]
1247
1262
  rules: CondRule[]
1248
1263
  }
1249
- const undoStack: Snapshot[] = []
1250
- const redoStack: Snapshot[] = []
1251
-
1264
+ const history = createHistory<Snapshot>(100)
1252
1265
  const takeSnapshot = (): Snapshot => ({
1253
1266
  rows: state.value.map((r) => ({ ...r })),
1254
1267
  cols: cols.value.map((c) => ({ ...c })),
@@ -1261,13 +1274,9 @@ const takeSnapshot = (): Snapshot => ({
1261
1274
  merges: merges.value.map((m) => ({ ...m })),
1262
1275
  rules: condRules.value.map((r) => ({ ...r })),
1263
1276
  })
1264
- const pushHistory = () => {
1265
- undoStack.push(takeSnapshot())
1266
- if (undoStack.length > 100) undoStack.shift()
1267
- redoStack.length = 0
1268
- }
1269
- const canUndo = computed(() => undoStack.length > 0 && !props.readonly && !props.disable)
1270
- const canRedo = computed(() => redoStack.length > 0 && !props.readonly && !props.disable)
1277
+ const pushHistory = () => history.push(takeSnapshot())
1278
+ const canUndo = computed(() => history.canUndo.value && !props.readonly && !props.disable)
1279
+ const canRedo = computed(() => history.canRedo.value && !props.readonly && !props.disable)
1271
1280
 
1272
1281
  const restoreSnapshot = (s: Snapshot) => {
1273
1282
  cols.value = s.cols.map((c) => ({ ...c }))
@@ -1282,17 +1291,35 @@ const restoreSnapshot = (s: Snapshot) => {
1282
1291
  condRules.value = s.rules.map((r) => ({ ...r }))
1283
1292
  emit("update:columns", cols.value)
1284
1293
  pushRows(state.value)
1294
+ // La sélection peut pointer sur une ligne/colonne qui n'existe plus (redo d'une
1295
+ // suppression) : on la ramène sur la première cellule plutôt que de laisser un
1296
+ // curseur fantôme hors grille.
1297
+ const current = sel.value
1298
+ if (current) {
1299
+ const maxRow = state.value.length - 1
1300
+ const bad =
1301
+ colIndex(current.column) === -1 ||
1302
+ (current.endColumn !== undefined && colIndex(current.endColumn) === -1) ||
1303
+ current.row > maxRow ||
1304
+ (current.endRow !== undefined && current.endRow > maxRow)
1305
+ if (bad) {
1306
+ if (state.value.length && cols.value.length) select(0, cols.value[0]!.name)
1307
+ else setSel(null)
1308
+ }
1309
+ }
1285
1310
  }
1286
1311
  const undo = () => {
1287
1312
  if (!canUndo.value) return
1288
- redoStack.push(takeSnapshot())
1289
- restoreSnapshot(undoStack.pop()!)
1313
+ const snapshot = history.undo(takeSnapshot())
1314
+ if (!snapshot) return
1315
+ restoreSnapshot(snapshot)
1290
1316
  emit("structure-change", { rows: state.value, columns: cols.value, reason: "undo" })
1291
1317
  }
1292
1318
  const redo = () => {
1293
1319
  if (!canRedo.value) return
1294
- undoStack.push(takeSnapshot())
1295
- restoreSnapshot(redoStack.pop()!)
1320
+ const snapshot = history.redo(takeSnapshot())
1321
+ if (!snapshot) return
1322
+ restoreSnapshot(snapshot)
1296
1323
  emit("structure-change", { rows: state.value, columns: cols.value, reason: "redo" })
1297
1324
  }
1298
1325
 
@@ -1924,6 +1951,41 @@ const fxCanEdit = computed(() => {
1924
1951
  )
1925
1952
  })
1926
1953
 
1954
+ /**
1955
+ * Aperçu **live** du résultat de la formule en cours de saisie dans la barre fx.
1956
+ * Évaluée dans le contexte de la ligne sélectionnée (références par nom de colonne
1957
+ * et A1 résolues sur la grille mémorisée). `null` si la saisie n'est pas une formule.
1958
+ */
1959
+ const fxPreview = computed<{ text: string; error: boolean; hint: string } | null>(() => {
1960
+ // Pendant l'édition en cellule, la grille affiche déjà le résultat.
1961
+ if (editing.value || !sel.value || !fxCanEdit.value) return null
1962
+ const source = fxDraft.value
1963
+ if (!isFormulaRaw(source)) return null
1964
+ const row = sel.value.row
1965
+ const value = evaluateFormula(source, {
1966
+ row,
1967
+ resolveField: (name) => {
1968
+ const i = colIndex(name)
1969
+ return i === -1 ? undefined : evaluated.value[row]?.[i]
1970
+ },
1971
+ resolveCell: (r, c) => evaluated.value[r]?.[c],
1972
+ })
1973
+ if (isError(value)) {
1974
+ return {
1975
+ text: value.code,
1976
+ error: true,
1977
+ hint: value.code === "#ERROR!" ? t("fxSyntax") : value.code,
1978
+ }
1979
+ }
1980
+ if (value === null || value === undefined || value === "") {
1981
+ return { text: t("fxEmpty"), error: false, hint: "" }
1982
+ }
1983
+ const text = typeof value === "number"
1984
+ ? new Intl.NumberFormat(lang.value, { maximumFractionDigits: 6 }).format(value)
1985
+ : String(value)
1986
+ return { text: text.length > 60 ? text.slice(0, 59) + "…" : text, error: false, hint: "" }
1987
+ })
1988
+
1927
1989
  const commitFx = async () => {
1928
1990
  if (!sel.value) return
1929
1991
  if (!fxCanEdit.value) {
@@ -1939,7 +2001,21 @@ const commitFx = async () => {
1939
2001
  }
1940
2002
 
1941
2003
  const onFxInput = (e: Event) => {
1942
- fxDraft.value = (e.target as HTMLInputElement).value
2004
+ const el = e.target as HTMLInputElement
2005
+ fxDraft.value = el.value
2006
+ fxCaret.value = el.selectionStart
2007
+ }
2008
+
2009
+ /** Position du curseur depuis un événement (`null` si indisponible). */
2010
+ const caretFrom = (e: Event): number | null => {
2011
+ const el = e.target as HTMLInputElement | HTMLTextAreaElement | null
2012
+ return el && typeof el.selectionStart === "number" ? el.selectionStart : null
2013
+ }
2014
+ const syncFxCaret = (e: Event) => {
2015
+ fxCaret.value = caretFrom(e)
2016
+ }
2017
+ const syncEditCaret = (e: Event) => {
2018
+ editCaret.value = caretFrom(e)
1943
2019
  }
1944
2020
 
1945
2021
  const onFxKeydown = (e: KeyboardEvent) => {
@@ -2084,16 +2160,38 @@ const FORMULA_FNS: { name: string; sig: string }[] = [
2084
2160
  // La source alimente des méthodes de chaîne (`.trim()`, `.startsWith()`) :
2085
2161
  // la valeur brute d'une cellule peut être un nombre / booléen → on coerce.
2086
2162
  const fxSource = computed(() => String((editing.value ? draft.value : fxDraft.value) ?? ""))
2087
- const fxQuery = computed(() => {
2088
- const s = fxSource.value
2089
- if (!s.trim().startsWith("=")) return ""
2090
- const m = s.match(/[A-Za-z]{1,}$/)
2091
- return m ? m[0].toLowerCase() : ""
2092
- })
2163
+ /** Position du curseur (fx bar / éditeur de cellule) — pilote les suggestions. */
2164
+ const fxCaret = ref<number | null>(null)
2165
+ const editCaret = ref<number | null>(null)
2166
+ const fxQuery = computed(() =>
2167
+ functionQueryAt(
2168
+ fxSource.value,
2169
+ (editing.value ? editCaret.value : fxCaret.value) ?? undefined,
2170
+ ),
2171
+ )
2093
2172
  const fxSuggestions = computed(() => {
2094
2173
  const q = fxQuery.value
2095
2174
  if (!q) return []
2096
- return FORMULA_FNS.filter((f) => f.name.toLowerCase().startsWith(q)).slice(0, 12)
2175
+ const fns = rankSuggestions(FORMULA_FNS, q, 8).map((f) => ({
2176
+ kind: "fn" as const,
2177
+ name: f.name,
2178
+ detail: f.sig,
2179
+ insert: f.name + "(",
2180
+ }))
2181
+ // Colonnes de la feuille : les formules acceptent `=price * qty`
2182
+ const fields = rankSuggestions(
2183
+ cols.value.map((col) => ({
2184
+ name: col.name,
2185
+ detail:
2186
+ col.label !== undefined && String(col.label) !== col.name
2187
+ ? String(col.label)
2188
+ : t("fxColumn"),
2189
+ insert: col.name,
2190
+ })),
2191
+ q,
2192
+ 4,
2193
+ ).map((f) => ({ kind: "field" as const, ...f }))
2194
+ return [...fns, ...fields]
2097
2195
  })
2098
2196
  const fxHide = ref(false)
2099
2197
  const fxActive = ref(0)
@@ -2102,6 +2200,23 @@ watch(fxQuery, () => {
2102
2200
  fxActive.value = 0
2103
2201
  fxHide.value = false
2104
2202
  })
2203
+ /** Signature de la fonction au curseur (argument courant surligné). */
2204
+ const fxHint = computed<{ name: string; args: string[]; active: number } | null>(() => {
2205
+ const source = fxSource.value
2206
+ if (!isFormulaRaw(source)) return null
2207
+ const caret = Math.min(
2208
+ (editing.value ? editCaret.value : fxCaret.value) ?? source.length,
2209
+ source.length,
2210
+ )
2211
+ const call = functionCallAt(source, caret, FORMULA_FNS.map((f) => f.name))
2212
+ if (!call) return null
2213
+ const fn = FORMULA_FNS.find((f) => f.name.toLowerCase() === call.name)
2214
+ if (!fn) return null
2215
+ const args = signatureArgs(fn.sig)
2216
+ if (!args.length) return null
2217
+ return { name: fn.name, args, active: Math.min(call.index, args.length - 1) }
2218
+ })
2219
+
2105
2220
  const fxPopStyle = ref<Record<string, string>>({})
2106
2221
  const refreshFxPop = () => {
2107
2222
  if (!fxOpen.value || !fxInputEl.value) return
@@ -2123,9 +2238,18 @@ const stepFx = (d: number) => {
2123
2238
  const acceptFx = () => {
2124
2239
  const f = fxSuggestions.value[fxActive.value]
2125
2240
  if (!f) return
2126
- const replaceTail = (s: string) => s.replace(/[A-Za-z]+$/, f.name + "(")
2127
- if (editing.value) draft.value = replaceTail(String(draft.value ?? ""))
2128
- else fxDraft.value = replaceTail(String(fxDraft.value ?? ""))
2241
+ const source = editing.value ? String(draft.value ?? "") : String(fxDraft.value ?? "")
2242
+ const caret = Math.min(
2243
+ (editing.value ? editCaret.value : fxCaret.value) ?? source.length,
2244
+ source.length,
2245
+ )
2246
+ const query = functionQueryAt(source, caret)
2247
+ if (!query) return
2248
+ const start = caret - query.length
2249
+ const inserted = f.insert
2250
+ const next = source.slice(0, start) + inserted + source.slice(caret)
2251
+ if (editing.value) draft.value = next
2252
+ else fxDraft.value = next
2129
2253
  fxActive.value = 0
2130
2254
  nextTick(() => {
2131
2255
  const el = editing.value
@@ -2133,7 +2257,12 @@ const acceptFx = () => {
2133
2257
  ? editAreaEl.value
2134
2258
  : editInputEl.value
2135
2259
  : fxInputEl.value
2136
- el?.focus({ preventScroll: true })
2260
+ if (!el) return
2261
+ el.focus({ preventScroll: true })
2262
+ const pos = start + inserted.length
2263
+ el.setSelectionRange(pos, pos)
2264
+ if (editing.value) editCaret.value = pos
2265
+ else fxCaret.value = pos
2137
2266
  })
2138
2267
  }
2139
2268
  const acceptFxAt = (i: number) => {
@@ -2960,8 +3089,8 @@ const applyFill = () => {
2960
3089
  const sr = b.r0 + (((rr - b.r0) % h) + h) % h
2961
3090
  const sc = b.c0 + (((cc - b.c0) % w) + w) % w
2962
3091
  let raw = state.value[sr]?.[colNameAt(sc)]
2963
- if (typeof raw === "string" && raw.startsWith("=")) {
2964
- raw = shiftFormulaRefs(raw, rr - sr, cc - sc)
3092
+ if (typeof raw === "string" && raw.trim().startsWith("=")) {
3093
+ raw = shiftFormulaRefs(raw.trim(), rr - sr, cc - sc)
2965
3094
  }
2966
3095
  val = raw
2967
3096
  }
@@ -3239,8 +3368,7 @@ const loadSheetIntoEngine = (idx: number) => {
3239
3368
  colWidths.value = { ...ext.widths }
3240
3369
  setSel(null)
3241
3370
  editing.value = null
3242
- undoStack.length = 0
3243
- redoStack.length = 0
3371
+ history.clear()
3244
3372
  if (scrollEl.value) scrollEl.value.scrollTop = 0
3245
3373
  }
3246
3374
  /**
@@ -3652,7 +3780,10 @@ const findReplaceCurrent = () => {
3652
3780
  const next = findCase.value
3653
3781
  ? raw.split(findQuery.value).join(findReplaceText.value)
3654
3782
  : raw.replace(new RegExp(escapeRegExp(findQuery.value), "gi"), findReplaceText.value)
3655
- if (next !== raw) setCellValue(m.r, m.name, raw, next)
3783
+ if (next !== raw) {
3784
+ pushHistory()
3785
+ setCellValue(m.r, m.name, raw, next)
3786
+ }
3656
3787
  findScan()
3657
3788
  findNext(1)
3658
3789
  }
@@ -4192,6 +4323,7 @@ const validateAndSet = async (r: number, name: string, old: any, next: any): Pro
4192
4323
  return false
4193
4324
  }
4194
4325
  clearValError(r, name)
4326
+ pushHistory()
4195
4327
  setCellValue(r, name, old, next)
4196
4328
  return true
4197
4329
  }
@@ -4572,9 +4704,23 @@ defineExpose({
4572
4704
  :placeholder="fxCanEdit ? t('fxPlaceholder') : t('fxNoSel')"
4573
4705
  spellcheck="false"
4574
4706
  @input="onFxInput"
4707
+ @keyup="syncFxCaret"
4708
+ @click="syncFxCaret"
4575
4709
  @keydown="onFxKeydown"
4576
4710
  @blur="commitFx"
4577
4711
  />
4712
+ <span v-if="fxHint && !fxOpen && !editing" class="q-spreadsheet__fxhint">
4713
+ <strong>{{ fxHint.name }}</strong>(<template v-for="(arg, i) in fxHint.args" :key="i"><span
4714
+ class="q-spreadsheet__fxhint-arg"
4715
+ :class="{ 'q-spreadsheet__fxhint-arg--on': i === fxHint.active }"
4716
+ >{{ arg }}</span><span v-if="i < fxHint.args.length - 1">, </span></template>)
4717
+ </span>
4718
+ <span
4719
+ v-if="fxPreview"
4720
+ class="q-spreadsheet__fx-preview"
4721
+ :class="{ 'q-spreadsheet__fx-preview--error': fxPreview.error }"
4722
+ :title="fxPreview.hint"
4723
+ ><span class="q-spreadsheet__fx-preview-eq" aria-hidden="true">=</span>{{ fxPreview.text }}</span>
4578
4724
  </div>
4579
4725
 
4580
4726
  <!-- ═══════ Barre Find & Replace ═══════ -->
@@ -4954,6 +5100,9 @@ defineExpose({
4954
5100
  class="q-spreadsheet__editor-input q-spreadsheet__editor-area"
4955
5101
  :rows="draftLines"
4956
5102
  spellcheck="false"
5103
+ @input="syncEditCaret"
5104
+ @keyup="syncEditCaret"
5105
+ @click="syncEditCaret"
4957
5106
  @keydown="onEditKeydown"
4958
5107
  @blur="commitEdit"
4959
5108
  />
@@ -4968,6 +5117,12 @@ defineExpose({
4968
5117
  @keydown="onEditKeydown"
4969
5118
  @blur="commitEdit"
4970
5119
  />
5120
+ <div v-if="editingIsFormula && fxHint && !fxOpen" class="q-spreadsheet__fxhint q-spreadsheet__fxhint--editor">
5121
+ <strong>{{ fxHint.name }}</strong>(<template v-for="(arg, i) in fxHint.args" :key="i"><span
5122
+ class="q-spreadsheet__fxhint-arg"
5123
+ :class="{ 'q-spreadsheet__fxhint-arg--on': i === fxHint.active }"
5124
+ >{{ arg }}</span><span v-if="i < fxHint.args.length - 1">, </span></template>)
5125
+ </div>
4971
5126
  <div v-if="editingIsChoice && selectOptions.length" class="q-spreadsheet__options">
4972
5127
  <button
4973
5128
  v-for="opt in selectOptions"
@@ -4992,7 +5147,7 @@ defineExpose({
4992
5147
  <div v-if="editingIsFormula && fxOpen" class="q-spreadsheet__fnsug">
4993
5148
  <button
4994
5149
  v-for="(f, fi) in fxSuggestions"
4995
- :key="f.name"
5150
+ :key="`${f.kind}:${f.name}`"
4996
5151
  type="button"
4997
5152
  class="q-spreadsheet__fnsug-item"
4998
5153
  :class="{ 'q-spreadsheet__fnsug-item--on': fxActive === fi }"
@@ -5000,7 +5155,8 @@ defineExpose({
5000
5155
  @click="acceptFxAt(fi)"
5001
5156
  >
5002
5157
  <code>{{ f.name }}</code>
5003
- <span>({{ f.sig }})</span>
5158
+ <span v-if="f.kind === 'fn'">({{ f.detail }})</span>
5159
+ <span v-else class="q-spreadsheet__fnsug-kind">{{ f.detail }}</span>
5004
5160
  </button>
5005
5161
  </div>
5006
5162
  </div>
@@ -5297,7 +5453,7 @@ defineExpose({
5297
5453
  >
5298
5454
  <button
5299
5455
  v-for="(f, fi) in fxSuggestions"
5300
- :key="f.name"
5456
+ :key="`${f.kind}:${f.name}`"
5301
5457
  type="button"
5302
5458
  class="q-spreadsheet__fnsug-item"
5303
5459
  :class="{ 'q-spreadsheet__fnsug-item--on': fxActive === fi }"
@@ -5305,7 +5461,8 @@ defineExpose({
5305
5461
  @click="acceptFxAt(fi)"
5306
5462
  >
5307
5463
  <code>{{ f.name }}</code>
5308
- <span>({{ f.sig }})</span>
5464
+ <span v-if="f.kind === 'fn'">({{ f.detail }})</span>
5465
+ <span v-else class="q-spreadsheet__fnsug-kind">{{ f.detail }}</span>
5309
5466
  </button>
5310
5467
  </div>
5311
5468
  </Teleport>
package/index.ts CHANGED
@@ -203,19 +203,29 @@ export { DEFAULT_LAYOUT_VIEW, parseView, qLayoutKey } from "./lib/layout"
203
203
  export type { LayoutView, LayoutZone, LayoutZonePlacement, QLayoutContext } from "./lib/layout"
204
204
  export { vRipple } from "./lib/ripple"
205
205
  export type { RippleOptions, RippleValue } from "./lib/ripple"
206
- export { chartToECharts, channelValue, pickFromEvent, tableModel, textValue } from "./lib/chart"
206
+ export { chartToECharts, channelValue, pickFromEvent, tableCsv, tableModel, textValue } from "./lib/chart"
207
207
  export type {
208
208
  QChartAxis,
209
209
  QChartChannel,
210
210
  QChartColumn,
211
211
  QChartConfig,
212
+ QChartFormat,
213
+ QChartFormatKind,
214
+ QChartFormatOptions,
215
+ QChartGaugeBand,
212
216
  QChartLegend,
213
217
  QChartMark,
214
218
  QChartMarkType,
215
219
  QChartOrientation,
216
220
  QChartTable,
221
+ QChartTableTotals,
222
+ QChartTableView,
223
+ QChartThreshold,
224
+ QChartTooltip,
217
225
  QChartPick,
218
226
  } from "./lib/chart"
227
+ export { DEFAULT_CURRENCY, DEFAULT_LOCALE, formatValue } from "./lib/chartFormat"
228
+ export { boxStats, histogram, linearFit, waterfallSteps } from "./lib/chartStats"
219
229
  export { vTouchPan } from "./lib/touchPan"
220
230
  export type { PanDetails, TouchPanHandler, PanDirection } from "./lib/touchPan"
221
231
  export { vTouchHold } from "./lib/touchHold"
@@ -28,7 +28,7 @@ function renderSvg(config: Record<string, any>, width: number, height: number):
28
28
  width,
29
29
  height,
30
30
  })
31
- chart.setOption(chartToECharts({ theme: THEME, ...config }))
31
+ chart.setOption(chartToECharts({ marks: [], theme: THEME, ...config }))
32
32
  const svg = chart.renderToSVGString()
33
33
  chart.dispose()
34
34
  return svg