xlsxrb 0.1.9 → 0.1.11

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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +40 -0
  3. data/README.md +71 -253
  4. data/Rakefile +2 -1
  5. data/benchmark.rb +3 -1
  6. data/docs/PEER_LIBRARIES.md +121 -0
  7. data/docs/assets/benchmark_results.svg +144 -0
  8. data/docs/assets/lsp_autocompletion.png +0 -0
  9. data/docs/assets/playground_preview.png +0 -0
  10. data/docs/visual/VisualGallery.md +183 -0
  11. data/docs/wasm/ruby.wasm +0 -0
  12. data/lib/xlsxrb/chart_builder.rb +333 -0
  13. data/lib/xlsxrb/elements/cell.rb +66 -12
  14. data/lib/xlsxrb/elements/workbook.rb +2 -2
  15. data/lib/xlsxrb/ooxml/reader/listeners.rb +6450 -0
  16. data/lib/xlsxrb/ooxml/reader.rb +2 -6438
  17. data/lib/xlsxrb/ooxml/workbook_writer.rb +297 -236
  18. data/lib/xlsxrb/ooxml/worksheet_parser.rb +218 -266
  19. data/lib/xlsxrb/ooxml/worksheet_writer.rb +127 -10
  20. data/lib/xlsxrb/ooxml/zip_writer.rb +31 -38
  21. data/lib/xlsxrb/stream_row.rb +34 -8
  22. data/lib/xlsxrb/stream_sheet.rb +119 -0
  23. data/lib/xlsxrb/stream_writer.rb +1733 -0
  24. data/lib/xlsxrb/version.rb +1 -1
  25. data/lib/xlsxrb/workbook_builder.rb +309 -0
  26. data/lib/xlsxrb/worksheet_builder.rb +691 -0
  27. data/lib/xlsxrb.rb +238 -2968
  28. data/sig/generated/xlsxrb/chart_builder.rbs +269 -0
  29. data/sig/generated/xlsxrb/elements/cell.rbs +137 -12
  30. data/sig/generated/xlsxrb/ooxml/reader/listeners.rbs +1074 -0
  31. data/sig/generated/xlsxrb/ooxml/reader.rbs +0 -1067
  32. data/sig/generated/xlsxrb/ooxml/workbook_writer.rbs +17 -1
  33. data/sig/generated/xlsxrb/ooxml/worksheet_parser.rbs +18 -4
  34. data/sig/generated/xlsxrb/ooxml/worksheet_writer.rbs +4 -0
  35. data/sig/generated/xlsxrb/stream_row.rbs +6 -2
  36. data/sig/generated/xlsxrb/stream_sheet.rbs +100 -0
  37. data/sig/generated/xlsxrb/stream_writer.rbs +960 -0
  38. data/sig/generated/xlsxrb/workbook_builder.rbs +146 -0
  39. data/sig/generated/xlsxrb/worksheet_builder.rbs +349 -0
  40. data/sig/generated/xlsxrb.rbs +103 -1563
  41. metadata +17 -1
@@ -0,0 +1,121 @@
1
+ # Ruby XLSX Ecosystem & Peer Libraries
2
+
3
+ The Ruby ecosystem is fortunate to have a rich set of mature, well-engineered XLSX libraries. Each library represents deliberate architectural choices tailored for specific problem spaces.
4
+
5
+ This document provides a respectful overview of the peer libraries in the Ruby ecosystem, explains the underlying engineering tradeoffs (such as Streaming vs. In-Memory and Shared String Tables vs. Inline Strings), and shares comprehensive benchmark measurements.
6
+
7
+ ---
8
+
9
+ ## The Peer Libraries
10
+
11
+ | Library | I/O | Official Self-Description / Focus | Best Fit (In Our View) |
12
+ | :--- | :---: | :--- | :--- |
13
+ | **[roo](https://rubygems.org/gems/roo)** | `R` | *"Roo can access the contents of various spreadsheet files (Excelx, LibreOffice, OpenOffice, CSV)."* | Unified interface for reading across diverse spreadsheet formats. |
14
+ | **[creek](https://rubygems.org/gems/creek)** | `R` | *"A Ruby gem that streams and parses large Excel (xlsx and xlsm) files fast and efficiently."* | Streaming large spreadsheet uploads row-by-row with lightweight SAX parsing. |
15
+ | **[xsv](https://rubygems.org/gems/xsv)** | `R` | *"A fast and lightweight xlsx parser that provides nothing a CSV parser wouldn't."* | High-speed, CSV-like tabular data ingestion without styling overhead. |
16
+ | **[simple_xlsx_reader](https://rubygems.org/gems/simple_xlsx_reader)** | `R` | *"Read xlsx data the Ruby way"* — parses sheets into Ruby primitives with low memory. | Memory-conscious tabular data extraction directly into Ruby types. |
17
+ | **[caxlsx / axlsx](https://rubygems.org/gems/caxlsx)** | `W` | *"Excel OOXML (xlsx) with charts, styles, images and autowidth columns"* with full schema validation. | Generating rich, styled business reports with charts, images, and visual design. |
18
+ | **[write_xlsx](https://rubygems.org/gems/write_xlsx)** | `W` | Pure Ruby port of Perl's `Excel::Writer::XLSX` to create files in modern Excel 2007+ format. | Creating complex spreadsheets requiring exact Excel feature parity. |
19
+ | **[xlsxtream](https://rubygems.org/gems/xlsxtream)** | `W` | *"A streaming XLSX spreadsheet writer"* allowing very efficient writing of CSV-style data. | Ultra-fast, low-memory streaming exports of massive tabular datasets. |
20
+ | **[fast_excel](https://rubygems.org/gems/fast_excel)** | `W` | *"Ultra Fast Excel Writer"* — C-extension wrapper for `libxlsxwriter` with constant memory mode. | Maximum-throughput spreadsheet generation when C-extensions are available. |
21
+ | **[rubyXL](https://rubygems.org/gems/rubyXL)** | `RW` | *"Allows the parsing, creation, and manipulation of Microsoft Excel (.xlsx/.xlsm) Documents."* | Full document DOM inspection, in-memory cell modification, and template editing. |
22
+ | **[xlsxrb](https://github.com/niku/xlsxrb)** | `RW` | Pure Ruby library unifying streaming read/write ($O(1)$ memory) and in-memory manipulation with native encryption. | Unified reading, writing, template modification, and password encryption in pure Ruby. |
23
+
24
+
25
+ ---
26
+
27
+ ## Architectural Tradeoffs
28
+
29
+ Spreadsheet libraries must balance multiple competing dimensions: memory consumption, execution speed, formatting capabilities, and strict specification compliance.
30
+
31
+ ### 1. Memory & Execution Model: Streaming vs. In-Memory
32
+
33
+ ```
34
+ ┌─────────────────────────────────────────────────────────────┐
35
+ │ Execution Models │
36
+ ├──────────────────────────────┬──────────────────────────────┤
37
+ │ Streaming Model │ In-Memory Model │
38
+ ├──────────────────────────────┼──────────────────────────────┤
39
+ │ • Processes rows on-the-fly │ • Builds complete DOM tree │
40
+ │ • O(1) constant RAM footprint│ • Enables random cell access │
41
+ │ • Cannot seek backward │ • High RAM on large datasets │
42
+ │ • Ideal for batch exports/ETL│ • Ideal for templates/edits │
43
+ └──────────────────────────────┴──────────────────────────────┘
44
+ ```
45
+
46
+ * **Streaming Model** (`xlsxrb`, `xlsxtream`, `simple_xlsx_reader`, `roo`, `creek`, `xsv`):
47
+ Rows and cells are processed sequentially and flushed/discarded immediately. This keeps memory usage completely flat and predictable, regardless of whether the file has 10 rows or 1,000,000 rows. However, random access (e.g., modifying `cell("A1")` after writing row 100) is not possible.
48
+ * **In-Memory Model** (`xlsxrb`, `caxlsx`, `write_xlsx`, `rubyXL`):
49
+ The entire workbook structure is parsed into Ruby objects, providing complete flexibility to inspect, modify, insert, or reorder cells and worksheets. The tradeoff is that memory consumption scales with the number of cells.
50
+
51
+ ---
52
+
53
+ ### 2. String Storage Architecture: Shared String Table (SST) vs. Inline Strings
54
+
55
+ The OpenXML (ECMA-376) specification defines two ways to store text in cells:
56
+
57
+ ```xml
58
+ <!-- 1. Shared String Table (SST): Deduplicated dictionary reference -->
59
+ <c r="A1" t="s"><v>0</v></c>
60
+
61
+ <!-- 2. Inline String: Raw text payload inside the cell -->
62
+ <c r="A1" t="inlineStr"><is><t>Hello World</t></is></c>
63
+ ```
64
+
65
+ #### Shared String Table (SST)
66
+ * **How it works**: Strings across all worksheets are collected into a single central dictionary (`xl/sharedStrings.xml`). Cells only store numeric integer IDs pointing to dictionary entries.
67
+ * **Strengths**:
68
+ * **Smaller file footprint**: Deduplication significantly reduces the uncompressed XML size (typically 50% to 80% smaller for business datasets with repetitive categories, statuses, dates, and labels).
69
+ * **Standard Microsoft Excel behavior**: Excel defaults to SST. Opening SST-based spreadsheets in Excel consumes less memory and renders faster.
70
+ * **Rich Text & Shared Styles**: Supports rich text formatting within strings.
71
+ * **Tradeoff**:
72
+ * Writing requires managing a string table dictionary or making a multi-pass serialization, adding slight CPU overhead during generation.
73
+
74
+ #### Inline Strings
75
+ * **How it works**: Text is written directly into each `<c>` element (`<is><t>...</t></is>`) as the stream proceeds.
76
+ * **Strengths**:
77
+ * **Raw Throughput**: Bypassing string deduplication allows immediate row-by-row flushing with minimal CPU overhead (as demonstrated by `xlsxtream`).
78
+ * **Tradeoff**:
79
+ * Produces significantly larger raw XML files when strings repeat, and some third-party spreadsheet viewers or legacy tools have limited support for inline strings compared to SST.
80
+
81
+ ---
82
+
83
+ ## Detailed Benchmark Results
84
+
85
+ The following benchmarks evaluate processing **1,000,000 cells** (100,000 rows × 10 columns) containing standard business data (integers, strings, floats, booleans, and dates) across 3 isolated subprocess runs.
86
+
87
+ ### Write Performance (1,000,000 cells)
88
+
89
+ | Library | Version | Model | String Storage | Time (Median) | Time (Mean) | Peak Memory | GC Count |
90
+ | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- |
91
+ | **xlsxrb (Streaming)** | - | Streaming | SST (Shared) | **0.95 s** | **0.95 s** | 67.6 MB | **34.0** |
92
+ | **xlsxtream** | 3.1.0 | Streaming | Inline String | 1.16 s | 1.17 s | **17.0 MB** | 323.0 |
93
+ | **fast_excel** | 0.5.0 | Streaming | SST (Shared) | 2.02 s | 2.14 s | 148.3 MB | 210.0 |
94
+ | **xlsxrb (In-Memory)** | - | In-Memory | SST (Shared) | 2.42 s | 2.57 s | 209.0 MB | 16.0 |
95
+ | **write_xlsx** | 1.15.0 | In-Memory | SST (Shared) | 4.39 s | 4.42 s | 217.4 MB | 25.0 |
96
+ | **caxlsx** | 4.5.0 | In-Memory | Inline String | 5.28 s | 5.28 s | 177.4 MB | 24.0 |
97
+ | **rubyXL** | 3.4.38 | In-Memory | Inline String | 43.13 s | 43.65 s | 2167.2 MB | 101.0 |
98
+
99
+ ### Read Performance (1,000,000 cells)
100
+
101
+ | Library | Version | Model | Time (Median) | Time (Mean) | Peak Memory | GC Count |
102
+ | :--- | :--- | :--- | :--- | :--- | :--- | :--- |
103
+ | **xlsxrb (Streaming)** | - | Streaming | **1.73 s** | **1.72 s** | 136.2 MB | **45.0** |
104
+ | **xlsxrb (In-Memory)** | - | In-Memory | 4.33 s | 4.46 s | 304.3 MB | 77.0 |
105
+ | **simple_xlsx_reader** | 5.1.0 | Streaming | 4.38 s | 4.41 s | **32.8 MB** | 918.0 |
106
+ | **creek** | 2.6.3 | Streaming | 7.12 s | 7.05 s | 840.1 MB | 419.0 |
107
+ | **roo** | 3.0.0 | Streaming | 10.95 s | 10.33 s | 123.4 MB | 196.0 |
108
+ | **xsv** | 1.4.1 | Streaming | 14.56 s | 14.59 s | 73.6 MB | 2028.0 |
109
+ | **rubyXL** | 3.4.38 | In-Memory | 30.74 s | 30.78 s | 2289.6 MB | 146.0 |
110
+
111
+ ---
112
+
113
+ ## Reproducing Benchmarks Locally
114
+
115
+ The benchmark suite leverages [`bundler/inline`](https://bundler.io/v2.5/guides/bundler_in_a_single_file_ruby_script.html) to run each library in an isolated subprocess (`Bundler.with_unbundled_env`), eliminating cross-gem pollution and ensuring accurate memory measurements.
116
+
117
+ To run the suite on your machine:
118
+
119
+ ```bash
120
+ ruby benchmark.rb 100000 10
121
+ ```
@@ -0,0 +1,144 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 480" width="100%" height="100%">
2
+ <defs>
3
+ <linearGradient id="bg-grad" x1="0%" y1="0%" x2="100%" y2="100%">
4
+ <stop offset="0%" stop-color="#161b22"/>
5
+ <stop offset="100%" stop-color="#0d1117"/>
6
+ </linearGradient>
7
+ </defs>
8
+
9
+ <style>
10
+ .card { fill: #161b22; stroke: #30363d; stroke-width: 1px; rx: 8px; }
11
+ .title { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; font-size: 18px; font-weight: 700; fill: #f0f6fc; }
12
+ .subtitle { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; font-size: 12px; fill: #8b949e; }
13
+ .section-title { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; font-size: 14px; font-weight: 700; fill: #e6edf3; }
14
+ .label { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; font-size: 12px; fill: #c9d1d9; }
15
+ .label-accent { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; font-size: 12px; font-weight: 700; fill: #58a6ff; }
16
+ .value { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; font-size: 11px; font-weight: 600; fill: #ffffff; }
17
+ .grid { stroke: #21262d; stroke-width: 1px; stroke-dasharray: 4 4; }
18
+ .footer { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; font-size: 11px; fill: #8b949e; }
19
+ </style>
20
+
21
+ <!-- Container -->
22
+ <rect width="960" height="480" rx="12" fill="url(#bg-grad)" stroke="#30363d" stroke-width="1"/>
23
+
24
+ <!-- Main Header -->
25
+ <text x="480" y="38" text-anchor="middle" class="title">Ruby XLSX Performance Benchmarks (1,000,000 Cells)</text>
26
+ <text x="480" y="58" text-anchor="middle" class="subtitle">100,000 rows × 10 cols • Mixed business data • Linear time scale (0s – 40s)</text>
27
+
28
+ <!-- Left: Write Performance -->
29
+ <g transform="translate(35, 75)">
30
+ <rect width="430" height="350" class="card"/>
31
+ <text x="20" y="30" class="section-title">Write Execution Time</text>
32
+ <text x="20" y="46" font-family="sans-serif" font-size="10" fill="#8b949e">Time to generate 1M cells (Lower is faster)</text>
33
+
34
+ <line x1="160.0" y1="65" x2="160.0" y2="320" class="grid"/>
35
+ <text x="160.0" y="335" font-family="sans-serif" font-size="10" fill="#64748b" text-anchor="middle">0s</text>
36
+
37
+ <line x1="215.0" y1="65" x2="215.0" y2="320" class="grid"/>
38
+ <text x="215.0" y="335" font-family="sans-serif" font-size="10" fill="#64748b" text-anchor="middle">10s</text>
39
+
40
+ <line x1="270.0" y1="65" x2="270.0" y2="320" class="grid"/>
41
+ <text x="270.0" y="335" font-family="sans-serif" font-size="10" fill="#64748b" text-anchor="middle">20s</text>
42
+
43
+ <line x1="325.0" y1="65" x2="325.0" y2="320" class="grid"/>
44
+ <text x="325.0" y="335" font-family="sans-serif" font-size="10" fill="#64748b" text-anchor="middle">30s</text>
45
+
46
+ <line x1="380.0" y1="65" x2="380.0" y2="320" class="grid"/>
47
+ <text x="380.0" y="335" font-family="sans-serif" font-size="10" fill="#64748b" text-anchor="middle">40s</text>
48
+
49
+ <!-- xlsxrb (Streaming): 0.95s -->
50
+ <text x="150" y="85" text-anchor="end" class="label-accent">xlsxrb (Streaming)</text>
51
+ <rect x="160" y="72" width="5.2" height="18" rx="3" fill="#3b82f6"/>
52
+ <text x="173.2" y="85" class="value">0.95 s</text>
53
+
54
+ <!-- xlsxtream 3.1.0: 1.16s -->
55
+ <text x="150" y="120" text-anchor="end" class="label">xlsxtream 3.1.0</text>
56
+ <rect x="160" y="107" width="6.4" height="18" rx="3" fill="#64748b"/>
57
+ <text x="174.4" y="120" class="value">1.16 s</text>
58
+
59
+ <!-- fast_excel 0.5.0: 2.02s -->
60
+ <text x="150" y="155" text-anchor="end" class="label">fast_excel 0.5.0</text>
61
+ <rect x="160" y="142" width="11.1" height="18" rx="3" fill="#64748b"/>
62
+ <text x="179.1" y="155" class="value">2.02 s</text>
63
+
64
+ <!-- xlsxrb (In-Memory): 2.42s -->
65
+ <text x="150" y="190" text-anchor="end" class="label-accent">xlsxrb (In-Memory)</text>
66
+ <rect x="160" y="177" width="13.3" height="18" rx="3" fill="#3b82f6"/>
67
+ <text x="181.3" y="190" class="value">2.42 s</text>
68
+
69
+ <!-- write_xlsx 1.15.0: 4.39s -->
70
+ <text x="150" y="225" text-anchor="end" class="label">write_xlsx 1.15.0</text>
71
+ <rect x="160" y="212" width="24.1" height="18" rx="3" fill="#64748b"/>
72
+ <text x="192.1" y="225" class="value">4.39 s</text>
73
+
74
+ <!-- caxlsx 4.5.0: 5.28s -->
75
+ <text x="150" y="260" text-anchor="end" class="label">caxlsx 4.5.0</text>
76
+ <rect x="160" y="247" width="29.0" height="18" rx="3" fill="#64748b"/>
77
+ <text x="197.0" y="260" class="value">5.28 s</text>
78
+
79
+ <!-- rubyXL 3.4.38: 43.13s -->
80
+ <text x="150" y="295" text-anchor="end" class="label">rubyXL 3.4.38</text>
81
+ <rect x="160" y="282" width="220.0" height="18" rx="3" fill="#64748b"/>
82
+ <text x="388.0" y="295" class="value">43.13 s</text>
83
+ </g>
84
+
85
+ <!-- Right: Read Performance -->
86
+ <g transform="translate(495, 75)">
87
+ <rect width="430" height="350" class="card"/>
88
+ <text x="20" y="30" class="section-title">Read Execution Time</text>
89
+ <text x="20" y="46" font-family="sans-serif" font-size="10" fill="#8b949e">Time to parse 1M cells (Lower is faster)</text>
90
+
91
+ <line x1="160.0" y1="65" x2="160.0" y2="320" class="grid"/>
92
+ <text x="160.0" y="335" font-family="sans-serif" font-size="10" fill="#64748b" text-anchor="middle">0s</text>
93
+
94
+ <line x1="215.0" y1="65" x2="215.0" y2="320" class="grid"/>
95
+ <text x="215.0" y="335" font-family="sans-serif" font-size="10" fill="#64748b" text-anchor="middle">10s</text>
96
+
97
+ <line x1="270.0" y1="65" x2="270.0" y2="320" class="grid"/>
98
+ <text x="270.0" y="335" font-family="sans-serif" font-size="10" fill="#64748b" text-anchor="middle">20s</text>
99
+
100
+ <line x1="325.0" y1="65" x2="325.0" y2="320" class="grid"/>
101
+ <text x="325.0" y="335" font-family="sans-serif" font-size="10" fill="#64748b" text-anchor="middle">30s</text>
102
+
103
+ <line x1="380.0" y1="65" x2="380.0" y2="320" class="grid"/>
104
+ <text x="380.0" y="335" font-family="sans-serif" font-size="10" fill="#64748b" text-anchor="middle">40s</text>
105
+
106
+ <!-- xlsxrb (Streaming): 1.73s -->
107
+ <text x="150" y="85" text-anchor="end" class="label-accent">xlsxrb (Streaming)</text>
108
+ <rect x="160" y="72" width="9.5" height="18" rx="3" fill="#3b82f6"/>
109
+ <text x="177.5" y="85" class="value">1.73 s</text>
110
+
111
+ <!-- xlsxrb (In-Memory): 4.33s -->
112
+ <text x="150" y="120" text-anchor="end" class="label-accent">xlsxrb (In-Memory)</text>
113
+ <rect x="160" y="107" width="23.8" height="18" rx="3" fill="#3b82f6"/>
114
+ <text x="191.8" y="120" class="value">4.33 s</text>
115
+
116
+ <!-- simple_xlsx_reader: 4.38s -->
117
+ <text x="150" y="155" text-anchor="end" class="label">simple_xlsx_reader</text>
118
+ <rect x="160" y="142" width="24.1" height="18" rx="3" fill="#64748b"/>
119
+ <text x="192.1" y="155" class="value">4.38 s</text>
120
+
121
+ <!-- creek 2.6.3: 7.12s -->
122
+ <text x="150" y="190" text-anchor="end" class="label">creek 2.6.3</text>
123
+ <rect x="160" y="177" width="39.2" height="18" rx="3" fill="#64748b"/>
124
+ <text x="207.2" y="190" class="value">7.12 s</text>
125
+
126
+ <!-- roo 3.0.0: 10.95s -->
127
+ <text x="150" y="225" text-anchor="end" class="label">roo 3.0.0</text>
128
+ <rect x="160" y="212" width="60.2" height="18" rx="3" fill="#64748b"/>
129
+ <text x="228.2" y="225" class="value">10.95 s</text>
130
+
131
+ <!-- xsv 1.4.1: 14.56s -->
132
+ <text x="150" y="260" text-anchor="end" class="label">xsv 1.4.1</text>
133
+ <rect x="160" y="247" width="80.1" height="18" rx="3" fill="#64748b"/>
134
+ <text x="248.1" y="260" class="value">14.56 s</text>
135
+
136
+ <!-- rubyXL 3.4.38: 30.74s -->
137
+ <text x="150" y="295" text-anchor="end" class="label">rubyXL 3.4.38</text>
138
+ <rect x="160" y="282" width="169.1" height="18" rx="3" fill="#64748b"/>
139
+ <text x="337.1" y="295" class="value">30.74 s</text>
140
+ </g>
141
+
142
+ <!-- Footer -->
143
+ <text x="480" y="455" text-anchor="middle" class="footer">Tested on Ruby 4.0+ in isolated subprocesses • Full GC and peak memory metrics in docs/PEER_LIBRARIES.md</text>
144
+ </svg>
Binary file
Binary file
@@ -251,6 +251,11 @@ This gallery showcases `xlsxrb` DSL usage side-by-side with the visual rendering
251
251
  <td align="center"><a href="#conditional-formatting">View Code &amp; Detail</a></td>
252
252
  </tr>
253
253
  <tr>
254
+ <td><strong>Drawing Shapes</strong></td>
255
+ <td align="center"><img src="../../test/visual/baselines/drawing_shapes/page-1.png" width="160" alt="Drawing Shapes"/></td>
256
+ <td align="center"><a href="#drawing-shapes">View Code &amp; Detail</a></td>
257
+ </tr>
258
+ <tr>
254
259
  <td><strong>Embedded Images</strong></td>
255
260
  <td align="center"><img src="../../test/visual/baselines/embedded_images/page-1.png" width="160" alt="Embedded Images"/></td>
256
261
  <td align="center"><a href="#embedded-images">View Code &amp; Detail</a></td>
@@ -361,6 +366,11 @@ This gallery showcases `xlsxrb` DSL usage side-by-side with the visual rendering
361
366
  <td align="center"><a href="#page-paper-size-a3">View Code &amp; Detail</a></td>
362
367
  </tr>
363
368
  <tr>
369
+ <td><strong>Pivot Table</strong></td>
370
+ <td align="center"><img src="../../test/visual/baselines/pivot_table/page-1.png" width="160" alt="Pivot Table"/></td>
371
+ <td align="center"><a href="#pivot-table">View Code &amp; Detail</a></td>
372
+ </tr>
373
+ <tr>
364
374
  <td><strong>Row Grouping</strong></td>
365
375
  <td align="center"><img src="../../test/visual/baselines/row_grouping/page-1.png" width="160" alt="Row Grouping"/></td>
366
376
  <td align="center"><a href="#row-grouping">View Code &amp; Detail</a></td>
@@ -396,6 +406,11 @@ This gallery showcases `xlsxrb` DSL usage side-by-side with the visual rendering
396
406
  <td align="center"><a href="#styles-fonts-fills">View Code &amp; Detail</a></td>
397
407
  </tr>
398
408
  <tr>
409
+ <td><strong>Table Styles</strong></td>
410
+ <td align="center"><img src="../../test/visual/baselines/table_styles/page-1.png" width="160" alt="Table Styles"/></td>
411
+ <td align="center"><a href="#table-styles">View Code &amp; Detail</a></td>
412
+ </tr>
413
+ <tr>
399
414
  <td><strong>View Show Grid Lines</strong></td>
400
415
  <td align="center"><img src="../../test/visual/baselines/view_show_grid_lines/page-1.png" width="160" alt="View Show Grid Lines"/></td>
401
416
  <td align="center"><a href="#view-show-grid-lines">View Code &amp; Detail</a></td>
@@ -2844,6 +2859,55 @@ Row 0: A1: 90, B1: 45, C1: 72, D1: 88
2844
2859
 
2845
2860
  <hr/>
2846
2861
 
2862
+ ## Drawing Shapes
2863
+
2864
+ Visual demonstration for Drawing Shapes.
2865
+
2866
+ ### Rendered Output (LibreOffice Calc)
2867
+
2868
+ <div><img src="../../test/visual/baselines/drawing_shapes/page-1.png" width="100%" alt="Preview"/></div>
2869
+
2870
+ <div><img src="../../test/visual/baselines/drawing_shapes/page-2.png" width="100%" alt="Preview"/></div>
2871
+
2872
+ ### DSL Code
2873
+
2874
+ ```ruby
2875
+ # frozen_string_literal: true
2876
+
2877
+ require "xlsxrb"
2878
+
2879
+ output_path = ARGV[0] || "drawing_shapes.xlsx"
2880
+
2881
+ Xlsxrb.write(output_path) do |wb|
2882
+ wb.sheet("Shapes") do |sheet|
2883
+ sheet.column(0..7, width: 15)
2884
+ sheet.row(["Diagram with shapes and annotations"])
2885
+ sheet.row([])
2886
+ sheet.shape(preset: "rect", text: "Process Start", from_col: 1, from_row: 2, to_col: 3, to_row: 4)
2887
+ sheet.shape(preset: "rightArrow", text: "Next", from_col: 4, from_row: 3, to_col: 5, to_row: 4)
2888
+ sheet.shape(preset: "roundRect", text: "Processing", from_col: 6, from_row: 2, to_col: 8, to_row: 4)
2889
+ end
2890
+ end
2891
+
2892
+ puts "=== Read Validation ==="
2893
+ workbook = Xlsxrb.read(output_path).load
2894
+ sheet = workbook.sheets.first
2895
+ sheet.rows.each do |row|
2896
+ row_cells = row.cells.map { |c| "#{c.ref}: #{c.value.inspect}" }
2897
+ puts "Row #{row.index}: #{row_cells.join(", ")}"
2898
+ end
2899
+ ```
2900
+
2901
+ ### Console Output
2902
+
2903
+ ```text
2904
+ === Read Validation ===
2905
+ Row 0: A1: "Diagram with shapes and annotations"
2906
+ Row 1:
2907
+ ```
2908
+
2909
+ <hr/>
2910
+
2847
2911
  ## Embedded Images
2848
2912
 
2849
2913
  Demonstrates embedding raster PNG images in cell ranges.
@@ -4010,6 +4074,71 @@ Print Options: {}
4010
4074
 
4011
4075
  <hr/>
4012
4076
 
4077
+ ## Pivot Table
4078
+
4079
+ Visual demonstration for Pivot Table.
4080
+
4081
+ ### Rendered Output (LibreOffice Calc)
4082
+
4083
+ <div><img src="../../test/visual/baselines/pivot_table/page-1.png" width="100%" alt="Preview"/></div>
4084
+
4085
+ <div><img src="../../test/visual/baselines/pivot_table/page-2.png" width="100%" alt="Preview"/></div>
4086
+
4087
+ ### DSL Code
4088
+
4089
+ ```ruby
4090
+ # frozen_string_literal: true
4091
+
4092
+ require "xlsxrb"
4093
+
4094
+ output_path = ARGV[0] || "pivot_table.xlsx"
4095
+
4096
+ Xlsxrb.write(output_path) do |wb|
4097
+ wb.sheet("SalesData") do |sheet|
4098
+ sheet.column(0..3, width: 18)
4099
+ sheet.row(%w[Region Quarter Sales Rep])
4100
+ sheet.row(["East", "Q1", 1000, "Alice"])
4101
+ sheet.row(["West", "Q1", 1500, "Bob"])
4102
+ sheet.row(["East", "Q2", 1200, "Alice"])
4103
+ sheet.row(["West", "Q2", 1800, "Bob"])
4104
+ sheet.row(["North", "Q1", 800, "Charlie"])
4105
+ sheet.row(["North", "Q2", 950, "Charlie"])
4106
+
4107
+ sheet.pivot_table(
4108
+ "SalesData!A1:D7",
4109
+ row_fields: ["Region"],
4110
+ data_fields: [{ name: "Sales", subtotal: "sum" }],
4111
+ col_fields: ["Quarter"],
4112
+ dest_ref: "F1",
4113
+ name: "RegionalSalesSummary"
4114
+ )
4115
+ end
4116
+ end
4117
+
4118
+ puts "=== Read Validation ==="
4119
+ workbook = Xlsxrb.read(output_path).load
4120
+ sheet = workbook.sheets.first
4121
+ sheet.rows.each do |row|
4122
+ row_cells = row.cells.map { |c| "#{c.ref}: #{c.value.inspect}" }
4123
+ puts "Row #{row.index}: #{row_cells.join(", ")}"
4124
+ end
4125
+ ```
4126
+
4127
+ ### Console Output
4128
+
4129
+ ```text
4130
+ === Read Validation ===
4131
+ Row 0: A1: "Region", B1: "Quarter", C1: "Sales", D1: "Rep"
4132
+ Row 1: A2: "East", B2: "Q1", C2: 1000, D2: "Alice"
4133
+ Row 2: A3: "West", B3: "Q1", C3: 1500, D3: "Bob"
4134
+ Row 3: A4: "East", B4: "Q2", C4: 1200, D4: "Alice"
4135
+ Row 4: A5: "West", B5: "Q2", C5: 1800, D5: "Bob"
4136
+ Row 5: A6: "North", B6: "Q1", C6: 800, D6: "Charlie"
4137
+ Row 6: A7: "North", B7: "Q2", C7: 950, D7: "Charlie"
4138
+ ```
4139
+
4140
+ <hr/>
4141
+
4013
4142
  ## Row Grouping
4014
4143
 
4015
4144
  Demonstrates outline grouping for rows.
@@ -4357,6 +4486,60 @@ Row 1: A2: "Normal Text" (font=, fill=), B2: "Highlighted Text" (font=Calibri, f
4357
4486
 
4358
4487
  <hr/>
4359
4488
 
4489
+ ## Table Styles
4490
+
4491
+ Visual demonstration for Table Styles.
4492
+
4493
+ ### Rendered Output (LibreOffice Calc)
4494
+
4495
+ <div><img src="../../test/visual/baselines/table_styles/page-1.png" width="100%" alt="Preview"/></div>
4496
+
4497
+ <div><img src="../../test/visual/baselines/table_styles/page-2.png" width="100%" alt="Preview"/></div>
4498
+
4499
+ ### DSL Code
4500
+
4501
+ ```ruby
4502
+ # frozen_string_literal: true
4503
+
4504
+ require "xlsxrb"
4505
+
4506
+ output_path = ARGV[0] || "table_styles.xlsx"
4507
+
4508
+ Xlsxrb.write(output_path) do |wb|
4509
+ wb.sheet("Table Example") do |sheet|
4510
+ sheet.column(0..3, width: 20)
4511
+ sheet.row(%w[ID Name Department Salary])
4512
+ sheet.row([101, "Alice Smith", "Engineering", 120_000])
4513
+ sheet.row([102, "Bob Jones", "Marketing", 95_000])
4514
+ sheet.row([103, "Carol White", "Sales", 110_000])
4515
+ sheet.row([104, "David Brown", "Engineering", 130_000])
4516
+
4517
+ sheet.table("A1:D5", columns: %w[ID Name Department Salary], name: "EmployeeTable", style: "TableStyleMedium9", total_row: false)
4518
+ end
4519
+ end
4520
+
4521
+ puts "=== Read Validation ==="
4522
+ workbook = Xlsxrb.read(output_path).load
4523
+ sheet = workbook.sheets.first
4524
+ sheet.rows.each do |row|
4525
+ row_cells = row.cells.map { |c| "#{c.ref}: #{c.value.inspect}" }
4526
+ puts "Row #{row.index}: #{row_cells.join(", ")}"
4527
+ end
4528
+ ```
4529
+
4530
+ ### Console Output
4531
+
4532
+ ```text
4533
+ === Read Validation ===
4534
+ Row 0: A1: "ID", B1: "Name", C1: "Department", D1: "Salary"
4535
+ Row 1: A2: 101, B2: "Alice Smith", C2: "Engineering", D2: 120000
4536
+ Row 2: A3: 102, B3: "Bob Jones", C3: "Marketing", D3: 95000
4537
+ Row 3: A4: 103, B4: "Carol White", C4: "Sales", D4: 110000
4538
+ Row 4: A5: 104, B5: "David Brown", C5: "Engineering", D5: 130000
4539
+ ```
4540
+
4541
+ <hr/>
4542
+
4360
4543
  ## View Show Grid Lines
4361
4544
 
4362
4545
  Demonstrates disabling visible grid lines in spreadsheet view.
data/docs/wasm/ruby.wasm CHANGED
Binary file