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