@particle-academy/holy-sheet 2.7.0 → 2.8.0

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/dist/index.js CHANGED
@@ -77,9 +77,9 @@ var Inference = {
77
77
  return { header: headerName, type: "auto" };
78
78
  }
79
79
  };
80
- function nonNullSample(values) {
80
+ function nonNullSample(values2) {
81
81
  const out = [];
82
- for (const v of values) {
82
+ for (const v of values2) {
83
83
  if (v === null || v === void 0) continue;
84
84
  out.push(v);
85
85
  if (out.length >= SAMPLE_SIZE) break;
@@ -269,10 +269,10 @@ var CellAddress = {
269
269
 
270
270
  // src/workbook/cell.ts
271
271
  var Cell = class {
272
- constructor(address, value, formula = null, format = null, comment = null, cachedValue = null) {
272
+ constructor(address, value, formula2 = null, format = null, comment = null, cachedValue = null) {
273
273
  this.address = address;
274
274
  this.value = value;
275
- this.formula = formula;
275
+ this.formula = formula2;
276
276
  this.format = format;
277
277
  this.comment = comment;
278
278
  this.cachedValue = cachedValue;
@@ -320,8 +320,8 @@ var ColumnWidths = {
320
320
 
321
321
  // src/workbook/cell-comment.ts
322
322
  var CellComment = class {
323
- constructor(text, author = null, color2 = null) {
324
- this.text = text;
323
+ constructor(text2, author = null, color2 = null) {
324
+ this.text = text2;
325
325
  this.author = author;
326
326
  this.color = color2;
327
327
  }
@@ -415,8 +415,8 @@ var MergedRegion = class {
415
415
  };
416
416
 
417
417
  // src/workbook/sheet.ts
418
- var Sheet = class {
419
- constructor(name, cells = /* @__PURE__ */ new Map(), mergedRegions = [], columnWidths = /* @__PURE__ */ new Map(), frozenRows = 0, frozenCols = 0, rowHeights = /* @__PURE__ */ new Map(), print = null) {
418
+ var Sheet = class _Sheet {
419
+ constructor(name, cells = /* @__PURE__ */ new Map(), mergedRegions = [], columnWidths = /* @__PURE__ */ new Map(), frozenRows = 0, frozenCols = 0, rowHeights = /* @__PURE__ */ new Map(), print = null, charts = []) {
420
420
  this.name = name;
421
421
  this.cells = cells;
422
422
  this.mergedRegions = mergedRegions;
@@ -425,6 +425,10 @@ var Sheet = class {
425
425
  this.frozenCols = frozenCols;
426
426
  this.rowHeights = rowHeights;
427
427
  this.print = print;
428
+ this.charts = charts;
429
+ }
430
+ withCharts(charts) {
431
+ return new _Sheet(this.name, this.cells, this.mergedRegions, this.columnWidths, this.frozenRows, this.frozenCols, this.rowHeights, this.print, charts);
428
432
  }
429
433
  /** rowNumber → (colLetter → Cell), sorted by ascending row number. */
430
434
  rows() {
@@ -455,6 +459,59 @@ var Sheet = class {
455
459
  }
456
460
  };
457
461
 
462
+ // src/workbook/chart-range.ts
463
+ var ChartRange = class _ChartRange {
464
+ constructor(sheet, c1, r1, c2, r2) {
465
+ this.sheet = sheet;
466
+ this.c1 = c1;
467
+ this.r1 = r1;
468
+ this.c2 = c2;
469
+ this.r2 = r2;
470
+ }
471
+ static parse(ref, defaultSheet) {
472
+ if (typeof ref !== "string") return null;
473
+ let text2 = ref.trim();
474
+ let sheet = defaultSheet;
475
+ const bang = text2.lastIndexOf("!");
476
+ if (bang >= 0) {
477
+ let sheetPart = text2.slice(0, bang);
478
+ text2 = text2.slice(bang + 1);
479
+ if (sheetPart.length >= 2 && sheetPart.startsWith("'") && sheetPart.endsWith("'")) {
480
+ sheetPart = sheetPart.slice(1, -1).replace(/''/g, "'");
481
+ }
482
+ if (sheetPart === "") return null;
483
+ sheet = sheetPart;
484
+ }
485
+ const ends = text2.replace(/\$/g, "").split(":");
486
+ if (ends.length > 2) return null;
487
+ const a = CellAddress.parse(ends[0]);
488
+ const b = CellAddress.parse(ends[1] ?? ends[0]);
489
+ if (a === null || b === null || a[1] < 1 || b[1] < 1) return null;
490
+ return new _ChartRange(sheet, Math.min(a[0], b[0]), Math.min(a[1], b[1]), Math.max(a[0], b[0]), Math.max(a[1], b[1]));
491
+ }
492
+ /** 'Q3 Revenue'!$B$5:$B$7 */
493
+ formula() {
494
+ const from = "$" + CellAddress.letter(this.c1) + "$" + this.r1;
495
+ const to = "$" + CellAddress.letter(this.c2) + "$" + this.r2;
496
+ return "'" + this.sheet.replace(/'/g, "''") + "'!" + from + (from === to ? "" : ":" + to);
497
+ }
498
+ /** "B5:B7" on its own sheet, "'Data'!B5:B7" on another. */
499
+ toSchema(ownSheet) {
500
+ const from = CellAddress.letter(this.c1) + this.r1;
501
+ const to = CellAddress.letter(this.c2) + this.r2;
502
+ const ref = from === to ? from : from + ":" + to;
503
+ return this.sheet === ownSheet ? ref : "'" + this.sheet.replace(/'/g, "''") + "'!" + ref;
504
+ }
505
+ /** A1 addresses, row by row. */
506
+ addresses() {
507
+ const out = [];
508
+ for (let r = this.r1; r <= this.r2; r++) {
509
+ for (let c = this.c1; c <= this.c2; c++) out.push(CellAddress.letter(c) + r);
510
+ }
511
+ return out;
512
+ }
513
+ };
514
+
458
515
  // src/workbook/workbook.ts
459
516
  var Workbook = class {
460
517
  constructor(sheets, meta = {}) {
@@ -667,20 +724,21 @@ var Normalizer = class {
667
724
  toInt(sheet.frozenRows ?? 0),
668
725
  toInt(sheet.frozenCols ?? 0),
669
726
  normalizeRowHeights(sheet.rowHeights),
670
- normalizePrint(sheet.print)
727
+ normalizePrint(sheet.print),
728
+ normalizeCharts(sheet.charts, name, cells)
671
729
  );
672
730
  }
673
731
  normalizeCellMap(map, sheetName2) {
674
732
  const cells = /* @__PURE__ */ new Map();
675
733
  for (const [address, cellData] of Object.entries(map)) {
676
734
  if (!isCellSpec(cellData)) {
677
- const [bare, formula] = promoteFormula(cellData);
735
+ const [bare, formula2] = promoteFormula(cellData);
678
736
  cells.set(
679
737
  address,
680
738
  new Cell(
681
739
  address,
682
- formula !== null ? null : this.coerceValue(bare, null, address, sheetName2),
683
- formula
740
+ formula2 !== null ? null : this.coerceValue(bare, null, address, sheetName2),
741
+ formula2
684
742
  )
685
743
  );
686
744
  continue;
@@ -796,11 +854,11 @@ var Normalizer = class {
796
854
  value.computedValue ?? null
797
855
  );
798
856
  }
799
- const [bare, formula] = promoteFormula(value);
857
+ const [bare, formula2] = promoteFormula(value);
800
858
  return new Cell(
801
859
  address,
802
- formula !== null ? null : this.coerceValue(bare, columnFormat, address, sheetName2),
803
- formula,
860
+ formula2 !== null ? null : this.coerceValue(bare, columnFormat, address, sheetName2),
861
+ formula2,
804
862
  columnFormat
805
863
  );
806
864
  }
@@ -858,6 +916,53 @@ function toInt(v) {
858
916
  const n = Math.trunc(Number(v));
859
917
  return Number.isFinite(n) ? n : 0;
860
918
  }
919
+ var CHART_TYPES = ["column", "bar", "line", "pie"];
920
+ function normalizeCharts(charts, sheetName2, cells) {
921
+ if (!isPlainObject(charts) && !Array.isArray(charts)) return [];
922
+ let lastCol = -1;
923
+ for (const address of cells.keys()) {
924
+ const parsed = CellAddress.parse(address);
925
+ if (parsed !== null) lastCol = Math.max(lastCol, parsed[0]);
926
+ }
927
+ const out = [];
928
+ for (const chart of Object.values(charts)) {
929
+ if (!isPlainObject(chart) || !CHART_TYPES.includes(chart.type)) continue;
930
+ const series = [];
931
+ const rawSeries = isPlainObject(chart.series) || Array.isArray(chart.series) ? Object.values(chart.series) : [];
932
+ for (const s of rawSeries) {
933
+ const values2 = isPlainObject(s) || Array.isArray(s) ? ChartRange.parse(s.values, sheetName2) : null;
934
+ if (values2 === null) continue;
935
+ const rec = s;
936
+ const m = typeof rec.color === "string" ? /^#([0-9A-Fa-f]{6})$/.exec(rec.color) : null;
937
+ series.push({ name: typeof rec.name === "string" ? rec.name : null, values: values2, color: m ? m[1].toUpperCase() : null });
938
+ }
939
+ if (series.length === 0) continue;
940
+ const stack = out.length;
941
+ let from = [lastCol + 2, 1 + 18 * stack];
942
+ let to = [from[0] + 8, from[1] + 16];
943
+ if (isPlainObject(chart.position) || Array.isArray(chart.position)) {
944
+ const p = chart.position;
945
+ const a = typeof p.from === "string" ? CellAddress.parse(p.from) : null;
946
+ const b = typeof p.to === "string" ? CellAddress.parse(p.to) : null;
947
+ if (a !== null && b !== null && b[0] > a[0] && b[1] > a[1]) {
948
+ from = [a[0], a[1] - 1];
949
+ to = [b[0], b[1] - 1];
950
+ }
951
+ }
952
+ out.push({
953
+ type: chart.type,
954
+ title: typeof chart.title === "string" && chart.title !== "" ? chart.title : null,
955
+ categories: ChartRange.parse(chart.categories, sheetName2),
956
+ series,
957
+ from,
958
+ to,
959
+ legend: typeof chart.legend === "boolean" ? chart.legend : null,
960
+ dataLabels: chart.dataLabels === true,
961
+ format: typeof chart.format === "string" && chart.format !== "" ? chart.format : null
962
+ });
963
+ }
964
+ return out;
965
+ }
861
966
  function phpNumeric(v) {
862
967
  if (typeof v === "number") return Number.isFinite(v) ? v : null;
863
968
  return isNumericString(v) ? numericStringToNumber(v) : null;
@@ -948,9 +1053,9 @@ var FormulaLinter = class {
948
1053
  isError(v) {
949
1054
  return typeof v === "string" && v.length > 0 && v[0] === "#";
950
1055
  }
951
- evaluate(formula, defaultSheet, stack) {
1056
+ evaluate(formula2, defaultSheet, stack) {
952
1057
  try {
953
- const c = { tokens: this.tokenize(formula), pos: 0 };
1058
+ const c = { tokens: this.tokenize(formula2), pos: 0 };
954
1059
  const result = this.parseExpr(c, defaultSheet, stack);
955
1060
  if (c.pos < c.tokens.length) return ERR_NAME;
956
1061
  return result;
@@ -1211,13 +1316,13 @@ var FormulaLinter = class {
1211
1316
  const row1 = Math.min(a[1], b[1]);
1212
1317
  const col2 = Math.max(a[0], b[0]);
1213
1318
  const row2 = Math.max(a[1], b[1]);
1214
- const values = [];
1319
+ const values2 = [];
1215
1320
  for (let r = row1; r <= row2; r++) {
1216
1321
  for (let col = col1; col <= col2; col++) {
1217
- values.push(this.resolveCell(sheet, CellAddress.letter(col) + r, stack));
1322
+ values2.push(this.resolveCell(sheet, CellAddress.letter(col) + r, stack));
1218
1323
  }
1219
1324
  }
1220
- return values;
1325
+ return values2;
1221
1326
  }
1222
1327
  expectOp(c, op) {
1223
1328
  const t = c.tokens[c.pos];
@@ -1349,12 +1454,12 @@ var FormulaLinter = class {
1349
1454
  return truthy ? args[1] : args[2] ?? false;
1350
1455
  }
1351
1456
  // ---- Hints ----
1352
- hint(error2, formula, sheet) {
1457
+ hint(error2, formula2, sheet) {
1353
1458
  switch (error2) {
1354
1459
  case ERR_VALUE:
1355
- return this.hintValue(formula, sheet);
1460
+ return this.hintValue(formula2, sheet);
1356
1461
  case ERR_REF:
1357
- return this.hintRef(formula);
1462
+ return this.hintRef(formula2);
1358
1463
  case ERR_NAME:
1359
1464
  return "The formula references an unknown function or has a syntax error. Holy Sheet supports: SUM, AVERAGE, COUNT, COUNTA, MIN, MAX, IF, ROUND, ABS, LEN, UPPER, LOWER, CONCAT.";
1360
1465
  case ERR_DIV0:
@@ -1365,8 +1470,8 @@ var FormulaLinter = class {
1365
1470
  return "Formula evaluation failed.";
1366
1471
  }
1367
1472
  }
1368
- hintRef(formula) {
1369
- for (const m of formula.matchAll(new RegExp(SHEET_QUALIFIER, "g"))) {
1473
+ hintRef(formula2) {
1474
+ for (const m of formula2.matchAll(new RegExp(SHEET_QUALIFIER, "g"))) {
1370
1475
  const name = this.cleanSheetName(m[1]);
1371
1476
  if (!this.sheetNames.has(name.toLowerCase())) {
1372
1477
  return `The formula refers to a sheet named '${name}', and this workbook has no such sheet. Its sheets are: ${[...this.sheetNames.values()].join(", ")}. Quote a name that contains spaces or punctuation: 'My Sheet'!A1.`;
@@ -1374,10 +1479,10 @@ var FormulaLinter = class {
1374
1479
  }
1375
1480
  return "A cell reference points to a cell that doesn't exist in the workbook. Check column letters and row numbers.";
1376
1481
  }
1377
- hintValue(formula, sheet) {
1482
+ hintValue(formula2, sheet) {
1378
1483
  const re = new RegExp(`(?:${SHEET_QUALIFIER})?\\$?([A-Z]+)\\$?(\\d+)`, "gi");
1379
1484
  const offenders = [];
1380
- for (const m of formula.matchAll(re)) {
1485
+ for (const m of formula2.matchAll(re)) {
1381
1486
  const sheetName2 = m[1] ? this.sheetNames.get(this.cleanSheetName(m[1]).toLowerCase()) ?? this.cleanSheetName(m[1]) : sheet;
1382
1487
  const a1 = m[2].toUpperCase() + m[3];
1383
1488
  const key = sheetName2 + "!" + a1;
@@ -1500,11 +1605,11 @@ var Repairer = class {
1500
1605
  sheet.columns.forEach((col, colIdx) => {
1501
1606
  if (!isPlainObject(col)) return;
1502
1607
  if (col.type !== void 0 && col.type !== "auto") return;
1503
- const values = [];
1608
+ const values2 = [];
1504
1609
  for (const row of sheet.rows) {
1505
- if (Array.isArray(row) && colIdx < row.length) values.push(row[colIdx]);
1610
+ if (Array.isArray(row) && colIdx < row.length) values2.push(row[colIdx]);
1506
1611
  }
1507
- const nonNull = values.filter((v) => v !== null && v !== void 0);
1612
+ const nonNull = values2.filter((v) => v !== null && v !== void 0);
1508
1613
  if (nonNull.length === 0) return;
1509
1614
  const allDateLike = nonNull.every((v) => typeof v === "string" && ISO_DATE2.test(v));
1510
1615
  if (allDateLike) {
@@ -1767,6 +1872,9 @@ var Validator = class {
1767
1872
  }
1768
1873
  }
1769
1874
  }
1875
+ if (sheet.charts != null) {
1876
+ errors.push(...validateCharts(sheet.charts, path, typeof sheet.name === "string" ? sheet.name : ""));
1877
+ }
1770
1878
  if (sheet.print != null) {
1771
1879
  const print = sheet.print;
1772
1880
  if (!isPlainObject(print) && !Array.isArray(print)) {
@@ -1867,6 +1975,113 @@ var Validator = class {
1867
1975
  return errors;
1868
1976
  }
1869
1977
  };
1978
+ function validateCharts(charts, path, sheetName2) {
1979
+ if (!Array.isArray(charts)) {
1980
+ return [
1981
+ error(
1982
+ `${path}.charts`,
1983
+ "a list of charts",
1984
+ typeOf(charts),
1985
+ charts,
1986
+ 'charts is a list: [{"type": "column", "categories": "A5:A7", "series": [{"name": "Revenue", "values": "B5:B7"}]}].'
1987
+ )
1988
+ ];
1989
+ }
1990
+ const errors = [];
1991
+ charts.forEach((chart, i) => {
1992
+ const at2 = `${path}.charts[${i}]`;
1993
+ if (!isPlainObject(chart) && !Array.isArray(chart)) {
1994
+ errors.push(error(at2, "object", typeOf(chart), chart, "A chart is an object with a type and series."));
1995
+ return;
1996
+ }
1997
+ const c = chart;
1998
+ if (!CHART_TYPES.includes(c.type)) {
1999
+ errors.push(error(
2000
+ `${at2}.type`,
2001
+ "one of: " + CHART_TYPES.join(", "),
2002
+ typeOf(c.type ?? null),
2003
+ c.type ?? null,
2004
+ "column compares values across categories; bar does the same horizontally (long labels); line shows change over time; pie shows parts of one whole."
2005
+ ));
2006
+ }
2007
+ const series = c.series;
2008
+ if (!Array.isArray(series) || series.length === 0) {
2009
+ errors.push(error(
2010
+ `${at2}.series`,
2011
+ "a non-empty list of series",
2012
+ typeOf(series ?? null),
2013
+ series ?? null,
2014
+ 'Each series names the range it plots: [{"name": "Revenue", "values": "B5:B7"}].'
2015
+ ));
2016
+ } else {
2017
+ series.forEach((s, j) => {
2018
+ const isObj = isPlainObject(s) || Array.isArray(s);
2019
+ const values2 = isObj ? s.values ?? null : s;
2020
+ if (!isObj || ChartRange.parse(values2, sheetName2) === null) {
2021
+ errors.push(error(
2022
+ `${at2}.series[${j}].values`,
2023
+ 'a cell range like "B5:B7"',
2024
+ typeOf(values2),
2025
+ values2,
2026
+ `A range of cells: "B5:B7" on this sheet, or "'Other sheet'!B5:B7".`
2027
+ ));
2028
+ } else {
2029
+ const color2 = s.color;
2030
+ if (color2 != null && (typeof color2 !== "string" || !/^#[0-9A-Fa-f]{6}$/.test(color2))) {
2031
+ errors.push(error(`${at2}.series[${j}].color`, "#RRGGBB", typeOf(color2), color2, 'A hex colour, like "#1F3A5F".'));
2032
+ }
2033
+ }
2034
+ });
2035
+ }
2036
+ if (c.categories != null && ChartRange.parse(c.categories, sheetName2) === null) {
2037
+ errors.push(error(
2038
+ `${at2}.categories`,
2039
+ 'a cell range like "A5:A7"',
2040
+ typeOf(c.categories),
2041
+ c.categories,
2042
+ 'The labels along the axis, as a range: "A5:A7".'
2043
+ ));
2044
+ }
2045
+ if (c.position != null) {
2046
+ const p = c.position;
2047
+ const isObj = isPlainObject(p) || Array.isArray(p);
2048
+ const a = isObj && typeof p.from === "string" ? CellAddress.parse(p.from) : null;
2049
+ const b = isObj && typeof p.to === "string" ? CellAddress.parse(p.to) : null;
2050
+ if (a === null || b === null || b[0] <= a[0] || b[1] <= a[1]) {
2051
+ errors.push(error(
2052
+ `${at2}.position`,
2053
+ '{"from": top-left cell, "to": bottom-right cell}',
2054
+ typeOf(p),
2055
+ p,
2056
+ 'Two cells, the second below and to the right of the first: {"from": "E2", "to": "L18"} is 7 columns by 16 rows.'
2057
+ ));
2058
+ }
2059
+ }
2060
+ for (const key of ["title", "format"]) {
2061
+ if (c[key] != null && typeof c[key] !== "string") {
2062
+ errors.push(error(
2063
+ `${at2}.${key}`,
2064
+ "string",
2065
+ typeOf(c[key]),
2066
+ c[key],
2067
+ key === "format" ? 'An Excel number format for the axis and labels, like "$#,##0" or "0.0%".' : "The chart title."
2068
+ ));
2069
+ }
2070
+ }
2071
+ for (const key of ["dataLabels", "legend"]) {
2072
+ if (c[key] != null && typeof c[key] !== "boolean") {
2073
+ errors.push(error(
2074
+ `${at2}.${key}`,
2075
+ "boolean",
2076
+ typeOf(c[key]),
2077
+ c[key],
2078
+ key === "dataLabels" ? "true prints each value on the chart." : "Defaults to on for several series or a pie."
2079
+ ));
2080
+ }
2081
+ }
2082
+ });
2083
+ return errors;
2084
+ }
1870
2085
  function error(path, expected, got, value, hint) {
1871
2086
  return { path, expected, got, value, hint };
1872
2087
  }
@@ -2383,15 +2598,15 @@ var FormatSniffer = {
2383
2598
  };
2384
2599
 
2385
2600
  // src/reader/ods/ods-formula.ts
2386
- function odsFormulaToA1(formula) {
2601
+ function odsFormulaToA1(formula2) {
2387
2602
  let grammar = "of";
2388
- const m = /^(of|oooc|msoxl):/i.exec(formula);
2603
+ const m = /^(of|oooc|msoxl):/i.exec(formula2);
2389
2604
  if (m) {
2390
2605
  grammar = m[1].toLowerCase();
2391
- formula = formula.slice(m[0].length);
2606
+ formula2 = formula2.slice(m[0].length);
2392
2607
  }
2393
- if (formula.startsWith("=")) formula = formula.slice(1);
2394
- return grammar === "msoxl" ? formula : translate(formula);
2608
+ if (formula2.startsWith("=")) formula2 = formula2.slice(1);
2609
+ return grammar === "msoxl" ? formula2 : translate(formula2);
2395
2610
  }
2396
2611
  function translate(s) {
2397
2612
  let out = "";
@@ -2740,8 +2955,8 @@ var OdsStyles = class {
2740
2955
  switch (el2.name) {
2741
2956
  case "number-style": {
2742
2957
  if (has2("scientific-number") || has2("fraction")) return null;
2743
- for (const text of parts.filter((p) => p.name === "text")) {
2744
- const iso = currencyCode(ownText(text).replace(/[-()"\u00A0]/g, ""));
2958
+ for (const text2 of parts.filter((p) => p.name === "text")) {
2959
+ const iso = currencyCode(ownText(text2).replace(/[-()"\u00A0]/g, ""));
2745
2960
  if (iso !== null) return { displayFormat: "currency", decimals: decimals ?? 0, currency: iso };
2746
2961
  }
2747
2962
  if (!number) return null;
@@ -2927,6 +3142,29 @@ function sheetToSchema(sheet) {
2927
3142
  }
2928
3143
  out["print"] = print;
2929
3144
  }
3145
+ if (sheet.charts.length > 0) {
3146
+ out["charts"] = sheet.charts.map((c) => chartToSchema(c, sheet.name));
3147
+ }
3148
+ return out;
3149
+ }
3150
+ function chartToSchema(chart, sheetName2) {
3151
+ const out = { type: chart.type };
3152
+ if (chart.title !== null) out["title"] = chart.title;
3153
+ if (chart.categories !== null) out["categories"] = chart.categories.toSchema(sheetName2);
3154
+ out["series"] = chart.series.map((s) => {
3155
+ const series = {};
3156
+ if (s.name !== null) series["name"] = s.name;
3157
+ series["values"] = s.values.toSchema(sheetName2);
3158
+ if (s.color !== null) series["color"] = "#" + s.color;
3159
+ return series;
3160
+ });
3161
+ out["position"] = {
3162
+ from: CellAddress.letter(chart.from[0]) + (chart.from[1] + 1),
3163
+ to: CellAddress.letter(chart.to[0]) + (chart.to[1] + 1)
3164
+ };
3165
+ if (chart.dataLabels) out["dataLabels"] = true;
3166
+ if (chart.legend !== null) out["legend"] = chart.legend;
3167
+ if (chart.format !== null) out["format"] = chart.format;
2930
3168
  return out;
2931
3169
  }
2932
3170
  function cellToSchema(cell) {
@@ -3034,10 +3272,10 @@ function parseXml(src, options = {}) {
3034
3272
  if (namespaces) root.content = [];
3035
3273
  const stack = [root];
3036
3274
  const scopes = [{ xml: XML_NS }];
3037
- const appendText = (text) => {
3275
+ const appendText = (text2) => {
3038
3276
  const top = stack[stack.length - 1];
3039
- top.text += text;
3040
- if (namespaces && text !== "") top.content.push(text);
3277
+ top.text += text2;
3278
+ if (namespaces && text2 !== "") top.content.push(text2);
3041
3279
  };
3042
3280
  while (i < n) {
3043
3281
  if (src[i] === "<") {
@@ -3258,7 +3496,7 @@ function hasContent(cell) {
3258
3496
  }
3259
3497
  function readCell(cell) {
3260
3498
  const type = attr(cell, NS.OFFICE, "value-type") ?? null;
3261
- const text = paragraphs(cell);
3499
+ const text2 = paragraphs(cell);
3262
3500
  let value = null;
3263
3501
  let seconds = null;
3264
3502
  let dateHasTime = false;
@@ -3287,20 +3525,20 @@ function readCell(cell) {
3287
3525
  case "string": {
3288
3526
  const error2 = attr(cell, NS.CALCEXT, "value-type") === "error";
3289
3527
  const stored = attr(cell, NS.OFFICE, "string-value");
3290
- value = stored !== void 0 && !error2 ? stored : text ?? "";
3528
+ value = stored !== void 0 && !error2 ? stored : text2 ?? "";
3291
3529
  break;
3292
3530
  }
3293
3531
  default:
3294
- value = text !== null && text !== "" ? text : null;
3532
+ value = text2 !== null && text2 !== "" ? text2 : null;
3295
3533
  }
3296
- let formula = null;
3534
+ let formula2 = null;
3297
3535
  let cached = null;
3298
3536
  const formulaAttr = attr(cell, NS.TABLE, "formula");
3299
3537
  if (formulaAttr !== void 0) {
3300
3538
  const translated = odsFormulaToA1(formulaAttr);
3301
3539
  const literalBoolean = type === "boolean" && /^[ \t\n\r\f\v]*(TRUE|FALSE)\(\)[ \t\n\r\f\v]*$/i.test(translated);
3302
3540
  if (!literalBoolean) {
3303
- formula = translated;
3541
+ formula2 = translated;
3304
3542
  cached = seconds !== null ? serial(seconds) : value;
3305
3543
  value = null;
3306
3544
  seconds = null;
@@ -3316,7 +3554,7 @@ function readCell(cell) {
3316
3554
  const currency = attr(cell, NS.OFFICE, "currency");
3317
3555
  return {
3318
3556
  value,
3319
- formula,
3557
+ formula: formula2,
3320
3558
  cached,
3321
3559
  type,
3322
3560
  currency: currency !== void 0 ? phpTrim(currency) : null,
@@ -3385,6 +3623,127 @@ function readMeta(xml) {
3385
3623
  return out;
3386
3624
  }
3387
3625
 
3626
+ // src/reader/chart-parser.ts
3627
+ function descendants(node, name) {
3628
+ const out = [];
3629
+ const stack = node ? [...node.children].reverse() : [];
3630
+ while (stack.length > 0) {
3631
+ const n = stack.pop();
3632
+ if (n.name === name) out.push(n);
3633
+ for (let i = n.children.length - 1; i >= 0; i--) stack.push(n.children[i]);
3634
+ }
3635
+ return out;
3636
+ }
3637
+ function load(xml) {
3638
+ if (/<!DOCTYPE/i.test(xml)) return null;
3639
+ return parseXml(xml);
3640
+ }
3641
+ function wrap2(root) {
3642
+ return { name: "#document", attrs: {}, children: [root], text: "" };
3643
+ }
3644
+ var ChartParser = {
3645
+ anchors(drawingXml) {
3646
+ const root = load(drawingXml);
3647
+ if (!root) return [];
3648
+ const out = [];
3649
+ for (const anchor of descendants(wrap2(root), "twoCellAnchor")) {
3650
+ const from = marker(anchor, "from");
3651
+ const to = marker(anchor, "to");
3652
+ const chart = descendants(anchor, "chart")[0];
3653
+ const rel = chart ? at(chart, "id") ?? "" : "";
3654
+ if (from !== null && to !== null && rel !== "") out.push({ from, to, rel });
3655
+ }
3656
+ return out;
3657
+ },
3658
+ chart(chartXml, sheetName2, from, to) {
3659
+ const root = load(chartXml);
3660
+ if (!root) return null;
3661
+ const doc = wrap2(root);
3662
+ let plot;
3663
+ let type = null;
3664
+ for (const tag of ["barChart", "lineChart", "pieChart"]) {
3665
+ const found = descendants(doc, tag)[0];
3666
+ if (found) {
3667
+ plot = found;
3668
+ type = tag === "lineChart" ? "line" : tag === "pieChart" ? "pie" : at(el(found, "barDir"), "val") === "bar" ? "bar" : "column";
3669
+ break;
3670
+ }
3671
+ }
3672
+ if (!plot || type === null) return null;
3673
+ let categories = null;
3674
+ const series = [];
3675
+ for (const ser of els(plot, "ser")) {
3676
+ const valRef = formula(ser, "val");
3677
+ const values2 = valRef !== null ? ChartRange.parse(valRef, sheetName2) : null;
3678
+ if (values2 === null) continue;
3679
+ if (categories === null) {
3680
+ const catRef = formula(ser, "cat");
3681
+ categories = catRef !== null ? ChartRange.parse(catRef, sheetName2) : null;
3682
+ }
3683
+ series.push({ name: seriesName(ser), values: values2, color: seriesColor(ser) });
3684
+ }
3685
+ if (series.length === 0) return null;
3686
+ const labels = el(plot, "dLbls");
3687
+ const dataLabels2 = labels !== void 0 && at(el(labels, "showVal"), "val") === "1";
3688
+ let format = null;
3689
+ for (const host of [labels, descendants(doc, "valAx")[0]]) {
3690
+ const numFmt = host ? el(host, "numFmt") : void 0;
3691
+ if (numFmt && at(numFmt, "sourceLinked") === "0" && (at(numFmt, "formatCode") ?? "") !== "") {
3692
+ format = at(numFmt, "formatCode");
3693
+ break;
3694
+ }
3695
+ }
3696
+ let title = null;
3697
+ const titleEl = descendants(doc, "title")[0];
3698
+ if (titleEl) {
3699
+ const text2 = descendants(titleEl, "t").map((t) => t.text).join("");
3700
+ title = text2 !== "" ? text2 : null;
3701
+ }
3702
+ return {
3703
+ type,
3704
+ title,
3705
+ categories,
3706
+ series,
3707
+ from,
3708
+ to,
3709
+ legend: descendants(doc, "legend").length > 0 ? true : null,
3710
+ dataLabels: dataLabels2,
3711
+ format
3712
+ };
3713
+ }
3714
+ };
3715
+ function marker(anchor, tag) {
3716
+ const m = el(anchor, tag);
3717
+ if (!m) return null;
3718
+ const col = descendants(m, "col")[0]?.text.trim() ?? "";
3719
+ const row = descendants(m, "row")[0]?.text.trim() ?? "";
3720
+ return /^\d+$/.test(col) && /^\d+$/.test(row) ? [parseInt(col, 10), parseInt(row, 10)] : null;
3721
+ }
3722
+ function formula(ser, name) {
3723
+ const host = el(ser, name);
3724
+ if (!host) return null;
3725
+ for (const ref of ["numRef", "strRef"]) {
3726
+ const f = el(el(host, ref), "f");
3727
+ if (f && f.text.trim() !== "") return f.text.trim();
3728
+ }
3729
+ return null;
3730
+ }
3731
+ function seriesName(ser) {
3732
+ const tx = el(ser, "tx");
3733
+ if (!tx) return null;
3734
+ const v = el(tx, "v");
3735
+ if (v) return v.text;
3736
+ const cached = descendants(tx, "v")[0];
3737
+ return cached ? cached.text : null;
3738
+ }
3739
+ function seriesColor(ser) {
3740
+ const spPr = el(ser, "spPr");
3741
+ if (!spPr) return null;
3742
+ const clr = descendants(spPr, "srgbClr")[0];
3743
+ const val = (clr ? at(clr, "val") ?? "" : "").toUpperCase();
3744
+ return /^[0-9A-F]{6}$/.test(val) ? val : null;
3745
+ }
3746
+
3388
3747
  // src/reader/comments-parser.ts
3389
3748
  var CommentsParser = {
3390
3749
  parse(commentsXml) {
@@ -3616,13 +3975,13 @@ function parseCells(xml, stylesIndex, comments, sharedStrings) {
3616
3975
  const type = c.attrs["t"] ?? "n";
3617
3976
  const styleIdx = c.attrs["s"] !== void 0 ? parseInt(c.attrs["s"], 10) : 0;
3618
3977
  const format = stylesIndex[styleIdx] ?? null;
3619
- let formula = null;
3978
+ let formula2 = null;
3620
3979
  let cachedValue = null;
3621
3980
  let value = null;
3622
3981
  const f = el(c, "f");
3623
3982
  const v = el(c, "v");
3624
3983
  if (f) {
3625
- formula = f.text;
3984
+ formula2 = f.text;
3626
3985
  cachedValue = v ? coerceValue(v.text, type) : null;
3627
3986
  } else if (type === "inlineStr" && el(c, "is") && el(el(c, "is"), "t")) {
3628
3987
  value = el(el(c, "is"), "t").text;
@@ -3633,7 +3992,7 @@ function parseCells(xml, stylesIndex, comments, sharedStrings) {
3633
3992
  value = coerceValue(v.text, type);
3634
3993
  }
3635
3994
  const comment = comments[address] ?? null;
3636
- cells.set(address, new Cell(address, value, formula, format, comment, cachedValue));
3995
+ cells.set(address, new Cell(address, value, formula2, format, comment, cachedValue));
3637
3996
  }
3638
3997
  }
3639
3998
  return cells;
@@ -3724,7 +4083,26 @@ var XlsxReader = class {
3724
4083
  if (commentsXml) comments = { ...comments, ...CommentsParser.parse(commentsXml) };
3725
4084
  }
3726
4085
  }
3727
- sheets.push(WorksheetParser.parse(worksheetXml, name, stylesIndex, comments, sharedStrings));
4086
+ let sheet = WorksheetParser.parse(worksheetXml, name, stylesIndex, comments, sharedStrings);
4087
+ if (sheetRelsXml) {
4088
+ const charts = [];
4089
+ for (const dr of Object.values(RelsParser.byType(RelsParser.parse(sheetRelsXml), "/drawing"))) {
4090
+ const drawingPath = resolveRelativePath(sheetPath, dr.Target);
4091
+ const slash = drawingPath.lastIndexOf("/");
4092
+ const drawingXml = getText(drawingPath);
4093
+ const drawingRelsXml = getText(drawingPath.slice(0, slash) + "/_rels/" + drawingPath.slice(slash + 1) + ".rels");
4094
+ if (drawingXml === null || drawingRelsXml === null) continue;
4095
+ const drawingRels = RelsParser.parse(drawingRelsXml);
4096
+ for (const anchor of ChartParser.anchors(drawingXml)) {
4097
+ const target2 = drawingRels[anchor.rel]?.Target;
4098
+ const chartXml = typeof target2 === "string" ? getText(resolveRelativePath(drawingPath, target2)) : null;
4099
+ const chart = chartXml !== null ? ChartParser.chart(chartXml, name, anchor.from, anchor.to) : null;
4100
+ if (chart !== null) charts.push(chart);
4101
+ }
4102
+ }
4103
+ if (charts.length > 0) sheet = sheet.withCharts(charts);
4104
+ }
4105
+ sheets.push(sheet);
3728
4106
  i++;
3729
4107
  }
3730
4108
  const meta = this.parseDocProps(getText("docProps/core.xml"));
@@ -3978,6 +4356,114 @@ function hexToArgb(hex2) {
3978
4356
  return "FF000000";
3979
4357
  }
3980
4358
 
4359
+ // src/writer/chart-xml.ts
4360
+ var NS_C = "http://schemas.openxmlformats.org/drawingml/2006/chart";
4361
+ var NS_A = "http://schemas.openxmlformats.org/drawingml/2006/main";
4362
+ var NS_R = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
4363
+ var ChartXml = {
4364
+ drawing(charts, firstChartNum) {
4365
+ let anchors = "";
4366
+ charts.forEach((chart, k) => {
4367
+ const id = k + 2;
4368
+ const num = firstChartNum + k;
4369
+ anchors += '<xdr:twoCellAnchor editAs="oneCell">' + marker2("from", chart.from) + marker2("to", chart.to) + `<xdr:graphicFrame macro=""><xdr:nvGraphicFramePr><xdr:cNvPr id="${id}" name="Chart ${num}"/><xdr:cNvGraphicFramePr/></xdr:nvGraphicFramePr><xdr:xfrm><a:off x="0" y="0"/><a:ext cx="0" cy="0"/></xdr:xfrm><a:graphic><a:graphicData uri="${NS_C}"><c:chart xmlns:c="${NS_C}" xmlns:r="${NS_R}" r:id="rId${k + 1}"/></a:graphicData></a:graphic></xdr:graphicFrame><xdr:clientData/></xdr:twoCellAnchor>`;
4370
+ });
4371
+ return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><xdr:wsDr xmlns:xdr="http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing" xmlns:a="${NS_A}">` + anchors + "</xdr:wsDr>";
4372
+ },
4373
+ drawingRels(charts, firstChartNum) {
4374
+ let rels = "";
4375
+ charts.forEach((_, k) => {
4376
+ rels += `<Relationship Id="rId${k + 1}" Type="${NS_R}/chart" Target="../charts/chart${firstChartNum + k}.xml"/>`;
4377
+ });
4378
+ return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">' + rels + "</Relationships>";
4379
+ },
4380
+ chart(chart, workbook) {
4381
+ const type = chart.type;
4382
+ let series = "";
4383
+ chart.series.forEach((s, i) => {
4384
+ series += seriesXml(i, s, chart, workbook);
4385
+ });
4386
+ const labels = chart.dataLabels ? dataLabels(type, chart.format) : "";
4387
+ let plot;
4388
+ if (type === "line") {
4389
+ plot = '<c:lineChart><c:grouping val="standard"/><c:varyColors val="0"/>' + series + labels + '<c:marker val="1"/><c:axId val="10"/><c:axId val="20"/></c:lineChart>';
4390
+ } else if (type === "pie") {
4391
+ plot = '<c:pieChart><c:varyColors val="1"/>' + series + labels + '<c:firstSliceAng val="0"/></c:pieChart>';
4392
+ } else {
4393
+ plot = `<c:barChart><c:barDir val="${type === "bar" ? "bar" : "col"}"/><c:grouping val="clustered"/><c:varyColors val="0"/>` + series + labels + '<c:gapWidth val="80"/><c:axId val="10"/><c:axId val="20"/></c:barChart>';
4394
+ }
4395
+ const axesXml = type === "pie" ? "" : axes(type === "bar", chart.format);
4396
+ const legend = chart.legend ?? (chart.series.length > 1 || type === "pie");
4397
+ const title = chart.title !== null ? `<c:title><c:tx><c:rich><a:bodyPr/><a:lstStyle/><a:p><a:pPr><a:defRPr sz="1200" b="1"/></a:pPr><a:r><a:rPr lang="en-US" sz="1200" b="1"/><a:t>${text(chart.title)}</a:t></a:r></a:p></c:rich></c:tx><c:overlay val="0"/></c:title><c:autoTitleDeleted val="0"/>` : '<c:autoTitleDeleted val="1"/>';
4398
+ return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><c:chartSpace xmlns:c="${NS_C}" xmlns:a="${NS_A}" xmlns:r="${NS_R}"><c:roundedCorners val="0"/><c:chart>` + title + "<c:plotArea><c:layout/>" + plot + axesXml + "</c:plotArea>" + (legend ? '<c:legend><c:legendPos val="b"/><c:overlay val="0"/></c:legend>' : "") + '<c:plotVisOnly val="1"/><c:dispBlanksAs val="gap"/></c:chart><c:spPr><a:noFill/><a:ln><a:noFill/></a:ln></c:spPr>' + textProps("595959", 900) + "</c:chartSpace>";
4399
+ }
4400
+ };
4401
+ function marker2(tag, at2) {
4402
+ return `<xdr:${tag}><xdr:col>${at2[0]}</xdr:col><xdr:colOff>0</xdr:colOff><xdr:row>${at2[1]}</xdr:row><xdr:rowOff>0</xdr:rowOff></xdr:${tag}>`;
4403
+ }
4404
+ function seriesXml(i, s, chart, workbook) {
4405
+ const type = chart.type;
4406
+ const name = s.name !== null ? `<c:tx><c:v>${text(s.name)}</c:v></c:tx>` : "";
4407
+ let fill = "";
4408
+ if (s.color !== null && type !== "pie") {
4409
+ fill = type === "line" ? `<c:spPr><a:ln w="28575" cap="rnd"><a:solidFill><a:srgbClr val="${s.color}"/></a:solidFill><a:round/></a:ln></c:spPr>` : `<c:spPr><a:solidFill><a:srgbClr val="${s.color}"/></a:solidFill></c:spPr>`;
4410
+ }
4411
+ const invert = type === "column" || type === "bar" ? '<c:invertIfNegative val="0"/>' : "";
4412
+ const mark = type === "line" ? '<c:marker><c:symbol val="circle"/><c:size val="6"/></c:marker>' : "";
4413
+ const cat = chart.categories !== null ? "<c:cat>" + strRef(chart.categories, workbook) + "</c:cat>" : "";
4414
+ const smooth = type === "line" ? '<c:smooth val="0"/>' : "";
4415
+ return `<c:ser><c:idx val="${i}"/><c:order val="${i}"/>` + name + fill + invert + mark + cat + "<c:val>" + numRef(s.values, workbook) + "</c:val>" + smooth + "</c:ser>";
4416
+ }
4417
+ function strRef(range, workbook) {
4418
+ let pts = "";
4419
+ const vals = values(range, workbook);
4420
+ vals.forEach((v, idx) => {
4421
+ if (v !== null) pts += `<c:pt idx="${idx}"><c:v>${text(category(v))}</c:v></c:pt>`;
4422
+ });
4423
+ return `<c:strRef><c:f>${text(range.formula())}</c:f><c:strCache><c:ptCount val="${vals.length}"/>` + pts + "</c:strCache></c:strRef>";
4424
+ }
4425
+ function numRef(range, workbook) {
4426
+ let pts = "";
4427
+ const vals = values(range, workbook);
4428
+ vals.forEach((v, idx) => {
4429
+ if (typeof v === "number") pts += `<c:pt idx="${idx}"><c:v>${formatFloat(v)}</c:v></c:pt>`;
4430
+ });
4431
+ return `<c:numRef><c:f>${text(range.formula())}</c:f><c:numCache><c:formatCode>General</c:formatCode><c:ptCount val="${vals.length}"/>` + pts + "</c:numCache></c:numRef>";
4432
+ }
4433
+ function values(range, workbook) {
4434
+ const sheet = workbook.sheets.find((s) => s.name === range.sheet);
4435
+ return range.addresses().map((address) => {
4436
+ const cell = sheet?.cells.get(address);
4437
+ if (cell === void 0) return null;
4438
+ const v = cell.formula !== null ? cell.cachedValue : cell.value;
4439
+ return v === void 0 ? null : v;
4440
+ });
4441
+ }
4442
+ function category(v) {
4443
+ if (typeof v === "boolean") return v ? "TRUE" : "FALSE";
4444
+ if (typeof v === "number") return formatFloat(v);
4445
+ return v;
4446
+ }
4447
+ function dataLabels(type, format) {
4448
+ const pos = type === "line" ? "t" : type === "pie" ? "bestFit" : "outEnd";
4449
+ return "<c:dLbls>" + (format !== null ? `<c:numFmt formatCode="${attr2(format)}" sourceLinked="0"/>` : "") + "<c:spPr><a:noFill/><a:ln><a:noFill/></a:ln></c:spPr>" + textProps("404040", 900) + `<c:dLblPos val="${pos}"/><c:showLegendKey val="0"/><c:showVal val="1"/><c:showCatName val="0"/><c:showSerName val="0"/><c:showPercent val="0"/><c:showBubbleSize val="0"/></c:dLbls>`;
4450
+ }
4451
+ function axes(horizontal, format) {
4452
+ const catPos = horizontal ? "l" : "b";
4453
+ const valPos = horizontal ? "b" : "l";
4454
+ const numFmt = format !== null ? `<c:numFmt formatCode="${attr2(format)}" sourceLinked="0"/>` : '<c:numFmt formatCode="General" sourceLinked="1"/>';
4455
+ return `<c:catAx><c:axId val="10"/><c:scaling><c:orientation val="${horizontal ? "maxMin" : "minMax"}"/></c:scaling><c:delete val="0"/><c:axPos val="${catPos}"/><c:numFmt formatCode="General" sourceLinked="1"/><c:majorTickMark val="none"/><c:minorTickMark val="none"/><c:tickLblPos val="low"/><c:spPr><a:ln w="9525"><a:solidFill><a:srgbClr val="BFBFBF"/></a:solidFill></a:ln></c:spPr><c:crossAx val="20"/><c:crosses val="autoZero"/><c:auto val="1"/><c:lblAlgn val="ctr"/><c:lblOffset val="100"/><c:noMultiLvlLbl val="0"/></c:catAx><c:valAx><c:axId val="20"/><c:scaling><c:orientation val="minMax"/></c:scaling><c:delete val="0"/><c:axPos val="${valPos}"/><c:majorGridlines><c:spPr><a:ln w="9525"><a:solidFill><a:srgbClr val="E5E7EB"/></a:solidFill></a:ln></c:spPr></c:majorGridlines>` + numFmt + `<c:majorTickMark val="none"/><c:minorTickMark val="none"/><c:tickLblPos val="nextTo"/><c:spPr><a:ln><a:noFill/></a:ln></c:spPr><c:crossAx val="10"/><c:crosses val="${horizontal ? "max" : "autoZero"}"/><c:crossBetween val="between"/></c:valAx>`;
4456
+ }
4457
+ function textProps(color2, size) {
4458
+ return `<c:txPr><a:bodyPr/><a:lstStyle/><a:p><a:pPr><a:defRPr sz="${size}"><a:solidFill><a:srgbClr val="${color2}"/></a:solidFill></a:defRPr></a:pPr><a:endParaRPr lang="en-US"/></a:p></c:txPr>`;
4459
+ }
4460
+ function text(s) {
4461
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
4462
+ }
4463
+ function attr2(s) {
4464
+ return text(s).replace(/"/g, "&quot;").replace(/'/g, "&apos;");
4465
+ }
4466
+
3981
4467
  // src/writer/xlsx-writer.ts
3982
4468
  var encoder2 = new TextEncoder();
3983
4469
  function footerCodes(footer) {
@@ -3989,6 +4475,9 @@ function footerCodes(footer) {
3989
4475
  }
3990
4476
  return codes;
3991
4477
  }
4478
+ function drawingRelId(comments) {
4479
+ return comments ? "rId3" : "rId1";
4480
+ }
3992
4481
  function decimal(value) {
3993
4482
  const out = value.toFixed(4).replace(/0+$/, "").replace(/\.$/, "");
3994
4483
  return out === "" || out === "-0" ? "0" : out;
@@ -4012,14 +4501,29 @@ var XlsxWriter = class {
4012
4501
  add("xl/styles.xml", styles.toXml());
4013
4502
  add("docProps/core.xml", this.coreXml(workbook));
4014
4503
  add("docProps/app.xml", this.appXml());
4504
+ const firstChart = [];
4505
+ let next = 1;
4506
+ workbook.sheets.forEach((sheet, i) => {
4507
+ firstChart[i] = next;
4508
+ next += sheet.charts.length;
4509
+ });
4015
4510
  workbook.sheets.forEach((sheet, i) => {
4016
4511
  const n = i + 1;
4017
4512
  add(`xl/worksheets/sheet${n}.xml`, sheetXmls[i]);
4513
+ if (sheet.hasComments() || sheet.charts.length > 0) {
4514
+ add(`xl/worksheets/_rels/sheet${n}.xml.rels`, this.sheetRelsXml(n, sheet.hasComments(), sheet.charts.length > 0));
4515
+ }
4018
4516
  if (sheet.hasComments()) {
4019
- add(`xl/worksheets/_rels/sheet${n}.xml.rels`, this.sheetRelsXml(n));
4020
4517
  add(`xl/comments${n}.xml`, this.commentsXml(sheet));
4021
4518
  add(`xl/drawings/vmlDrawing${n}.vml`, this.vmlDrawingXml(sheet));
4022
4519
  }
4520
+ if (sheet.charts.length > 0) {
4521
+ add(`xl/drawings/drawing${n}.xml`, ChartXml.drawing(sheet.charts, firstChart[i]));
4522
+ add(`xl/drawings/_rels/drawing${n}.xml.rels`, ChartXml.drawingRels(sheet.charts, firstChart[i]));
4523
+ sheet.charts.forEach((chart, k) => {
4524
+ add(`xl/charts/chart${firstChart[i] + k}.xml`, ChartXml.chart(chart, workbook));
4525
+ });
4526
+ }
4023
4527
  });
4024
4528
  return zipSync(files);
4025
4529
  }
@@ -4033,6 +4537,16 @@ var XlsxWriter = class {
4033
4537
  const n = i + 1;
4034
4538
  overrides += `<Override PartName="/xl/comments${n}.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml"/>`;
4035
4539
  }
4540
+ let chartNum = 1;
4541
+ workbook.sheets.forEach((sheet, i) => {
4542
+ if (sheet.charts.length === 0) return;
4543
+ const n = i + 1;
4544
+ overrides += `<Override PartName="/xl/drawings/drawing${n}.xml" ContentType="application/vnd.openxmlformats-officedocument.drawing+xml"/>`;
4545
+ for (let k = 0; k < sheet.charts.length; k++) {
4546
+ overrides += `<Override PartName="/xl/charts/chart${chartNum}.xml" ContentType="application/vnd.openxmlformats-officedocument.drawingml.chart+xml"/>`;
4547
+ chartNum++;
4548
+ }
4549
+ });
4036
4550
  let defaults = '<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/>';
4037
4551
  if (sheetsWithComments.length > 0) {
4038
4552
  defaults += '<Default Extension="vml" ContentType="application/vnd.openxmlformats-officedocument.vmlDrawing"/>';
@@ -4061,8 +4575,16 @@ var XlsxWriter = class {
4061
4575
  rels += `<Relationship Id="rId${workbook.sheets.length + 1}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>`;
4062
4576
  return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">' + rels + "</Relationships>";
4063
4577
  }
4064
- sheetRelsXml(sheetNum) {
4065
- return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawing" Target="../drawings/vmlDrawing${sheetNum}.vml"/><Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments" Target="../comments${sheetNum}.xml"/></Relationships>`;
4578
+ /** Comments keep rId1 (vml) and rId2 (comments); the chart drawing takes the next id. */
4579
+ sheetRelsXml(sheetNum, comments, charts) {
4580
+ let rels = "";
4581
+ if (comments) {
4582
+ rels += `<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawing" Target="../drawings/vmlDrawing${sheetNum}.vml"/><Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments" Target="../comments${sheetNum}.xml"/>`;
4583
+ }
4584
+ if (charts) {
4585
+ rels += `<Relationship Id="${drawingRelId(comments)}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing" Target="../drawings/drawing${sheetNum}.xml"/>`;
4586
+ }
4587
+ return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">' + rels + "</Relationships>";
4066
4588
  }
4067
4589
  sheetXml(sheet, styles) {
4068
4590
  let sheetViewsXml = "";
@@ -4110,7 +4632,7 @@ var XlsxWriter = class {
4110
4632
  pageSetupXml = "<pageSetup" + (print.orientation !== null ? ` orientation="${print.orientation}"` : "") + (print.fitToWidth ? ' fitToWidth="1" fitToHeight="0"' : "") + "/>";
4111
4633
  }
4112
4634
  const headerFooterXml = print?.footer ? `<headerFooter><oddFooter>${this.escape(footerCodes(print.footer))}</oddFooter></headerFooter>` : "";
4113
- return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?><worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">' + sheetPrXml + sheetViewsXml + colsXml + `<sheetData>${rowsXml}</sheetData>` + mergesXml + pageSetupXml + headerFooterXml + legacyDrawing + "</worksheet>";
4635
+ return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?><worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">' + sheetPrXml + sheetViewsXml + colsXml + `<sheetData>${rowsXml}</sheetData>` + mergesXml + pageSetupXml + headerFooterXml + (sheet.charts.length > 0 ? `<drawing r:id="${drawingRelId(sheet.hasComments())}"/>` : "") + legacyDrawing + "</worksheet>";
4114
4636
  }
4115
4637
  cellXml(cell, styles) {
4116
4638
  const ref = cell.address;
@@ -4150,8 +4672,8 @@ var XlsxWriter = class {
4150
4672
  const author = entry.comment.author ?? "Author";
4151
4673
  if (!authors.includes(author)) authors.push(author);
4152
4674
  const authorIdx = authors.indexOf(author);
4153
- const text = this.escape(entry.comment.text);
4154
- commentList += `<comment ref="${entry.address}" authorId="${authorIdx}"><text><r><t xml:space="preserve">${text}</t></r></text></comment>`;
4675
+ const text2 = this.escape(entry.comment.text);
4676
+ commentList += `<comment ref="${entry.address}" authorId="${authorIdx}"><text><r><t xml:space="preserve">${text2}</t></r></text></comment>`;
4155
4677
  }
4156
4678
  let authorsXml = "<authors>";
4157
4679
  for (const a of authors) authorsXml += `<author>${this.escape(a)}</author>`;
@@ -5031,19 +5553,19 @@ function canonEntries(root) {
5031
5553
  continue;
5032
5554
  }
5033
5555
  stack.pop();
5034
- const text = top.list ? "[" + top.parts.join(",") + "]" : "{" + top.parts.join(",") + "}";
5556
+ const text2 = top.list ? "[" + top.parts.join(",") + "]" : "{" + top.parts.join(",") + "}";
5035
5557
  const parent = stack[stack.length - 1];
5036
5558
  if (parent === void 0) {
5037
- return text;
5559
+ return text2;
5038
5560
  }
5039
- parent.parts.push(member(parent, top.key, text));
5561
+ parent.parts.push(member(parent, top.key, text2));
5040
5562
  }
5041
5563
  }
5042
5564
  function entriesFor(value) {
5043
5565
  return Array.isArray(value) ? Array.from(value, (item, i) => [String(i), item]) : entriesOf(value);
5044
5566
  }
5045
- function member(frame, key, text) {
5046
- return frame.list ? text : quote(key) + ":" + text;
5567
+ function member(frame, key, text2) {
5568
+ return frame.list ? text2 : quote(key) + ":" + text2;
5047
5569
  }
5048
5570
  function scalar(value) {
5049
5571
  switch (typeof value) {
@@ -5061,11 +5583,11 @@ function scalar(value) {
5061
5583
  return JSON.stringify(value) ?? "null";
5062
5584
  }
5063
5585
  var LONE_SURROGATE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/;
5064
- function quote(text) {
5065
- if (LONE_SURROGATE.test(text)) {
5586
+ function quote(text2) {
5587
+ if (LONE_SURROGATE.test(text2)) {
5066
5588
  throw unencodable("Single unpaired UTF-16 surrogate");
5067
5589
  }
5068
- return JSON.stringify(text);
5590
+ return JSON.stringify(text2);
5069
5591
  }
5070
5592
  function unencodable(reason) {
5071
5593
  return new TypeError(`[holy-sheet] cannot compare a value JSON cannot hold: ${reason}`);
@@ -5234,6 +5756,43 @@ var holy_sheet_schema_default = {
5234
5756
  propertyNames: { pattern: "^[1-9][0-9]*$" },
5235
5757
  additionalProperties: { type: "number", minimum: 0 }
5236
5758
  },
5759
+ charts: {
5760
+ type: "array",
5761
+ description: "Native Excel charts over CELL RANGES on this sheet: edit a figure and the chart follows. Use one whenever the sheet compares values -- a board reader sees the comparison before the numbers. Keep them quiet: a title that states the point, dataLabels with a format, one colour per series.",
5762
+ items: {
5763
+ type: "object",
5764
+ additionalProperties: false,
5765
+ required: ["type", "series"],
5766
+ properties: {
5767
+ type: { type: "string", enum: ["column", "bar", "line", "pie"], description: "column compares categories; bar does it horizontally (long labels, ranked lists); line shows change over time; pie shows parts of ONE whole (at most ~5 slices)." },
5768
+ title: { type: "string" },
5769
+ categories: { type: "string", description: `The labels along the axis (or the pie's slices), as a range: "A5:A7". Another sheet: "'Data'!A5:A7".` },
5770
+ series: {
5771
+ type: "array",
5772
+ minItems: 1,
5773
+ items: {
5774
+ type: "object",
5775
+ additionalProperties: false,
5776
+ required: ["values"],
5777
+ properties: {
5778
+ name: { type: "string", description: "Shown in the legend." },
5779
+ values: { type: "string", description: 'The numbers, as a range the same length as categories: "B5:B7".' },
5780
+ color: { type: "string", description: "#RRGGBB. Ignored for a pie, whose slices take a palette." }
5781
+ }
5782
+ }
5783
+ },
5784
+ position: {
5785
+ type: "object",
5786
+ additionalProperties: false,
5787
+ description: 'Where the chart sits: from its top-left cell to its bottom-right cell, {"from": "E2", "to": "L18"}. Omitted: to the right of the data, 8 columns by 16 rows.',
5788
+ properties: { from: { type: "string" }, to: { type: "string" } }
5789
+ },
5790
+ dataLabels: { type: "boolean", description: "Print each value on the chart. Usually right for a board chart." },
5791
+ legend: { type: "boolean", description: "Defaults to on for several series or a pie, off for one series." },
5792
+ format: { type: "string", description: 'Excel number format for the value axis and labels: "$#,##0", "0.0%", "$#,##0,K".' }
5793
+ }
5794
+ }
5795
+ },
5237
5796
  print: {
5238
5797
  type: "object",
5239
5798
  additionalProperties: false,
@@ -5460,7 +6019,7 @@ var holy_sheet_schema_default = {
5460
6019
  };
5461
6020
 
5462
6021
  // src/agent.ts
5463
- var VERSION = "2.7.0";
6022
+ var VERSION = "2.8.0";
5464
6023
  function toU8(input) {
5465
6024
  return input instanceof Uint8Array ? input : new Uint8Array(input);
5466
6025
  }