Merged Cells docs

This commit is contained in:
SheetJS 2024-07-15 21:40:51 -04:00
parent b0dbe7dc43
commit df36a616a9
13 changed files with 307 additions and 44 deletions

@ -1089,7 +1089,7 @@ When the app is loaded, the data will be displayed in rows.
[^7]: See [`sheet_to_html` in "Utilities"](/docs/api/utilities/html#html-table-output)
[^8]: See [`sheet_to_json` in "Utilities"](/docs/api/utilities/array#array-output)
[^9]: See [`sheet_to_json` in "Utilities"](/docs/api/utilities/array#array-output)
[^10]: See [`!merges` in "Sheet Objects"](/docs/csf/sheet#worksheet-object)
[^10]: See ["Merged Cells" in "SheetJS Data Model"](/docs/csf/features/merges)
[^11]: See ["Column Names" in "Addresses and Ranges"](/docs/csf/general#column-names)
[^12]: See ["Array of Objects" in "ReactJS"](/docs/demos/frontend/react#array-of-objects)
[^13]: See ["Running on Device"](https://reactnative.dev/docs/running-on-device) in the React Native documentation for more details.

@ -35,7 +35,7 @@ This demo was tested in the following configurations:
| Date | Platform |
|:-----------|:--------------------------------------------------------------|
| 2024-06-19 | Apple M2 Max 12-Core CPU + 30-Core GPU (32 GB unified memory) |
| 2024-07-15 | Apple M2 Max 12-Core CPU + 30-Core GPU (32 GB unified memory) |
| 2024-07-14 | NVIDIA RTX 4090 (24 GB VRAM) + i9-10910 (128 GB RAM) |
| 2024-07-14 | NVIDIA RTX 4080 SUPER (16 GB VRAM) + i9-10910 (128 GB RAM) |
@ -819,6 +819,16 @@ The filtered results should match the following screenshot:
![Expected Results](pathname:///loadofsheet/expected.png)
:::tip pass
The [SheetJS model](/docs/csf) exposes [formulae](/docs/csf/features/formulae)
and other features.
[SheetJS Pro](https://sheetjs.com/pro) builds expose cell styling, images,
charts, tables, and other features.
:::
[^1]: See ["How to load CSV data"](https://js.langchain.com/v0.2/docs/how_to/document_loader_csv) in the LangChain documentation
[^2]: See [`readFile` in "Reading Files"](/docs/api/parse-options)
[^3]: See ["SheetJS Data Model"](/docs/csf/)

@ -537,7 +537,7 @@ will generate a workbook that can be opened in a spreadsheet editor.
The main disadvantage of the Array of Objects approach is the specific nature
of the columns. For more general use, passing around an Array of Arrays works.
However, this does not handle merge cells well!
However, this does not handle merge cells[^6] well!
The [`sheet_to_html`](/docs/api/utilities/html#html-table-output) function
generates HTML that is aware of merges and other worksheet features. To add the
@ -685,3 +685,4 @@ will generate a workbook that can be opened in a spreadsheet editor.
[^3]: See [`useEffect`](https://kaioken.dev/docs/hooks/useEffect) in the Kaioken documentation.
[^4]: See [`useCallback`](https://kaioken.dev/docs/hooks/useCallback) in the Kaioken documentation.
[^5]: See [`useCallback`](https://kaioken.dev/docs/hooks/useCallback) in the Kaioken documentation.
[^6]: See ["Merged Cells" in "SheetJS Data Model"](/docs/csf/features/merges) for more details.

@ -554,11 +554,11 @@ will generate a workbook that can be opened in a spreadsheet editor.
The main disadvantage of the Array of Objects approach is the specific nature
of the columns. For more general use, passing around an Array of Arrays works.
However, this does not handle merge cells well!
However, this does not handle merge cells[^5] well!
The [`sheet_to_html`](/docs/api/utilities/html#html-table-output) function
generates HTML that is aware of merges and other worksheet features. ReactJS
`dangerouslySetInnerHTML`[^5] prop allows code to set the `innerHTML` attribute,
`dangerouslySetInnerHTML`[^6] prop allows code to set the `innerHTML` attribute,
effectively inserting the code into the page.
In this example, the component attaches a `ref` to the `DIV` container. During
@ -761,4 +761,5 @@ transpiled in the browser using Babel standalone library.
[^2]: See [`useEffect`](https://react.dev/reference/react/useEffect) in the ReactJS documentation.
[^3]: See [`useCallback`](https://react.dev/reference/react/useCallback) in the ReactJS documentation.
[^4]: See [`useCallback`](https://react.dev/reference/react/useCallback) in the ReactJS documentation.
[^5]: [`dangerouslySetInnerHTML`](https://react.dev/reference/react-dom/components/common#common-props) is a ReactJS prop supported for all built-in components.
[^5]: See ["Merged Cells" in "SheetJS Data Model"](/docs/csf/features/merges) for more details.
[^6]: [`dangerouslySetInnerHTML`](https://react.dev/reference/react-dom/components/common#common-props) is a ReactJS prop supported for all built-in components.

@ -507,12 +507,12 @@ to test the bundled site.
The main disadvantage of the Array of Objects approach is the specific nature
of the columns. For more general use, passing around an Array of Arrays works.
However, this does not handle merge cells well!
However, this does not handle merge cells[^5] well!
The `sheet_to_html` function generates HTML that is aware of merges and other
worksheet features. The generated HTML does not contain any `<script>` tags,
and should therefore be safe to pass to an `innerHTML`-bound variable, but the
`DomSanitizer` approach[^5] is strongly recommended:
`DomSanitizer` approach[^6] is strongly recommended:
<Tabs groupId="ngVer">
<TabItem value="2" label="Angular 2-16">
@ -1042,4 +1042,5 @@ npm run build
[^2]: See `OnInit` in the [Angular 2-16 docs](https://angular.io/api/core/OnInit) or [Angular 17 docs](https://angular.dev/guide/components/lifecycle#ngoninit)
[^3]: See [`ngFor`](https://angular.io/api/common/NgFor) in the Angular 2-16 docs.
[^4]: See [`@for`](https://angular.dev/api/core/@for) in the Angular 17 docs.
[^5]: See `DomSanitizer` in the [Angular 2-16 docs](https://angular.io/api/platform-browser/DomSanitizer) or [Angular 17 docs](https://angular.dev/api/platform-browser/DomSanitizer)
[^5]: See ["Merged Cells" in "SheetJS Data Model"](/docs/csf/features/merges) for more details.
[^6]: See `DomSanitizer` in the [Angular 2-16 docs](https://angular.io/api/platform-browser/DomSanitizer) or [Angular 17 docs](https://angular.dev/api/platform-browser/DomSanitizer)

@ -464,11 +464,11 @@ and test the page.
The main disadvantage of the Array of Objects approach is the specific nature
of the columns. For more general use, passing around an Array of Arrays works.
However, this does not handle merge cells well!
However, this does not handle merge cells[^5] well!
The [`sheet_to_html`](/docs/api/utilities/html#html-table-output) function
generates HTML that is aware of merges and other worksheet features. VueJS
`v-html`[^5] attribute allows code to set the `innerHTML` attribute, effectively
`v-html`[^6] attribute allows code to set the `innerHTML` attribute, effectively
inserting the code into the page.
In this example, the component attaches a `ref` to the `DIV` container. During
@ -672,4 +672,5 @@ modern design patterns.
[^2]: See [`onMounted()`](https://vuejs.org/api/composition-api-lifecycle.html#onmounted) in the VueJS documentation.
[^3]: See [`v-for`](https://vuejs.org/api/built-in-directives.html#v-for) in the VueJS documentation.
[^4]: See [`v-on`](https://vuejs.org/api/built-in-directives.html#v-on) in the VueJS documentation.
[^5]: See [`v-html`](https://vuejs.org/api/built-in-directives.html#v-html) in the VueJS documentation.
[^5]: See ["Merged Cells" in "SheetJS Data Model"](/docs/csf/features/merges) for more details.
[^6]: See [`v-html`](https://vuejs.org/api/built-in-directives.html#v-html) in the VueJS documentation.

@ -193,7 +193,7 @@ and test the page.
The main disadvantage of the Array of Objects approach is the specific nature
of the columns. For more general use, passing around an Array of Arrays works.
However, this does not handle merge cells well!
However, this does not handle merge cells[^1] well!
The `sheet_to_html` function generates HTML that is aware of merges and other
worksheet features. Svelte `@html` tag allows raw HTML strings:
@ -288,3 +288,5 @@ Access the displayed URL (typically `http://localhost:8080`) with a web browser
and test the page.
</details>
[^1]: See ["Merged Cells" in "SheetJS Data Model"](/docs/csf/features/merges) for more details.

@ -301,12 +301,12 @@ a worksheet is created and exported to XLSX.
The main disadvantage of the Array of Objects approach is the specific nature
of the columns. For more general use, passing around an Array of Arrays works.
However, this does not handle merge cells well!
However, this does not handle merge cells[^4] well!
The `sheet_to_html` function generates HTML that is aware of merges and other
worksheet features. The generated HTML does not contain any `<script>` tags,
and should therefore be safe to pass to an `ng-bind-html` binding. This approach
requires the `ngSanitize` plugin[^4].
requires the `ngSanitize` plugin[^5].
```html
<div ng-controller="sheetjs">
@ -399,4 +399,5 @@ worksheet is created and exported to XLSX.
[^1]: See [`$http`](https://docs.angularjs.org/api/ng/service/$http) in the AngularJS documentation.
[^2]: See ["Workbook Object"](/docs/csf/book)
[^3]: See ["Creating Directives"](https://docs.angularjs.org/guide/directive#creating-a-directive-that-manipulates-the-dom) in the AngularJS documentation.
[^4]: See [`ngSanitize`](https://docs.angularjs.org/api/ngSanitize) in the AngularJS documentation.
[^4]: See ["Merged Cells" in "SheetJS Data Model"](/docs/csf/features/merges) for more details.
[^5]: See [`ngSanitize`](https://docs.angularjs.org/api/ngSanitize) in the AngularJS documentation.

@ -173,25 +173,4 @@ XLSX.utils.sheet_add_aoa(worksheet, [
_Merge a group of cells_
```js
if(!worksheet["!merges"]) worksheet["!merges"] = [];
worksheet["!merges"].push(XLSX.utils.decode_range("A1:E1"));
```
The `!merges` property of a worksheet object is a list of [Cell Ranges](/docs/csf/general#cell-ranges).
The data for the cell will be taken from the top-left cell.
A range can be created with `decode_range` or specified manually:
```js
worksheet["!merges"].push({
s: { r: 2, c: 1 }, // s ("start"): c = 1 r = 2 -> "B3"
e: { r: 3, c: 4 } // e ("end"): c = 4 r = 3 -> "E4"
});
```
:::caution pass
This approach does not verify if two merged ranges intersect.
:::
**[The exposition has been moved to a separate page.](/docs/csf/features/merges)**

@ -688,8 +688,9 @@ var html = XLSX.utils.sheet_to_html(worksheet);
```
The `sheet_to_html` utility function generates HTML code based on the worksheet
data. Each cell in the worksheet is mapped to a `<TD>` element. Merged cells
in the worksheet are serialized by setting `colspan` and `rowspan` attributes.
data. Each cell in the worksheet is mapped to a `<TD>` element.
[Merged cells](/docs/csf/features/merges) are serialized using the TR and TH
`colspan` and `rowspan` attributes.
#### Examples

@ -199,10 +199,8 @@ In addition to the aforementioned sheet keys, worksheets also add:
- `ws['!rows']`: [array of row objects](/docs/csf/features/rowprops).
Each row object encodes properties including level, height and visibility.
- `ws['!merges']`: array of range objects corresponding to the merged cells in
the worksheet. Plain text formats do not support merge cells. CSV export
will write all cells in the merge range if they exist, so be sure that only
the first cell (upper-left) in the range is set.
- `ws['!merges']`: [array of merge ranges](/docs/csf/features/merges). Each
merge object is a range object that represents the covered range.
- `ws['!outline']`: configure how outlines should behave. Options default to
the default settings in Excel 2019:

@ -0,0 +1,268 @@
---
title: Merged Cells
sidebar_position: 11
---
<details>
<summary><b>File Format Support</b> (click to show)</summary>
By default, no cells are merged. Merge metadata is ignored when exporting to a
file format that does not support merge cells.
Formats store the actual contents of a merged cell in the "top-left" corner
(first row and first column of the merge range). Some formats can hold data for
cells that are covered by the merge range.
| Formats | Merge | Covered |
|:----------|:-----:|:-------:|
| XLSX/XLSM | ✔ | ✔ |
| XLSB | ✔ | ✔ |
| XLML | ✔ | ✔ |
| BIFF8 XLS | ✔ | ✔ |
| ODS/FODS | ✔ | ✔ |
| NUMBERS | ✔ | ✔ |
| HTML | ✔ | |
There are multiple representations of merge cells in the NUMBERS file format.
Writers use the simplified `.TST.MergeRegionMapArchive` representation. Parsers
understand the classic form and the modern `.TST.MergeOwnerArchive` form.
</details>
Modern spreadsheet software typically allow users to combine blocks of cells
into a single unit. This unit can span multiple columns and rows. As shown in
the following table, HTML TH and TD elements use `colspan` and `rowspan`
attributes to effectuate merging:
<table><tbody>
<tr><td colSpan="4"><center>This title spans four columns</center></td></tr>
<tr><td>SheetJS</td><td>supports</td><td>merge</td><td>cells</td></tr>
</tbody></table>
:::tip pass
This feature was expanded in version `0.20.3`. It is strongly recommended to
[upgrade to the latest version](/docs/getting-started/installation/).
:::
## Storage
The `!merges` property of the worksheet object is expected to be an array of
[SheetJS range objects](/docs/csf/general#sheetjs-range). Each range object
corresponds to a merged range in the worksheet.
The following snippet creates a merge range spanning `A1:B2` :
```js title="Merge the range A1:B2 in a worksheet"
ws["!merges"] = [
{ s: { c: 0, r: 0 }, e: { c: 1, r: 1 } } // A1:B2
];
```
:::caution pass
**Overlapping merges are not automatically detected!**
:::
### Range
The [`decode_range`](/docs/csf/general#cell-ranges-1) method creates range
objects from A1-style range strings.
The following snippet creates a merge range spanning `A1:B2` :
```js title="Merge the range A1:B2 in a worksheet"
ws["!merges"] = [
XLSX.utils.decode_range("A1:B2")
];
```
### Overlap
When adding merges to an existing workbook, it is strongly recommended to scan
the merges array and test for collisions:
```js title="Add a merged range to a worksheet"
function sheet_add_merge(ws, range) {
/* if `range` is a string, parse into a range object */
var merge = typeof range == "string" ? XLSX.utils.decode_range(range) : range;
/* create array merge if it does not exist */
if(!ws["!merges"]) ws["!merges"] = [];
/* check if the new merge collides with any existing merge */
ws["!merges"].forEach(function(range) {
if(merge.e.r < range.s.r) return;
if(range.e.r < merge.s.r) return;
if(merge.e.c < range.s.c) return;
if(range.e.c < merge.s.c) return;
throw new Error(XLSX.utils.encode_range(merge)+" overlaps "+XLSX.utils.encode_range(range));
});
/* add merge */
ws["!merges"].push(merge);
}
```
### Cells
Spreadsheet tools will store and use the top-left cell of a merge range. For
example, if the range `B2:C5` is merged, the cell corresponding to the range
will be stored in the worksheet in cell `B2`.
#### Covered Cells
Spreadsheet tools can store cells that are covered by a merged cell.
The [SheetJS worksheet object](/docs/csf/sheet) can store covered cells.
[API Functions](#functions) may omit or include covered cells.
## Live Demo
This example generates a worksheet that matches the following screenshot:
![screenshot](pathname:///files/merges.png)
The merge ranges are `A1:B2`, `C1:C2`, `A3:B3`, `D1:D2`, and `A4:B4`.
```jsx live
function SheetJSMergeCellsExport() { return (<button onClick={() => {
/* write data to the top-left corner of each range */
var ws = XLSX.utils.aoa_to_sheet([
["A1:B2", /* B1 */, "C1:C2", "Separate blocks"], // row 1
[], // row 2
["A3:B3", /* B3 */, "C3"], // row 3
["... are merged separately"], // row 4
]);
/* add merges */
ws["!merges"] = [
{ s: { c: 0, r: 0 }, e: { c: 1, r: 1 } }, // A1:B2
{ s: { c: 2, r: 0 }, e: { c: 2, r: 1 } }, // C1:C2
{ s: { c: 0, r: 2 }, e: { c: 1, r: 2 } }, // A3:B3
{ s: { c: 3, r: 0 }, e: { c: 3, r: 1 } }, // D1:D2
{ s: { c: 0, r: 3 }, e: { c: 1, r: 3 } } // A4:B4
];
/* export to XLSX */
var wb = XLSX.utils.book_new(ws, "Merges");
XLSX.writeFile(wb, "SheetJSMergeCells.xlsx");
}}><b>Click here to Export</b></button>); }
```
## Functions
#### HTML
[`table_to_sheet` and `table_to_book`](/docs/api/utilities/html#html-table-input)
will generate worksheets that include merged ranges:
<details>
<summary><b>Live Demo</b> (click to show)</summary>
```jsx live
function SheetJSDOMMergedCells() {
const ref = React.useRef(null);
const [ merges, setMerges ] = React.useState([]);
React.useEffect(() => {
if(ref.current) {
const tbl = ref.current.getElementsByTagName("TABLE");
if(!tbl || !tbl[0]) return;
const ws = XLSX.utils.table_to_sheet(tbl[0]);
console.log(ws["!merges"])
setMerges(ws["!merges"] || []);
}
}, [ref])
const ws = XLSX.utils.aoa_to_sheet([
["A1:B1 is merged", "This cell is covered" ],
["A2 is not merged", "B2 is not merged"]
]);
ws["!merges"] = [XLSX.utils.decode_range("A1:B1")];
const __html = XLSX.utils.sheet_to_html(ws);
return ( <>
<b>Table:</b>
<div ref={ref} dangerouslySetInnerHTML={{__html}}/>
<b>Merges:</b>
<pre>{merges ? merges.map(m => XLSX.utils.encode_range(m)).join("\n") : ""}</pre>
</>
);
}
```
</details>
[`sheet_to_html`](/docs/api/utilities/html#html-table-output) will generate HTML
strings that use `colspan` and `rowspan` for merged ranges:
<details>
<summary><b>Live Demo</b> (click to show)</summary>
```jsx live
function SheetJSHTMLMergedCells() {
const ws = XLSX.utils.aoa_to_sheet([
["A1:B1 is merged", "This cell is covered" ],
["A2 is not merged", "B2 is not merged"]
]);
ws["!merges"] = [XLSX.utils.decode_range("A1:B1")];
const __html = XLSX.utils.sheet_to_html(ws);
return ( <div dangerouslySetInnerHTML={{__html}}/> );
}
```
</details>
#### Reading Files
[`read` and `readFile`](/docs/api/parse-options) will extract merge metadata
from supported files.
#### Writing Files
[`write` and `writeFile`](/docs/api/write-options) will attempt to write merge
metadata when exporting to file formats that support merged ranges.
When writing to CSV and other formats that do not support merged ranges, every
cell in the range will be exported. This includes covered cells!
<details>
<summary><b>Live Demo</b> (click to show)</summary>
```jsx live
function SheetJSCSVMergedCells() {
const ws = XLSX.utils.aoa_to_sheet([
["A1:B1 is merged", "This cell is covered" ],
["A2 is not merged", "B2 is not merged"]
]);
ws["!merges"] = [XLSX.utils.decode_range("A1:B1")];
const wb = XLSX.utils.book_new(ws, "Sheet1");
const csv = XLSX.write(wb, { type: "string", bookType: "csv"});
return ( <pre>{csv}</pre> );
}
```
</details>
#### Exporting Data
[`sheet_to_csv`](/docs/api/utilities/csv#delimiter-separated-output) and
[`sheet_to_json`](/docs/api/utilities/array#array-output) do not support merged
ranges. The exports will include covered cells:
<details>
<summary><b>Live Demo</b> (click to show)</summary>
```jsx live
function SheetJSAOAMergedCells() {
const ws = XLSX.utils.aoa_to_sheet([
["A1:B1 is merged", "This cell is covered" ],
["A2 is not merged", "B2 is not merged"]
]);
ws["!merges"] = [XLSX.utils.decode_range("A1:B1")];
const aoa = XLSX.utils.sheet_to_json(ws, {header:1});
return ( <pre>{JSON.stringify(aoa,2,2)}</pre> );
}
```
</details>

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB