diff --git a/README.en.md b/README.en.md
index 8194923..1d7e952 100644
--- a/README.en.md
+++ b/README.en.md
@@ -44,7 +44,7 @@ right here, with the URL, the header and the data never passing through our serv
-
+
@@ -65,7 +65,7 @@ self-hosted, straight from PostgreSQL/MySQL — one saved read-only query, and n
and full-fidelity Excel/PDF export, where a chart exported to `.xlsx` is a real, editable chart
bound to its cells, because the OOXML chart parts are written by hand (ExcelJS writes none). Plus optional
bring-your-own-backend cloud save and live co-editing over it — presence, last-writer-wins with the loser told, and
-an undo that does not erase the other person's work. Bilingual UI (Thai/English), 2013 automated tests.
+an undo that does not erase the other person's work. Bilingual UI (Thai/English), 2014 automated tests.
---
@@ -109,7 +109,7 @@ Want the harder parts: [embedding a Thai font in the PDF, with stacked tone mark
---
-### 🧪 What 2013 passing tests could not catch
+### 🧪 What 2014 passing tests could not catch
Every test of the assistant **mocks the model** — it returns what I imagined it would. Put a real
API key behind it, ask fourteen ordinary questions, and **six answers used functions this engine
@@ -120,7 +120,7 @@ Then **the first fix made it worse.** The rule started as "give the closest form
allows", so _"join all the names into one line"_ came back as `=SUM(A2:A20)` — `0` in the cell, no
error, nothing to notice. **A visible `#NAME?` traded for an invisible wrong number.**
-**And it happened again, in a different place.** With every gate green — 2013 tests, `axe` clean on
+**And it happened again, in a different place.** With every gate green — 2014 tests, `axe` clean on
both pages at two widths — an hour of clicking through the public build the way a first-time visitor
would found three things no gate can see:
@@ -395,7 +395,7 @@ Other available commands:
| `npm run build` | Build a production bundle |
| `npm run start` | Run the production build (run `npm run build` first) |
| `npm run lint` | Check code quality with ESLint |
-| `npm test` | Run the 2013-case Vitest suite |
+| `npm test` | Run the 2014-case Vitest suite |
| `npm run check:readme` | Check the READMEs still match the code (links/images/test count/new modules/both languages) |
| `npm run check:screens` | Figures printed on a screenshot still match the source |
| `npm run check:rls` | Two real accounts against your own Supabase: does the database refuse what the policies say it should (needs env) |
@@ -1482,7 +1482,11 @@ ExcelJS writes no chart XML at all (`addImage` is its entire drawing API), so th
are written by hand and spliced into the finished package; see `xlsxChartXml.ts` and
`xlsxCharts.ts`. If that splice ever fails the export falls back to embedding pictures, because a
package with a dangling relationship is one Excel calls corrupt and refuses to open — a much worse
-outcome than a chart that has stopped updating.
+outcome than a chart that has stopped updating. Each series refers to the column it was actually
+read from (#59). A text column between the labels and the numbers, such as "Notes" between the
+branch and Q1, is not a series but still takes up a column; the references used to be counted by
+series, so Excel drew Notes where Q1 should be and Q1 where Q2 should be, while the app's own chart
+was right.
**Not supported:** charts in the PDF are still pictures (jsPDF has no chart primitive). A pie still
draws one series at a time. The chart parts carry no colour or theme styling, so whichever app opens
@@ -3041,7 +3045,7 @@ architecture behind it.
| `@anthropic-ai/sdk` | Connects to the Claude API for the AI assistant |
| `lucide-react` | UI icons |
| `clsx` | Conditional className composition |
-| `vitest` | Unit tests for the formula engine, sort logic, JSON-to-table conversion, pagination, rate limiting, templates, file fidelity, conditional formatting and live blocks (2013 cases) |
+| `vitest` | Unit tests for the formula engine, sort logic, JSON-to-table conversion, pagination, rate limiting, templates, file fidelity, conditional formatting and live blocks (2014 cases) |
> **Note:** No off-the-shelf formula library (e.g. HyperFormula) is used — the **formula engine is hand-written**
> (tokenizer, parser, evaluator, and functions) to keep full control over its behavior. See
@@ -4096,13 +4100,13 @@ the framework bundle itself, which isn't a trade worth making here. Written down
## 🧪 Testing
```bash
-npm test # 2013 cases across 145 files, via Vitest
+npm test # 2014 cases across 145 files, via Vitest
```
Testing is focused on the **formula engine, sort logic, JSON-to-table conversion, pagination, rate-limit backoff, Excel templates and live-block placement** — pure functions with no React/DOM dependency, so
they run fast and give high confidence.
-**But not one of those 2013 cases opens the app**, and nearly every bug this project found by hand lived in
+**But not one of those 2014 cases opens the app**, and nearly every bug this project found by hand lived in
the wiring *between* pieces that all passed their tests — the toolbar's "+ row" called `addRow`, which
announced nothing, while `insertRowAtSelection` next to it announced correctly (both tested) · the AI
assistant sent a range including its text header, because the context builder read raw `sheet.cells`
@@ -4221,10 +4225,10 @@ once; disable `ArrowRight` in the grid and two assertions in the third fail. (Th
second one stayed green: the `case` I inserted landed *after* the existing `case "ArrowRight"` and was dead
code. Proving a gate means checking that the thing you meant to break actually broke.)
-> **2013 tests passed, and 43% of the assistant's answers were unusable** — because those tests mock
+> **2014 tests passed, and 43% of the assistant's answers were unusable** — because those tests mock
> the model, so it returns what the test author imagined. A test count says what you thought to ask,
> not whether you asked enough. Only a real API key found this: see
-> [What 2013 passing tests could not catch](#-what-2013-passing-tests-could-not-catch), repeatable
+> [What 2014 passing tests could not catch](#-what-2014-passing-tests-could-not-catch), repeatable
> with `npm run check:ai`.
| File | Cases | Tests |
diff --git a/README.md b/README.md
index 5722908..566cfd6 100644
--- a/README.md
+++ b/README.md
@@ -40,7 +40,7 @@
-
+
@@ -61,7 +61,7 @@ and saying so when data came back incomplete. A range can be given a name (in Th
what it means, and a cell can carry a rule about what may go in it, refused before it is written rather than
flagged afterwards. Conditional formatting re-colours cells from their current values — comparisons,
top/bottom ranks, colour scales and data bars — and round-trips through Excel's own rule format. Bilingual UI
-(Thai/English), 2013 automated tests. บันทึกบนคลาวด์และการแก้พร้อมกันหลายคนมีให้เลือกใช้ได้ โดยต่อ backend ของคุณเอง
+(Thai/English), 2014 automated tests. บันทึกบนคลาวด์และการแก้พร้อมกันหลายคนมีให้เลือกใช้ได้ โดยต่อ backend ของคุณเอง
---
@@ -106,7 +106,7 @@ top/bottom ranks, colour scales and data bars — and round-trips through Excel'
---
-### 🧪 สิ่งที่เทสต์ 2013 เคสจับไม่ได้
+### 🧪 สิ่งที่เทสต์ 2014 เคสจับไม่ได้
เทสต์ของผู้ช่วย AI ใช้ **mock ทั้งหมด** — มันตอบตามที่ผมคิดว่ามันจะตอบ พอเอา API key จริงมายิงคำถามธรรมดา
14 ข้อ **6 คำตอบใช้ฟังก์ชันที่เอนจินนี้ไม่มี** (`TEXTJOIN` `FIND` `RANK.EQ` `SUMPRODUCT` `CEILING` `CHAR`)
@@ -116,7 +116,7 @@ top/bottom ranks, colour scales and data bars — and round-trips through Excel'
มันจึงตอบคำถาม *"ต่อชื่อทั้งหมดเป็นบรรทัดเดียว"* ด้วย `=SUM(A2:A20)` ซึ่งได้ `0` โดยไม่มีอะไรบอกว่าผิด
**เปลี่ยน error ที่มองเห็น เป็นตัวเลขผิดที่มองไม่เห็น**
-**แล้วมันก็เกิดอีก คนละที่** ตอนที่ทุกด่านเขียวหมด — 2013 เทสต์ผ่าน, `axe` ไม่มี violation ทั้งสองหน้า
+**แล้วมันก็เกิดอีก คนละที่** ตอนที่ทุกด่านเขียวหมด — 2014 เทสต์ผ่าน, `axe` ไม่มี violation ทั้งสองหน้า
ที่สองความกว้าง — ผมลองไล่คลิก build สาธารณะแบบคนที่เพิ่งเข้ามาครั้งแรกอยู่ชั่วโมงหนึ่ง แล้วเจอสามอย่าง
ที่ไม่มีด่านไหนมองเห็น:
@@ -381,7 +381,7 @@ npm run dev
| `npm run build` | build เป็นเวอร์ชัน production |
| `npm run start` | รันเวอร์ชันที่ build แล้ว (ต้อง `npm run build` ก่อน) |
| `npm run lint` | ตรวจสอบคุณภาพโค้ดด้วย ESLint |
-| `npm test` | รัน unit test 2013 เคสด้วย Vitest |
+| `npm test` | รัน unit test 2014 เคสด้วย Vitest |
| `npm run check:readme` | ตรวจว่า README ยังตรงกับโค้ด (ลิงก์/ภาพ/จำนวนเทสต์/โมดูลใหม่/สองภาษาตรงกัน) |
| `npm run check:screens` | ตัวเลขที่พิมพ์อยู่บนภาพหน้าจอยังตรงกับซอร์สไหม |
| `npm run check:rls` | ยิงจริงสองบัญชีบน Supabase ของคุณ ว่าฐานข้อมูลปฏิเสธตามที่ policy เขียนไว้ไหม (ต้องตั้ง env) |
@@ -1306,6 +1306,9 @@ conditional formatting ไม่นับมันเป็นตัวเลข
เราจึงเขียน OOXML chart part เอง แล้วแทรกลงในไฟล์ zip ที่ ExcelJS สร้างเสร็จ — ดู `xlsxChartXml.ts`
กับ `xlsxCharts.ts` ถ้าการแทรกล้มเหลวจะถอยไปฝังเป็นรูปภาพแบบเดิม เพราะไฟล์ที่มี relationship ห้อย
Excel จะบอกว่า "เสียหาย" แล้วเปิดไม่ขึ้นเลย ซึ่งแย่กว่ากราฟที่หยุดอัปเดตมาก
+แต่ละชุดข้อมูลอ้างคอลัมน์ที่มันถูกอ่านมาจริง (#59) คอลัมน์ข้อความที่คั่นอยู่ เช่น "หมายเหตุ" ระหว่างชื่อสาขากับ Q1
+ไม่ใช่ชุดข้อมูลแต่ยังกินหนึ่งคอลัมน์ เดิมอ้างอิงนับตามลำดับชุด กราฟใน Excel จึงวาดหมายเหตุแทน Q1 และ Q1 แทน Q2
+ทั้งที่กราฟในแอปถูก
**ยังไม่รองรับ:** กราฟใน PDF ยังเป็นรูปภาพ (jsPDF ไม่มีกราฟของตัวเอง) · กราฟวงกลมยังวาดได้ทีละชุดข้อมูล ·
ไม่ได้ใส่สี/ธีมลงใน chart part ปล่อยให้โปรแกรมที่เปิดใช้ค่าเริ่มต้นของตัวเอง
@@ -2644,7 +2647,7 @@ stack ของ `font-mono` จึงต่อท้ายด้วย Plex Sans
| `@anthropic-ai/sdk` | เชื่อมต่อ Claude API สำหรับผู้ช่วย AI |
| `lucide-react` | ไอคอน UI |
| `clsx` | รวม className แบบมีเงื่อนไข |
-| `vitest` | unit test เอนจินคำนวณสูตร, ตรรกะเรียงข้อมูล, แปลง JSON เป็นตาราง, การแบ่งหน้า/rate limit, แม่แบบ/รูปแบบจากไฟล์ และบล็อกข้อมูลสด (2013 เคส) |
+| `vitest` | unit test เอนจินคำนวณสูตร, ตรรกะเรียงข้อมูล, แปลง JSON เป็นตาราง, การแบ่งหน้า/rate limit, แม่แบบ/รูปแบบจากไฟล์ และบล็อกข้อมูลสด (2014 เคส) |
> **หมายเหตุ:** ไม่ได้ใช้ไลบรารีคำนวณสูตรสำเร็จรูป (เช่น HyperFormula) แต่เขียน **เอนจินคำนวณสูตรขึ้นเอง**
> ทั้ง tokenizer, parser, evaluator และฟังก์ชันต่างๆ เพื่อควบคุมพฤติกรรมได้เต็มที่ ดูรายละเอียดที่หัวข้อ
@@ -3553,13 +3556,13 @@ B อ่านและแก้ได้ แต่ยังยึดเป็
## 🧪 การทดสอบ
```bash
-npm test # 2013 เคส ใน 145 ไฟล์ ด้วย Vitest
+npm test # 2014 เคส ใน 145 ไฟล์ ด้วย Vitest
```
โฟกัสเทสต์ไปที่ **เอนจินคำนวณสูตร, ตรรกะเรียงข้อมูล, การแปลง JSON เป็นตาราง, การไล่ดึงหน้าถัดไป, การถอยเมื่อโดน rate limit, แม่แบบจากไฟล์ Excel, กฎจัดรูปแบบตามเงื่อนไข และการวางบล็อกข้อมูลสด** — ส่วนที่เป็น pure function ล้วน ไม่ต้องพึ่ง React/DOM
จึงเทสต์ได้เร็วและมั่นใจได้สูง
-**แต่ 2013 เคสนั้นไม่มีสักเคสที่เปิดแอป** และบั๊กที่โปรเจกต์นี้เจอด้วยการนั่งกดเอง อยู่ที่รอยต่อระหว่างชิ้นส่วนที่ผ่านเทสต์
+**แต่ 2014 เคสนั้นไม่มีสักเคสที่เปิดแอป** และบั๊กที่โปรเจกต์นี้เจอด้วยการนั่งกดเอง อยู่ที่รอยต่อระหว่างชิ้นส่วนที่ผ่านเทสต์
ทุกตัวแทบทั้งหมด — ปุ่ม "+ แถว" บนแถบเครื่องมือเรียก `addRow` ที่ไม่ประกาศอะไรเลย ขณะที่ `insertRowAtSelection`
ข้าง ๆ ประกาศถูกต้อง (ทั้งคู่เทสต์ผ่าน) · ผู้ช่วย AI ส่งช่วงที่รวมหัวคอลัมน์ไปด้วย เพราะตัวประกอบ context อ่าน
`sheet.cells` ที่เป็นสูตรดิบแทนค่าที่คำนวณแล้ว (ทั้งคู่เทสต์ผ่าน) · ปุ่มใหม่หนึ่งปุ่มดันปุ่มสลับภาษาตกขอบจอไป 42px
@@ -3659,9 +3662,9 @@ seed ถูกปักไว้ ด่านที่แดงจึงแด
ในตารางแล้วสองข้อในflow ที่สามตกทันที (ครั้งแรกที่ลองปิดยังเขียวอยู่ เพราะ `case` ที่แทรกเข้าไปไปอยู่หลัง
`case "ArrowRight"` เดิม กลายเป็นโค้ดตาย — การพิสูจน์ต้องเช็กด้วยว่าของที่ตั้งใจทำให้พังนั้นพังจริง)
-> **2013 เคสผ่านหมด แต่ 43% ของคำตอบจากผู้ช่วย AI ใช้ไม่ได้** — เพราะเทสต์ส่วนนั้น mock ตัวโมเดลไว้
+> **2014 เคสผ่านหมด แต่ 43% ของคำตอบจากผู้ช่วย AI ใช้ไม่ได้** — เพราะเทสต์ส่วนนั้น mock ตัวโมเดลไว้
> มันจึงตอบตามที่คนเขียนเทสต์คิดว่ามันจะตอบ จำนวนเทสต์บอกได้แค่ว่าเราถามอะไรไปบ้าง ไม่ได้บอกว่าเราถาม
-> ครบหรือยัง เรื่องนี้เจอได้ก็ต่อเมื่อเอา API key จริงมายิง — ดู [สิ่งที่เทสต์ 2013 เคสจับไม่ได้](#-สิ่งที่เทสต์-2013-เคสจับไม่ได้)
+> ครบหรือยัง เรื่องนี้เจอได้ก็ต่อเมื่อเอา API key จริงมายิง — ดู [สิ่งที่เทสต์ 2014 เคสจับไม่ได้](#-สิ่งที่เทสต์-2014-เคสจับไม่ได้)
> และรันซ้ำเองได้ด้วย `npm run check:ai`
| ไฟล์ | เคส | ทดสอบอะไร |
diff --git a/public/screenshots/27-landing-stats.png b/public/screenshots/27-landing-stats.png
index 8321550..68eac2d 100644
Binary files a/public/screenshots/27-landing-stats.png and b/public/screenshots/27-landing-stats.png differ
diff --git a/public/screenshots/en/27-landing-stats.png b/public/screenshots/en/27-landing-stats.png
index 081a6ce..343aeeb 100644
Binary files a/public/screenshots/en/27-landing-stats.png and b/public/screenshots/en/27-landing-stats.png differ
diff --git a/public/screenshots/screenshots.json b/public/screenshots/screenshots.json
index b9ad01a..af4988d 100644
--- a/public/screenshots/screenshots.json
+++ b/public/screenshots/screenshots.json
@@ -5,7 +5,7 @@
"27-landing-stats.png": {
"paletteFormulas": 37,
"engineFunctions": 66,
- "tests": 2013,
+ "tests": 2014,
"securityTests": 325
},
"38-landing-problems.png": {
@@ -21,7 +21,7 @@
"en/27-landing-stats.png": {
"paletteFormulas": 37,
"engineFunctions": 66,
- "tests": 2013,
+ "tests": 2014,
"securityTests": 325
},
"en/38-landing-problems.png": {
diff --git a/public/social-preview.png b/public/social-preview.png
index 9352101..956e80b 100644
Binary files a/public/social-preview.png and b/public/social-preview.png differ
diff --git a/src/app/opengraph-image.tsx b/src/app/opengraph-image.tsx
index 88ff13f..bf61053 100644
--- a/src/app/opengraph-image.tsx
+++ b/src/app/opengraph-image.tsx
@@ -29,7 +29,7 @@ export default function OgImage() {
const stats = [
["66", "engine functions"],
["37", "palette formulas"],
- ["2013", "automated tests"],
+ ["2014", "automated tests"],
["325", "of them security"],
["0", "formula libraries"],
];
diff --git a/src/i18n/en.ts b/src/i18n/en.ts
index c6579d3..4e113d6 100644
--- a/src/i18n/en.ts
+++ b/src/i18n/en.ts
@@ -432,7 +432,7 @@ export const en: Messages = {
stats: [
{ value: "37", label: "ready-made formulas" },
{ value: "66", label: "engine functions" },
- { value: "2013", label: "automated tests" },
+ { value: "2014", label: "automated tests" },
{ value: "325", label: "security tests" },
{ value: "0", label: "formula libraries used" },
],
diff --git a/src/i18n/th.ts b/src/i18n/th.ts
index 5ac644b..c17e278 100644
--- a/src/i18n/th.ts
+++ b/src/i18n/th.ts
@@ -432,7 +432,7 @@ export const th: Messages = {
stats: [
{ value: "37", label: "สูตรพร้อมใช้" },
{ value: "66", label: "ฟังก์ชันในเอนจิน" },
- { value: "2013", label: "เทสต์อัตโนมัติ" },
+ { value: "2014", label: "เทสต์อัตโนมัติ" },
{ value: "325", label: "เทสต์ด้านความปลอดภัย" },
{ value: "0", label: "ไลบรารีคำนวณสูตร" },
],
diff --git a/src/lib/charts.test.ts b/src/lib/charts.test.ts
index 05ec430..8aa412a 100644
--- a/src/lib/charts.test.ts
+++ b/src/lib/charts.test.ts
@@ -93,20 +93,20 @@ describe("reading a range into a chart", () => {
describe("the value axis", () => {
it("always includes zero, so a bar's length means what it looks like", () => {
- expect(valueExtent([{ name: "a", points: [80, 100] }])).toEqual({ min: 0, max: 100 });
+ expect(valueExtent([{ name: "a", column: 1, points: [80, 100] }])).toEqual({ min: 0, max: 100 });
});
it("extends below zero when the data does", () => {
- expect(valueExtent([{ name: "a", points: [-30, 50] }])).toEqual({ min: -30, max: 50 });
+ expect(valueExtent([{ name: "a", column: 1, points: [-30, 50] }])).toEqual({ min: -30, max: 50 });
});
it("never returns a zero-width span, which would divide by nothing when scaling", () => {
- const flat = valueExtent([{ name: "a", points: [0, 0] }]);
+ const flat = valueExtent([{ name: "a", column: 1, points: [0, 0] }]);
expect(flat.max).toBeGreaterThan(flat.min);
});
it("handles a series with no numbers at all", () => {
- const none = valueExtent([{ name: "a", points: [null, null] }]);
+ const none = valueExtent([{ name: "a", column: 1, points: [null, null] }]);
expect(none.max).toBeGreaterThan(none.min);
});
});
diff --git a/src/lib/charts.ts b/src/lib/charts.ts
index fe5c0ee..47dd389 100644
--- a/src/lib/charts.ts
+++ b/src/lib/charts.ts
@@ -101,6 +101,10 @@ export function clampFrame(frame: ChartFrame, canvas: { width: number; height: n
export interface ChartSeries {
name: string;
+ /** Which column of the range it came from, counted from the range's first column. A column with
+ * no numbers is not a series, so this is not always the series' position after the labels, and
+ * an exported chart that assumed it was pointed at the wrong column (#59). */
+ column: number;
/** One value per label; a cell that holds no number becomes null and leaves a gap. */
points: (number | null)[];
}
@@ -192,7 +196,7 @@ export function chartDataFrom(values: FormulaValue[][], range: SheetRange, dateT
// A column with nothing numeric in it is not a series; plotting it would draw a flat nothing.
if (points.every((p) => p === null)) continue;
const headerName = usedHeaderRow ? cellText(cells[0][c]).trim() : "";
- series.push({ name: headerName || `${c - firstDataCol + 1}`, points });
+ series.push({ name: headerName || `${c - firstDataCol + 1}`, column: c, points });
}
// Labels name points; with nothing to plot they name nothing, so an empty range stays empty
diff --git a/src/lib/xlsxChartXml.test.ts b/src/lib/xlsxChartXml.test.ts
index 4cd3f1f..bccc840 100644
--- a/src/lib/xlsxChartXml.test.ts
+++ b/src/lib/xlsxChartXml.test.ts
@@ -132,8 +132,8 @@ describe("seriesRefsFrom", () => {
const data: ChartData = {
labels: ["Jan", "Feb", "Mar"],
series: [
- { name: "Sales", points: [1, 2, 3] },
- { name: "Profit", points: [4, 5, 6] },
+ { name: "Sales", column: 1, points: [1, 2, 3] },
+ { name: "Profit", column: 2, points: [4, 5, 6] },
],
usedHeaderRow: true,
usedLabelColumn: true,
diff --git a/src/lib/xlsxChartXml.ts b/src/lib/xlsxChartXml.ts
index 576bf9b..973b0fc 100644
--- a/src/lib/xlsxChartXml.ts
+++ b/src/lib/xlsxChartXml.ts
@@ -202,11 +202,10 @@ export function seriesRefsFrom(
labelCol === null ? undefined : colRef(sheetName, columnLetter(labelCol), firstDataRow, lastDataRow);
const chosen = pie ? data.series.slice(seriesIndex, seriesIndex + 1) : data.series;
- return chosen.map((s, i) => {
- const sourceIndex = pie ? seriesIndex : i;
- // Series sit in the range's columns, after the label column when there is one.
- const col = range.startCol + (data.usedLabelColumn ? 1 : 0) + sourceIndex;
- const letter = columnLetter(col);
+ return chosen.map((s) => {
+ // The column the series was read from, not its position among the series: a text column
+ // between two numeric ones is skipped as a series but still takes up a column (#59).
+ const letter = columnLetter(range.startCol + s.column);
return {
name: s.name,
nameRef: data.usedHeaderRow
diff --git a/src/lib/xlsxCharts.test.ts b/src/lib/xlsxCharts.test.ts
index 519543b..af0246c 100644
--- a/src/lib/xlsxCharts.test.ts
+++ b/src/lib/xlsxCharts.test.ts
@@ -74,6 +74,36 @@ describe("charts in the exported .xlsx", () => {
expect(Object.keys(zip.files).some((n) => n.startsWith("xl/media/"))).toBe(false);
});
+ // #59: a text column between the labels and the numbers is not a series, but it still takes up
+ // a column. The references used to be counted from the series' position, so Q1 pointed at
+ // Notes and Q2 at Q1, and Excel, which reads the references, drew the wrong numbers.
+ it("points each series at its own column when a text column sits between them (#59)", async () => {
+ const sheet = createEmptySheet();
+ const rows = [
+ ["Branch", "Notes", "Q1", "Q2"],
+ ["North", "new", "120", "130"],
+ ["South", "ok", "150", "140"],
+ ["East", "late", "90", "95"],
+ ["West", "ok", "60", "70"],
+ ];
+ rows.forEach((r, ri) => r.forEach((v, ci) => (sheet.cells[ri][ci] = v)));
+ const range = { startRow: 0, startCol: 0, endRow: 4, endCol: 3 };
+ sheet.charts = [
+ { id: "bar", kind: "bar", range, anchor: { col: 5, row: 1, dx: 0, dy: 0, w: 480, h: 300 } },
+ { id: "pie", kind: "pie", range, seriesIndex: 1, anchor: { col: 5, row: 18, dx: 0, dy: 0, w: 480, h: 300 } },
+ ];
+ const zip = await exportZip([{ name: "Sheet1", sheet, computed: computeSheet(sheet) }]);
+ const bar = await zip.file("xl/charts/chart1.xml")!.async("string");
+ expect(bar).toContain("Sheet1!$C$1");
+ expect(bar).toContain("Sheet1!$C$2:$C$5");
+ expect(bar).toContain("Sheet1!$D$1");
+ expect(bar).toContain("Sheet1!$D$2:$D$5");
+ expect(bar).not.toContain("$B$2:$B$5");
+ const pie = await zip.file("xl/charts/chart2.xml")!.async("string");
+ expect(pie).toContain("Sheet1!$D$2:$D$5");
+ expect(pie).toContain("Sheet1!$A$2:$A$5");
+ });
+
// The failure this guards against is the worst one: a package with a relationship pointing at a
// part that isn't there opens as "corrupt" with nothing saying which part is missing.
it("leaves no dangling relationship anywhere in the package", async () => {