
Officecli
- 800 installs
- 25.5k repo stars
- Updated August 4, 2026
- iofficeai/officecli
Create, analyze, proofread, and modify .docx, .xlsx, and .pptx Office documents from the terminal using the officecli binary.
About
An AI-friendly CLI to create, analyze, proofread, and modify Office documents (.docx, .xlsx, .pptx) as a single dependency-free binary. A developer uses it to inspect, check formatting, add charts, or modify Office files without installing Office.
- Single binary, no dependencies, no Office installation needed
- Create, analyze, proofread, and modify .docx, .xlsx, and .pptx files
Officecli by the numbers
- 800 all-time installs (skills.sh)
- +125 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #139 of 688 Office & Documents skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/iofficeai/officecli --skill officecliAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 800 |
|---|---|
| repo stars | ★ 25.5k |
| Last updated | August 4, 2026 |
| Repository | iofficeai/officecli ↗ |
What it does
Create, analyze, proofread, and modify .docx, .xlsx, and .pptx Office documents from the terminal using the officecli binary.
Files
officecli
AI-friendly CLI for .docx, .xlsx, .pptx. Single binary, no dependencies, no Office installation needed.
Install
If officecli is not installed:
# macOS / Linux
curl -fsSL https://d.officecli.ai/install.sh | bash
# Windows (PowerShell)
irm https://d.officecli.ai/install.ps1 | iexVerify with officecli --version. If still not found after install, open a new terminal.
---
Strategy
L1 (read) → L2 (DOM edit) → L3 (raw XML). Always prefer higher layers. Add --json for structured output.
Before doc work, check Specialized Skills (bottom of this file). Fundraising decks, academic papers, financial models, dashboards, and Morph animations need their own skill loaded first — load_skill once, then proceed.
---
Help System (IMPORTANT)
When unsure about property names, value formats, or command syntax, ALWAYS run help instead of guessing. One help query beats guess-fail-retry loops.
officecli help ≡ officecli --help, and officecli <cmd> --help ≡ officecli help <cmd> — same content.
officecli help # All commands + global options + schema entry points
officecli help docx # List all docx elements
officecli help docx paragraph # Full schema: properties, aliases, examples, readbacks
officecli help docx set paragraph # Verb-filtered: only props usable with `set`
officecli help docx paragraph --json # Structured schema (machine-readable)Format aliases: word→docx, excel→xlsx, ppt/powerpoint→pptx. Verbs: add, set, get, query, remove. MCP exposes the same schema via {"command":"help","format":"docx","type":"paragraph"}.
---
Performance: Resident Mode
Every command auto-starts a resident on first access (60s idle timeout) — file-lock conflicts are automatically avoided. Explicit open/close is still recommended for longer sessions (12min idle):
officecli open report.docx # explicitly keep in memory
officecli set report.docx ... # no file I/O overhead
officecli close report.docx # save and releaseOpt out of auto-start: OFFICECLI_NO_AUTO_RESIDENT=1.
---
Quick Start
PPT:
officecli create slides.pptx
officecli add slides.pptx / --type slide --prop title="Q4 Report" --prop background=1A1A2E
officecli add slides.pptx '/slide[1]' --type shape --prop text="Revenue grew 25%" --prop x=2cm --prop y=5cm --prop font=Arial --prop size=24 --prop color=FFFFFFWord:
officecli create report.docx
officecli add report.docx /body --type paragraph --prop text="Executive Summary" --prop style=Heading1
officecli add report.docx /body --type paragraph --prop text="Revenue increased by 25% year-over-year."Excel:
officecli create data.xlsx
officecli set data.xlsx /Sheet1/A1 --prop value="Name" --prop bold=true
officecli set data.xlsx /Sheet1/A2 --prop value="Alice"---
L1: Create, Read & Inspect
officecli create <file> # Create blank .docx/.xlsx/.pptx (type from extension)
officecli view <file> <mode> # outline | stats | issues | text | annotated | html
officecli get <file> <path> --depth N # Get a node and its children [--json]
officecli query <file> <selector> # CSS-like query
officecli validate <file> # Validate against OpenXML schemaview modes
| Mode | Description | Useful flags |
|---|---|---|
outline | Document structure | |
stats | Statistics (pages, words, shapes) | |
issues | Formatting/content/structure problems | `--type format\ |
text | Plain text extraction | --start N --end N, --max-lines N |
annotated | Text with formatting annotations | |
html | Static HTML snapshot — same renderer as watch, no server needed | --browser, --page N (docx), --start N --end N (pptx) |
screenshot / svg / pdf / forms | PNG via headless browser / SVG (pptx slide) / PDF via exporter plugin / form-fields JSON via format-handler plugin | -o, --screenshot-width/-height, pptx --grid N |
Use view html for one-shot snapshots (CI artifacts, archival, diffing); use watch when you need live refresh or browser-side click-to-select.
get
Any XML path via element localName. Use --depth N to expand children. Add --json for structured output. Default text output is grep-friendly: path (type) "text" key=val key=val ...
officecli get report.docx '/body/p[3]' --depth 2 --json
officecli get slides.pptx '/slide[1]' --depth 1 # list all shapes on slide 1
officecli get data.xlsx '/Sheet1/B2' --jsonStable ID Addressing
Elements with stable IDs return @attr=value paths instead of positional indices. Prefer these in multi-step workflows — positional indices shift on insert/delete, stable IDs do not.
/slide[1]/shape[@id=550950021] # PPT shape
/slide[1]/table[@id=1388430425]/tr[1]/tc[2] # PPT table
/body/p[@paraId=1A2B3C4D] # Word paragraph
/comments/comment[@commentId=1] # Word commentPPT also accepts @name= (e.g. shape[@name=Title 1]), with morph !! prefix awareness. Elements without stable IDs (slide, run, tr/tc, row) fall back to positional indices.
query
CSS-like selectors: [attr=value], [attr!=value], [attr~=text], [attr>=value], [attr<=value], :contains("text"), :empty, :has(formula), :no-alt. Boolean and/or supported across query/set/remove: cell[value>5000 or value<100], cell[(type=Number or type=Date) and value>0]. Excel row-by-column-name: Sheet1!row[Salary>5000]. set accepts selectors and Excel-native paths (parity with get/query). Bare unscoped selectors rejected on set/remove.
officecli query report.docx 'paragraph[style=Normal] > run[font!=Arial]'
officecli query slides.pptx 'shape[fill=FF0000]'---
Watch & Interactive Selection
Live HTML preview that auto-refreshes on every file change. Browsers can click / shift-click / box-drag to select shapes; the CLI can read the current browser selection and act on it.
officecli watch <file> [--port N] # Start preview server (default port 26315)
officecli unwatch <file> # Stop
officecli goto <file> <path> # Scroll watching browser(s) to element (docx: p / table / tr / tc)Open the printed http://localhost:N URL. Click to select; shift/cmd/ctrl+click to multi-select; drag from empty space to box-select. PPT/Word use blue outline; Excel uses native-style green selection (double-click cell to edit inline; drag a chart to reposition).
get <file> selected — read what the user clicked
officecli get <file> selected [--json]Returns DocumentNodes for whatever is currently selected. Empty result if nothing selected. Exit code != 0 if no watch is running.
# User clicks shapes in the browser, then asks "make these red"
PATHS=$(officecli get deck.pptx selected --json | jq -r '.data.Results[].path')
for p in $PATHS; do officecli set deck.pptx "$p" --prop fill=FF0000; doneKey properties
- Selection survives file edits. Paths use stable
@id=form. - All connected browsers share one selection. Last-write-wins.
- Same-file single-watch. A given file can have only one watch process at a time.
- Group shapes select as a whole. Drilling into individual children of a group is not supported in v1.
- Coverage:
.pptxshapes/pictures/tables/charts/connectors/groups;.docxtop-level paragraphs and tables. Inherited layout/master decorations and Word nested elements (table cells, run-level) are not addressable. `.xlsx` does not emit `data-path` —mark/selectionon xlsx always resolvestale=true(v2 candidate).
Marks — edit proposals waiting for review
Use mark when changes need human review BEFORE they hit the file. Marks live in the watch process only; a separate set pipeline applies accepted ones. For one-shot changes use set directly; for permanent file annotations use add --type comment (Word native).
officecli mark <file> <path> [--prop find=... color=... note=... tofix=... regex=true] [--json]
officecli unmark <file> [--path <p> | --all] [--json]
officecli get-marks <file> [--json]Props: find (literal or regex when regex=true; raw form find='r"[abc]"'), color (hex / rgb(...) / 22 named whitelist), note, tofix (drives apply pipeline). Path must be data-path format from watch HTML — see subskills for full pipeline.
---
L2: DOM Operations
set — modify properties
officecli set <file> <path> --prop key=value [--prop ...]Any XML attribute is settable via element path (found via get --depth N) — even attributes not currently present. Without find=, set applies format to the entire element.
Value formats:
| Type | Format | Examples |
|---|---|---|
| Colors | Hex (with/without #), named, RGB, theme | FF0000, #FF0000, red, rgb(255,0,0), accent1..accent6 |
| Spacing | Unit-qualified | 12pt, 0.5cm, 1.5x, 150% |
| Dimensions | EMU or suffixed | 914400, 2.54cm, 1in, 72pt, 96px |
Dotted-attr aliases — font.<attr> forms accepted on shape/run/paragraph/table/row/cell/section/styles, e.g. --prop font.color=red --prop font.bold=true --prop font.size=14pt. Run officecli help <fmt> <element> for the full list.
find — format or replace matched text
Use top-level --find / --replace on set (and --find on query). Legacy --prop find=X still works but emits a hint.
# Format matched text (auto-splits runs)
officecli set doc.docx '/body/p[1]' --find weather --prop bold=true --prop color=red
# Regex matching (regex= still a prop flag)
officecli set doc.docx '/body/p[1]' --find '\d+%' --prop regex=true --prop color=red
# Replace text (use `/` for whole-document scope)
officecli set doc.docx / --find draft --replace final
# docx: tracked Find&Replace
officecli set doc.docx / --find draft --replace final --prop revision.author=Alice
# PPT — same syntax, different paths
officecli set slides.pptx / --find draft --replace finalPath controls search scope: / = whole document, /body/p[1] or /slide[N]/shape[M] = specific element, /header[1] / /footer[1] = headers/footers.
Notes:
- Case-sensitive by default. Case-insensitive:
--prop 'find=(?i)error' --prop regex=true - Matches work across run boundaries
- No match = silent success.
--jsonincludes"matched": N - Excel: only
find+replacesupported (no find + format props)
add — add elements or clone
officecli add <file> <parent> --type <type> [--prop ...]
officecli add <file> <parent> --type <type> --after <path> [--prop ...] # insert after anchor
officecli add <file> <parent> --type <type> --before <path> [--prop ...] # insert before anchor
officecli add <file> <parent> --type <type> --index N [--prop ...] # 0-based position (legacy)
officecli add <file> <parent> --from <path> # clone existing element--after, --before, --index are mutually exclusive. No position flag = append to end.
Element types (with aliases):
| Format | Types |
|---|---|
| pptx | slide (incl. hidden), shape (font.latin/ea/cs, direction=rtl, underline.color, highlight=COLOR (Add/Set/Get/HTML preview), effective.X+effective.X.src; arrow alias for rightArrow; slideMaster/slideLayout typed add/set/remove), picture (SVG, brightness/contrast/glow/shadow, rotation, link, tooltip), chart (direction=rtl, pieOfPie, barOfPie, axisLine/gridline per-attr setters, animation+chartBuild=byCategory |
| docx | paragraph (direction/font.latin/ea/cs, bold.cs/italic.cs/size.cs, lang.latin/ea/cs, wordWrap, framePr.\, tabs shorthand), run (lang slots, direction, underline.color, position half-pts, *revision.type=ins\ |
| xlsx | sheet (visible/hidden/veryHidden, print margins, printTitleRows/Cols, rightToLeft sheetView, cascade-aware rename), row (c{N}= cell-content shorthand; add accepts --from /Sheet/col[L]; formula-ref rewrite on insert), col (formula-ref rewrite, named-range follow on move), cell (type=richtext+runs, merge=range/sweep, direction=rtl, phonetic; **--shift left\ |
Pivot tables (xlsx)
officecli add data.xlsx /Sheet1 --type pivottable \
--prop source="Sheet1!A1:E100" --prop rows=Region,Category \
--prop cols=Year --prop values="Sales:sum,Qty:count" \
--prop grandTotals=rows --prop subtotals=off --prop sort=ascKey props: rows, cols, values (Field:func[:showDataAs]), filters, source, position, layout (compact/outline/tabular), repeatLabels, blankRows, aggregate, showDataAs (percent_of_total/row/col, running_total), grandTotals, subtotals, sort. Aggregators: sum, count, average, max, min, product, stdDev, stdDevp, var, varp, countNums. Date columns auto-group. Run officecli help xlsx pivottable for full schema.
Document-level properties (all formats)
officecli set doc.docx / --prop docDefaults.font=Arial --prop docDefaults.fontSize=11pt
officecli set doc.docx / --prop protection=forms --prop evenAndOddHeaders=true
officecli set data.xlsx / --prop calc.mode=manual --prop calc.refMode=r1c1
officecli set slides.pptx / --prop defaultFont=Arial --prop show.loop=true --prop print.what=handoutsRun officecli help <format> / for all document-level properties (docDefaults, docGrid, CJK spacing, calc, print, show, theme, extended).
Sort (xlsx)
officecli set data.xlsx /Sheet1 --prop sort="C desc" --prop sortHeader=true
officecli set data.xlsx '/Sheet1/A1:D100' --prop sort="A asc" --prop sortHeader=trueFormat: COL DIR[, COL DIR ...]. Rejects ranges with merged cells or formulas. Sidecar metadata (hyperlinks, comments, conditional formatting, drawings) follows rows automatically.
Text-anchored insert (--after find:X / --before find:X)
Locate an insertion point by text match within a paragraph. Inline types (run, picture, hyperlink) insert within the paragraph; block types (table, paragraph) auto-split it. PPT only supports inline.
# Word: inline run after matched text
officecli add doc.docx '/body/p[1]' --type run --after find:weather --prop text=" (sunny)"
# Word: block table after matched text (auto-splits paragraph)
officecli add doc.docx '/body/p[1]' --type table --after "find:First sentence." --prop rows=2 --prop cols=2Clone
officecli add <file> / --from '/slide[1]' — copies with all cross-part relationships.
move, swap, remove
officecli move <file> <path> [--to <parent>] [--index N] [--after <path>] [--before <path>]
officecli swap <file> <path1> <path2>
officecli remove <file> '/body/p[4]'When using --after or --before, --to can be omitted — the target container is inferred from the anchor.
batch — multiple operations in one save cycle
Continues on error by default (returns exit 1 if any item fails). Use --stop-on-error to abort on the first failure. --force is the docx-protection bypass.
officecli dump <file> [<path>] emits a replayable batch JSON for round-trip — .docx (full coverage) and .pptx (text/tables/pictures/charts/notes/theme + OLE/3D/video/audio/SmartArt/morph/p15 transitions via raw-set passthrough). Path defaults to / (whole document); pass a subtree path (/body, /body/p[N], /body/tbl[N], /theme, /settings, /numbering, /styles) to scope the dump. officecli refresh <file.docx> recalculates TOC page numbers / PAGE / cross-references after replay (Word backend on Windows; headless-HTML fallback elsewhere). officecli plugins list extends support to .doc, .hwpx, .pdf export.
echo '[
{"command":"set","path":"/Sheet1/A1","props":{"value":"Name","bold":"true"}},
{"command":"set","path":"/Sheet1/B1","props":{"value":"Score","bold":"true"}}
]' | officecli batch data.xlsx --json
officecli batch data.xlsx --commands '[{"op":"set","path":"/Sheet1/A1","props":{"value":"Done"}}]' --json
officecli batch data.xlsx --input updates.json --force --jsonSupports: add, set, get, query, remove, move, swap, view, raw, raw-set, validate. Fields: command (or op), path, parent, type, from, to, index, after, before, props, selector, mode, depth, part, xpath, action, xml.
---
L3: Raw XML
Use when L2 cannot express what you need. No xmlns declarations needed — prefixes auto-registered.
officecli raw <file> <part> # view raw XML
officecli raw-set <file> <part> --xpath "..." --action replace --xml '<w:p>...</w:p>'
officecli add-part <file> <parent> # create new document part (returns rId)raw-set actions: append, prepend, insertbefore, insertafter, replace, remove, setattr. Run officecli help <format> raw for available parts.
---
Common Pitfalls
| Pitfall | Correct Approach |
|---|---|
--name "foo" | Use --prop name="foo" — all attributes go through --prop |
Unquoted [N] paths in zsh/bash | Always quote: '/slide[1]' or "/slide[1]" (shell glob-expands brackets) |
PPT shape[1] for content | shape[1] is typically the title placeholder. Use shape[2]+ for content shapes |
/shape[myname] | Name indexing not supported. Use numeric index or @name= (PPT only) |
| Guessing property names | Run officecli help <format> <element> to see exact names |
| Modifying an open file | Close the file in PowerPoint/WPS first |
\n in shell strings | Use \\n for newlines in --prop text="..." |
$ in shell text | --prop text="$15M" strips $15. Use single quotes: --prop text='$15M', or heredoc batch |
---
Specialized Skills
officecli load_skill <name> — output is a SKILL.md, follow its rules.
Loading rule:
- Pick the most specific match in "When to use"; if none fits, load the format default (
word/pptx/excel). - Scenes already contain the format default's rules — load one skill per artifact, never stack.
- Loaded rules persist across turns; don't re-load each reply.
- Two distinct artifacts → two separate loads.
Word (.docx)
| Name | When to use |
|---|---|
word | Reports, letters, memos, proposals, generic documents |
academic-paper | Journal / conference / thesis: APA / Chicago / IEEE / MLA citations, equations, SEQ + PAGEREF cross-refs, multi-column journal layout, bibliography. NOT for business reports or letters (route those to word) |
PowerPoint (.pptx)
| Name | When to use |
|---|---|
pptx | Generic decks: board reviews, sales decks, all-hands, product launches |
pitch-deck | Fundraising only — seed / Series A-C / SAFE / convertible / strategic raise. NOT for sales / product / board decks (route those to pptx) |
morph-ppt | Cinematic Morph-animated presentations. NOT for static decks (route those to pptx) |
morph-ppt-3d | 3D Morph: GLB models, camera moves, depth. NOT for 2D-only Morph (route those to morph-ppt) |
Excel (.xlsx)
| Name | When to use |
|---|---|
excel | Generic workbooks, formulas, pivots, trackers |
financial-model | Financial models, scenarios, projections. NOT for general data analysis (route those to excel) |
data-dashboard | CSV/tabular data → KPI / analytics / executive dashboards with charts and sparklines. NOT for raw data tracking (route those to excel) |
Example: a fundraising deck task → officecli load_skill pitch-deck → use the printed rules.
---
Notes
- Paths are 1-based (XPath convention):
'/body/p[3]'= third paragraph --indexis 0-based (array convention):--index 0= first position- Excel exception: for
add --type rowandadd --type col,--index Nis 1-based (matches OOXML RowIndex / column letter index).--index 5inserts at row 5 / column 5. - After modifications, verify with
validateand/orview issues - When unsure, run
officecli help <format> <element>instead of guessing
name: Build
on:
workflow_dispatch:
push:
tags:
- 'v*'
permissions:
contents: read
jobs:
build:
strategy:
matrix:
include:
- rid: osx-arm64
name: officecli-mac-arm64
os: macos-latest
- rid: osx-x64
name: officecli-mac-x64
os: macos-latest
- rid: linux-x64
name: officecli-linux-x64
os: ubuntu-latest
- rid: linux-arm64
name: officecli-linux-arm64
os: ubuntu-latest
- rid: linux-musl-x64
name: officecli-linux-alpine-x64
os: ubuntu-latest
- rid: linux-musl-arm64
name: officecli-linux-alpine-arm64
os: ubuntu-latest
- rid: win-x64
name: officecli-win-x64.exe
os: windows-latest
- rid: win-arm64
name: officecli-win-arm64.exe
os: windows-latest
runs-on: ${{ matrix.os }}
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v5
- name: Setup .NET
uses: actions/setup-dotnet@v5
with:
dotnet-version: '10.0.x'
- name: Publish
run: dotnet publish src/officecli/officecli.csproj -c Release -r ${{ matrix.rid }} -o publish --nologo
- name: Rename output
run: |
if [ -f publish/officecli.exe ]; then
mv publish/officecli.exe publish/${{ matrix.name }}
else
mv publish/officecli publish/${{ matrix.name }}
fi
- name: Setup macOS code signing
if: startsWith(matrix.rid, 'osx-')
env:
BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
run: |
CERT_PATH="$RUNNER_TEMP/build_certificate.p12"
KEYCHAIN_PATH="$RUNNER_TEMP/build.keychain-db"
echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode > "$CERT_PATH"
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security import "$CERT_PATH" -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k "$KEYCHAIN_PATH"
security set-key-partition-list -S apple-tool:,apple:,codesign: -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security list-keychain -d user -s "$KEYCHAIN_PATH" login.keychain-db
rm -f "$CERT_PATH"
- name: Codesign (macOS)
if: startsWith(matrix.rid, 'osx-')
run: |
IDENTITY=$(security find-identity -v -p codesigning | grep "Developer ID Application" | head -n 1 | sed -E 's/.*"(.+)"/\1/')
echo "Signing with: $IDENTITY"
# --options runtime (Hardened Runtime, required by notarization) denies the
# self-contained CoreCLR's MAP_JIT executable memory by default, so the runtime
# fails to start with "Failed to create CoreCLR, HRESULT: 0x80070008" on every
# command. build/officecli.entitlements re-permits exactly allow-jit (the plist
# holds no XML comments — codesign's AMFI parser rejects them).
codesign --force --options runtime --entitlements build/officecli.entitlements --timestamp --sign "$IDENTITY" publish/${{ matrix.name }}
codesign --verify --strict --verbose=2 publish/${{ matrix.name }}
# Fail the build if allow-jit did not actually embed (e.g. a malformed plist
# makes codesign silently drop entitlements) — otherwise the bug ships again.
codesign -d --entitlements - --xml publish/${{ matrix.name }} | grep -q allow-jit
- name: Notarize (macOS)
if: startsWith(matrix.rid, 'osx-')
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
APP_SPECIFIC_PASSWORD: ${{ secrets.APP_SPECIFIC_PASSWORD }}
TEAM_ID: ${{ secrets.TEAM_ID }}
run: |
# A bare Mach-O binary cannot be stapled; notarytool requires a
# zip/pkg/dmg container. Submit a zip — the notarization ticket is
# recorded on Apple's servers and Gatekeeper validates online.
ditto -c -k --keepParent publish/${{ matrix.name }} "$RUNNER_TEMP/notarize.zip"
xcrun notarytool submit "$RUNNER_TEMP/notarize.zip" \
--apple-id "$APPLE_ID" \
--team-id "$TEAM_ID" \
--password "$APP_SPECIFIC_PASSWORD" \
--wait
rm -f "$RUNNER_TEMP/notarize.zip"
- name: Smoke test - create document
if: >-
(matrix.rid == 'osx-arm64' && runner.arch == 'ARM64') ||
(matrix.rid == 'osx-x64' && runner.arch == 'X64') ||
(matrix.rid == 'linux-x64' && runner.os == 'Linux') ||
(matrix.rid == 'win-x64' && runner.os == 'Windows')
env:
# Disable Git Bash (MSYS) POSIX-to-Windows path conversion on
# windows-latest, which otherwise mangles `/body` into
# `C:/Program Files/Git/body` before it reaches the CLI.
MSYS_NO_PATHCONV: '1'
MSYS2_ARG_CONV_EXCL: '*'
run: |
chmod +x publish/${{ matrix.name }}
publish/${{ matrix.name }} create test_smoke.docx
publish/${{ matrix.name }} add test_smoke.docx /body --type paragraph --prop text="Hello from CI"
publish/${{ matrix.name }} get test_smoke.docx '/body/p[1]'
publish/${{ matrix.name }} close test_smoke.docx
rm -f test_smoke.docx
- name: Smoke test - .NET 8-only runtime (linux-x64)
if: matrix.rid == 'linux-x64' && runner.os == 'Linux'
run: |
chmod +x publish/${{ matrix.name }}
mkdir -p smoke_net8
docker run --rm \
-v "$PWD/publish:/app:ro" \
-v "$PWD/smoke_net8:/work" \
-w /work \
mcr.microsoft.com/dotnet/runtime:8.0 \
bash -c "set -e; \
/app/${{ matrix.name }} create issue115.docx; \
/app/${{ matrix.name }} add issue115.docx /body --type paragraph --prop text='net8 smoke'; \
/app/${{ matrix.name }} close issue115.docx; \
/app/${{ matrix.name }} view issue115.docx text --json"
- name: Smoke test - install
if: >-
(matrix.rid == 'osx-arm64' && runner.arch == 'ARM64') ||
(matrix.rid == 'osx-x64' && runner.arch == 'X64') ||
(matrix.rid == 'linux-x64' && runner.os == 'Linux') ||
(matrix.rid == 'win-x64' && runner.os == 'Windows')
env:
MSYS_NO_PATHCONV: '1'
MSYS2_ARG_CONV_EXCL: '*'
run: |
publish/${{ matrix.name }} install
if [ "$RUNNER_OS" == "Windows" ]; then
test -f "$LOCALAPPDATA/OfficeCLI/officecli.exe" || { echo "FAIL: officecli.exe not found in %LOCALAPPDATA%\\OfficeCLI"; exit 1; }
"$LOCALAPPDATA/OfficeCLI/officecli.exe" --version
else
test -f "$HOME/.local/bin/officecli" || { echo "FAIL: officecli not found in ~/.local/bin"; exit 1; }
"$HOME/.local/bin/officecli" --version
fi
- name: Smoke test - install.sh / install.ps1
# Exercises the shell installers themselves (separate from the CLI's
# own `install` subcommand tested above). Downloads from
# d.officecli.ai with github fallback — validates the production
# mirror is reachable AND the in-repo script is syntactically and
# logically correct on each platform.
if: >-
(matrix.rid == 'osx-arm64' && runner.arch == 'ARM64') ||
(matrix.rid == 'osx-x64' && runner.arch == 'X64') ||
(matrix.rid == 'linux-x64' && runner.os == 'Linux') ||
(matrix.rid == 'win-x64' && runner.os == 'Windows')
shell: bash
env:
MSYS_NO_PATHCONV: '1'
MSYS2_ARG_CONV_EXCL: '*'
run: |
if [ "$RUNNER_OS" == "Windows" ]; then
pwsh -NoProfile -ExecutionPolicy Bypass -File ./install.ps1
test -f "$LOCALAPPDATA/OfficeCLI/officecli.exe" || { echo "FAIL: officecli.exe not installed"; exit 1; }
"$LOCALAPPDATA/OfficeCLI/officecli.exe" --version
else
bash install.sh
test -f "$HOME/.local/bin/officecli" || { echo "FAIL: officecli not installed at ~/.local/bin"; exit 1; }
"$HOME/.local/bin/officecli" --version
fi
- name: Upload artifact
uses: actions/upload-artifact@v6
with:
name: ${{ matrix.name }}
path: publish/${{ matrix.name }}
release:
needs: build
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v')
permissions:
contents: write
steps:
- name: Download all artifacts
uses: actions/download-artifact@v8
with:
path: artifacts
- name: Flatten artifacts and generate checksums
run: |
mkdir -p flat
find artifacts -type f -exec mv {} flat/ \;
rm -rf artifacts
mv flat artifacts
cd artifacts
sha256sum officecli-* > SHA256SUMS
echo "=== SHA256SUMS ==="
cat SHA256SUMS
- name: Create Draft Release
uses: softprops/action-gh-release@v3
with:
files: artifacts/**/*
generate_release_notes: true
draft: true
name: Skill parity
# Root SKILL.md is the file the public URL serves
# (https://d.officecli.ai/SKILL.md, raw GitHub URL) and is what
# `curl ... | bash` consumers fetch. skills/officecli/SKILL.md is a
# symlink to it — the spec-conforming location that `gh skill install`
# and `npx skills add` discover, and what the binary embeds.
#
# On macOS / Linux the symlink resolves transparently and this diff
# always passes. The check exists for the Windows case: git on Windows
# without core.symlinks=true checks out symlinks as plain text files
# containing the link target path (e.g. literal "../../SKILL.md"), so
# diff catches that corruption before it ships in a release build.
on:
push:
branches: [main]
pull_request:
paths:
- 'SKILL.md'
- 'skills/officecli/SKILL.md'
- '.github/workflows/skill-parity.yml'
permissions:
contents: read
jobs:
diff:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Diff root SKILL.md vs skills/officecli/SKILL.md
run: |
if ! diff -q SKILL.md skills/officecli/SKILL.md; then
echo "::error::SKILL.md and skills/officecli/SKILL.md are out of sync."
echo "The two files must be byte-identical. The binary embeds skills/officecli/SKILL.md;"
echo "the public URL (d.officecli.ai/SKILL.md, raw GitHub) serves the root copy."
echo "Fix: cp SKILL.md skills/officecli/SKILL.md (or the reverse) and commit."
exit 1
fi
echo "OK: SKILL.md == skills/officecli/SKILL.md"
#!/bin/bash
set -e
PROJECT="src/officecli/officecli.csproj"
ALL_TARGETS="osx-arm64:officecli-mac-arm64 osx-x64:officecli-mac-x64 linux-x64:officecli-linux-x64 linux-arm64:officecli-linux-arm64 linux-musl-x64:officecli-linux-alpine-x64 linux-musl-arm64:officecli-linux-alpine-arm64 win-x64:officecli-win-x64.exe win-arm64:officecli-win-arm64.exe"
# Detect current platform RID
detect_local_rid() {
local OS=$(uname -s | tr '[:upper:]' '[:lower:]')
local ARCH=$(uname -m)
local LIBC="gnu"
if [ "$OS" = "linux" ]; then
if command -v ldd >/dev/null 2>&1 && ldd --version 2>&1 | grep -qi musl; then
LIBC="musl"
elif [ -f /etc/alpine-release ]; then
LIBC="musl"
fi
fi
case "$OS" in
darwin)
case "$ARCH" in
arm64) echo "osx-arm64" ;;
x86_64) echo "osx-x64" ;;
esac ;;
linux)
case "$ARCH" in
x86_64)
if [ "$LIBC" = "musl" ]; then echo "linux-musl-x64"; else echo "linux-x64"; fi ;;
aarch64|arm64)
if [ "$LIBC" = "musl" ]; then echo "linux-musl-arm64"; else echo "linux-arm64"; fi ;;
esac ;;
esac
}
# Find target entry by RID
find_target() {
local RID="$1"
for target in $ALL_TARGETS; do
if [ "${target%%:*}" = "$RID" ]; then
echo "$target"
return
fi
done
}
build_config() {
local CONFIG="$1"
local TARGETS="$2"
local OUTPUT="bin/$(echo "$CONFIG" | tr '[:upper:]' '[:lower:]')"
rm -rf "$OUTPUT"
mkdir -p "$OUTPUT"
for target in $TARGETS; do
RID="${target%%:*}"
NAME="${target##*:}"
TMPDIR=$(mktemp -d)
echo "[$CONFIG] Building $RID -> $NAME"
dotnet publish "$PROJECT" -c "$CONFIG" -r "$RID" -o "$TMPDIR" --nologo -v quiet
# Atomic replace: stage as .new alongside the target, sign there, then rename.
# Overwriting the binary in place would trash the text segment of any
# running officecli process that happens to be mmap'd on this path
# (macOS does not block ETXTBSY), leaving it stuck in uninterruptible
# `UE` state on the next code page fault.
if [ -f "$TMPDIR/officecli.exe" ]; then
cp "$TMPDIR/officecli.exe" "$OUTPUT/$NAME.new"
else
cp "$TMPDIR/officecli" "$OUTPUT/$NAME.new"
fi
# Ad-hoc codesign on macOS (required by AppleSystemPolicy).
# Done on the staged .new copy so the live binary is never mutated in place.
if [ "$(uname -s)" = "Darwin" ] && [[ "$RID" == osx-* ]]; then
codesign -s - -f "$OUTPUT/$NAME.new" 2>/dev/null || true
fi
mv -f "$OUTPUT/$NAME.new" "$OUTPUT/$NAME"
cp "$TMPDIR/officecli.pdb" "$OUTPUT/${NAME%.*}.pdb"
rm -rf "$TMPDIR"
done
rm -rf src/officecli/bin src/officecli/obj
echo ""
echo "$CONFIG build complete:"
ls -lh "$OUTPUT"
}
CONFIG="${1:-release}"
case "$CONFIG" in
release|Release)
LOCAL_RID=$(detect_local_rid)
TARGET=$(find_target "$LOCAL_RID")
if [ -z "$TARGET" ]; then
echo "Unsupported platform: $(uname -s) $(uname -m)"
exit 1
fi
build_config "Release" "$TARGET"
;;
debug|Debug)
LOCAL_RID=$(detect_local_rid)
TARGET=$(find_target "$LOCAL_RID")
if [ -z "$TARGET" ]; then
echo "Unsupported platform: $(uname -s) $(uname -m)"
exit 1
fi
build_config "Debug" "$TARGET"
;;
all)
build_config "Release" "$ALL_TARGETS"
;;
*)
echo "Usage: ./build.sh [release|debug|all]"
echo " release - Build Release for current platform (default)"
echo " debug - Build Debug for current platform"
echo " all - Build Release for all platforms"
exit 1
;;
esac
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
</dict>
</plist>
Contributing to OfficeCLI
中文版 / Chinese version: CONTRIBUTING.zh.md
You must follow the two rules below. Code style, dependencies, tests, and
docs are handled by the maintainer in post-merge cleanup — do not worry
about them.
Rule 1: One PR = one atomic change
A PR must contain exactly one feature or one bug fix that cannot be further decomposed. If your change can be split into multiple pieces that each have standalone value, submit each piece as a separate PR.
Self-check
Before opening the PR, ask your AI tool:
"Analyze this diff. Can it be decomposed into multiple PRs where each
could be merged or reverted independently? If yes, list them."
If the answer is "yes, N PRs", split into N PRs before submitting.
Examples
✅ Single-PR bugs — one root cause, one fix
Picture added with only 'width' specified gets wrong default heightBody-level find: anchor throws ArgumentExceptionAddParagraph --index N is off-by-one when the body contains a table
✅ Single-PR features — one coherent capability
query ole: list embedded OLE objects with ProgID and dimensionsset wrap/hposition/vposition on floating pictures
❌ Must split — multiple independent changes bundled together
Fix picture index bug + add OLE detection + add HTML heading numbering
→ 3 PRs, zero shared code
Add OLE object detection + add EMF→PNG conversion
→ 2 PRs, two independent layers
Add auto aspect ratio + fix index off-by-one + fix line spacing clipping
→ 3 PRs, three unrelated root causes
🤔 Judgment calls — default to splitting
Add helper function + its first consumer
→ 1 or 2 PRs; split if the helper has standalone reuse potential
Add read support + add write support for the same property
→ 1 or 2 PRs; split if you want read to land before write is vetted
Rule 2: Every PR must include a verifiable validation method
State in the PR description (or a linked issue) how a reviewer can confirm your change actually works.
For bug-fix PRs — pick one (in order of preference)
1. officecli command sequence showing broken output before and fixed output after 2. Shell or Python script that reproduces the bug and runs clean after the fix 3. Authoritative reference showing what the correct behavior should be (OOXML spec, Microsoft / ECMA docs, etc.) 4. Screenshot — only when the bug is purely visual
For feature PRs — include at minimum
- A screenshot of the feature in action (Word / Excel / PowerPoint
window, HTML preview, or terminal output)
- Optionally a command sequence showing how to trigger it
Examples
Bug fix — command sequence (ideal):
# Before my fix:
officecli blank test.docx
officecli add test.docx picture --prop "path=photo-2x1.png" --prop "width=10cm"
officecli query test.docx picture
# → height: "10.2cm" ❌ WRONG (hardcoded 4-inch default)
# After my fix:
officecli blank test.docx
officecli add test.docx picture --prop "path=photo-2x1.png" --prop "width=10cm"
officecli query test.docx picture
# → height: "5.0cm" ✓ CORRECT (auto-computed from 2:1 pixel ratio)Feature — screenshot (ideal):
Heading auto-numbering from style chain
>
Before: ![heading-before.png] (plain "Chapter One" with no number)
After: ![heading-after.png] ("1. Chapter One" with auto-numbering span)
>
How to trigger:
```bash
officecli blank demo.docx
officecli add demo.docx paragraph --prop "style=Heading1" --prop "text=Chapter One"
officecli watch demo.docx
```
If you don't follow these rules
The maintainer reserves two options.
Option A — Reject and ask for resubmission (preferred)
The maintainer closes the PR with a link to this guide and asks you to resubmit as properly decomposed PRs with validation methods.
Your credit: the PR is entirely yours, including the "Merged" badge after resubmission.
Option B — Cherry-pick the valuable parts (last resort)
If part of your PR is clearly valuable and worth saving, the maintainer runs git cherry-pick on those commits into main directly and closes the original PR.
Your credit:
git cherry-pickpreserves the original author, sogit logand
git blame still show you as author of those lines.
- The maintainer's reconcile commit message carries a
Co-authored-by: <you> <your-email> trailer, which counts toward your GitHub contribution graph.
- However, the original PR shows as "Closed" instead of "Merged".
为 OfficeCLI 贡献代码
English / 英文主文件: CONTRIBUTING.md
你必须遵守下面两条规则。代码风格、依赖、测试、文档由维护者在 merge 之后通过
follow-up commit 处理 —— 不用操心。
Rule 1: 一个 PR 只做一件不可再拆的事
一个 PR 必须包含且仅包含一个 feature 或一个 bug 修复,而且这个单元不能再被拆分。 如果你的改动可以被拆成多个每个都有独立价值的部分,就拆成多个 PR 分别提交。
自检
提交前,先让你的 AI 做一次拆分分析:
"分析下面这一坨 diff,它能不能拆成多个独立的 PR,每个都可以独立 merge 或独立
revert?如果可以,列出来。"
如果回答是"可以,N 个 PR",就先拆再提。
Examples
✅ 可以作为一个 PR 的 bug —— 单一根因,单一修复
图片只指定 width 时 height fallback 错了body 级 find: 锚点抛 ArgumentExceptionAddParagraph --index N 在 body 含 table 时偏移
✅ 可以作为一个 PR 的 feature —— 单一 coherent 能力
query ole: 列出所有嵌入的 OLE 对象及其 ProgID 和尺寸set wrap/hposition/vposition on floating pictures
❌ 必须拆 —— 多个独立改动被打包
修图片索引 bug + 加 OLE 检测 + 加 HTML heading 编号
→ 3 个 PR,零共享代码
加 OLE 对象检测 + 加 EMF→PNG 转换
→ 2 个 PR,两个独立 layer
加自动宽高比 + 修索引 off-by-one + 修行距裁剪
→ 3 个 PR,三个不相关的根因
🤔 可拆可不拆 —— 默认选拆
加一个 helper 函数 + 第一处调用者
→ 1 或 2 个 PR;helper 有独立复用价值就拆
加 read 支持 + 加 write 支持(同一属性)
→ 1 或 2 个 PR;希望 read 先被 vet 就拆
Rule 2: 每个 PR 必须附带可验证的验证方法
在 PR description 或关联 issue 里写清楚:reviewer 怎么才能验证你的改动真的有效。
Bug 修复 PR —— 至少给出一种(按优先顺序)
1. officecli 命令序列,展示改动前的错误输出和改动后的正确输出 2. shell 或 python 脚本,能复现 bug、在修复后干净退出 3. 权威文档引用,说明正确行为应该是什么样(OOXML spec、Microsoft / ECMA 文档等) 4. 截图 —— 仅当 bug 纯粹是视觉问题时
Feature PR —— 至少包含
- 一张截图,展示 feature 实际效果(Word / Excel / PowerPoint 窗口、HTML
预览、或终端输出)
- 可选:一段 shell 命令序列说明如何触发这个 feature
Examples
Bug 修复 —— 命令序列格式(最理想):
# Before my fix:
officecli blank test.docx
officecli add test.docx picture --prop "path=photo-2x1.png" --prop "width=10cm"
officecli query test.docx picture
# → height: "10.2cm" ❌ 错(硬编码 4 英寸 fallback)
# After my fix:
officecli blank test.docx
officecli add test.docx picture --prop "path=photo-2x1.png" --prop "width=10cm"
officecli query test.docx picture
# → height: "5.0cm" ✓ 对(根据 2:1 像素比例自动计算)Feature —— 截图格式(最理想):
标题自动编号(从 style chain 解析)
>
Before: ![heading-before.png] (纯 "Chapter One",无编号)
After: ![heading-after.png] ("1. Chapter One",带自动编号 span)
>
如何触发:
```bash
officecli blank demo.docx
officecli add demo.docx paragraph --prop "style=Heading1" --prop "text=Chapter One"
officecli watch demo.docx
```
如果你不遵守这两条规则
维护者保留以下两种处理方式。
Option A —— 拒绝并要求重新提交(首选)
维护者关闭 PR,留一条指向本 guide 的 comment,请你按规则拆分后重新提交。
你的 credit: PR 完全归你,重新提交成功后仍然拿 "Merged" badge。
Option B —— Cherry-pick 有价值的部分(最后手段)
如果你的 PR 里有一部分明显有价值、值得保留,维护者会用 git cherry-pick 直接把 这些 commit 摘到 main,然后关闭原 PR。
你的 credit:
git cherry-pick保留原作者,所以git log和git blame里那些代码行仍然
显示你是作者。
- 维护者创建的 reconcile commit message 会附带
Co-authored-by: <you> <your-email> trailer,GitHub 贡献图会把它算进你的 contribution。
- 但原 PR 会显示为 "Closed" 而不是 "Merged"。
#!/bin/bash
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT="$SCRIPT_DIR/src/officecli/officecli.csproj"
BINARY_NAME="officecli"
# Detect platform
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
ARCH=$(uname -m)
case "$OS" in
darwin)
case "$ARCH" in
arm64) RID="osx-arm64" ;;
x86_64) RID="osx-x64" ;;
*) echo "Unsupported architecture: $ARCH"; exit 1 ;;
esac
;;
linux)
# Detect musl libc (Alpine, etc.)
LIBC="gnu"
if command -v ldd >/dev/null 2>&1 && ldd --version 2>&1 | grep -qi musl; then
LIBC="musl"
elif [ -f /etc/alpine-release ]; then
LIBC="musl"
fi
case "$ARCH" in
x86_64)
if [ "$LIBC" = "musl" ]; then RID="linux-musl-x64"; else RID="linux-x64"; fi ;;
aarch64|arm64)
if [ "$LIBC" = "musl" ]; then RID="linux-musl-arm64"; else RID="linux-arm64"; fi ;;
*) echo "Unsupported architecture: $ARCH"; exit 1 ;;
esac
;;
*)
echo "Unsupported OS: $OS"
exit 1
;;
esac
# Build
echo "Building officecli ($RID)..."
TMPDIR=$(mktemp -d)
dotnet publish "$PROJECT" -c Release -r "$RID" -o "$TMPDIR" --nologo -v quiet
echo "Build complete."
# Install
EXISTING=$(command -v "$BINARY_NAME" 2>/dev/null || true)
if [ -n "$EXISTING" ]; then
INSTALL_DIR=$(dirname "$EXISTING")
echo "Found existing installation at $EXISTING, upgrading..."
else
INSTALL_DIR="$HOME/.local/bin"
fi
mkdir -p "$INSTALL_DIR"
# Atomic replace: stage as .new alongside the target, sign there, then rename.
# Overwriting the binary in place would trash the text segment of any
# running officecli process (macOS does not block ETXTBSY), leaving it
# stuck in uninterruptible `UE` state on the next code page fault.
cp "$TMPDIR/$BINARY_NAME" "$INSTALL_DIR/$BINARY_NAME.new"
chmod +x "$INSTALL_DIR/$BINARY_NAME.new"
rm -rf "$TMPDIR"
# macOS: remove quarantine flag and ad-hoc codesign (required by AppleSystemPolicy)
# Done on the staged .new copy so the live binary is never mutated in place.
if [ "$(uname -s)" = "Darwin" ]; then
xattr -d com.apple.quarantine "$INSTALL_DIR/$BINARY_NAME.new" 2>/dev/null || true
codesign -s - -f "$INSTALL_DIR/$BINARY_NAME.new" 2>/dev/null || true
fi
mv -f "$INSTALL_DIR/$BINARY_NAME.new" "$INSTALL_DIR/$BINARY_NAME"
# Hint if not in PATH
case ":$PATH:" in
*":$INSTALL_DIR:"*) ;;
*) echo "Add to PATH: export PATH=\"$INSTALL_DIR:\$PATH\""
echo "Or add the line above to your ~/.zshrc or ~/.bashrc" ;;
esac
echo "OfficeCLI installed successfully!"
echo "Run 'officecli --help' to get started."
Cell Formatting Showcase
Exercises the full xlsx cell property surface — the single most-used Excel element. Three files work together:
- cell-formatting.py — Python script that drives
officeclito build the workbook. - cell-formatting.xlsx — The generated 6-sheet workbook.
- cell-formatting.md — This file.
Regenerate
cd examples/excel
python3 cell-formatting.py
# → cell-formatting.xlsxset auto-creates the target cell, so no per-cell add is needed. The script uses resident mode (open … close) for speed and registers an atexit close so the resident process is never left dangling on error.
Thecell()helper wraps each--prop k=vinshlex.quote. This matters:
a currency format likenumberformat=$#,##0.00contains$#, which a shell
would otherwise expand to the positional-arg count. Quoting keeps the code
literal.
Sheets
Sheet1 — Fonts
Each row pairs a property label (column A) with a rendered sample (column B): font.name, font.size, font.bold, font.italic, font.color, underline=single, underline=double, strike, and a combined run.
officecli set file.xlsx /Sheet1/B11 \
--prop value="Bold + italic + blue + 14pt" \
--prop font.bold=true --prop font.italic=true \
--prop font.color=2E75B6 --prop font.size=14Sheet2 — Fills & alignment
| Feature | Spec |
|---|---|
| Solid hex fill | fill=E63946 |
| Named color | fill=gold |
rgb() form | fill="rgb(46,157,182)" |
| Horizontal align | `alignment.horizontal=left\ |
| Vertical align | `alignment.vertical=top\ |
| Wrap text | alignment.wrapText=true (aliases wrap, wrapText) |
| Reading order | alignment.readingOrder=rtl |
Vertical alignment only shows visibly when the row is taller than the text, so the script bumps row[6..8] height to 34pt via /Fills/row[6] --prop height=34.
The sheet also shows three alignment properties set directly (canonical keys):
| Feature | Spec |
|---|---|
| Text rotation | alignment.textRotation=45 (0-90 up / 91-180 down / 255 stacked; alias rotation) |
| Indent | alignment.indent=3 (alias indent) |
| Shrink to fit | alignment.shrinkToFit=true (alias shrink) |
Sheet3 — Borders
officecli set file.xlsx /Borders/B3 --prop border=thin # shorthand: all four sides
officecli set file.xlsx /Borders/B5 --prop border.all=medium # explicit "all" form
officecli set file.xlsx /Borders/B7 --prop border=thick --prop border.color=C00000
officecli set file.xlsx /Borders/B9 --prop border.bottom=double # single side
officecli set file.xlsx /Borders/B13 --prop border.left=thick --prop border.top=thin \
--prop border.right=medium --prop border.bottom=double
# Diagonal borders — direction via diagonalUp/Down; color requires a diagonal line.
officecli set file.xlsx /Borders/B15 --prop border.diagonal=thin --prop border.diagonalUp=true
officecli set file.xlsx /Borders/B17 --prop border.diagonal=medium --prop border.diagonalDown=true \
--prop border.diagonal.color=C00000Styles accepted: thin, medium, thick, double, dashed, … (full list in schemas/help/xlsx/cell.json → border.*). border.diagonal.color requires a border.diagonal line to attach to.
Sheet4 — Number formats
The label column is the format code; column B is the same kind of value with that numberformat applied:
numberformat= | Value → Display |
|---|---|
#,##0 | 1234567 → 1,234,567 |
#,##0.00 | 1234.5 → 1,234.50 |
0.00% | 0.1834 → 18.34% |
$#,##0.00 | 29999.9 → $29,999.90 |
yyyy-mm-dd | 45413 → 2024-05-01 |
0.00E+00 | 602214 → 6.02E+05 |
_(* #,##0.00_);_(* (#,##0.00);_(* "-"??_) | -4250 → (4,250.00) |
The0.00E+00label cell is written withtype=string, otherwise Excel
parses the literal text0.00E+00as the number0.
Sheet5 — Values, formulas, links
officecli set file.xlsx /Data/B5 --prop formula="B3*B4" --prop numberformat="$#,##0.00" # 12 × 4.50 = $54.00
officecli set file.xlsx /Data/B7 --prop value=007 --prop type=string # keep leading zeros
officecli set file.xlsx /Data/A9 --prop value="OfficeCLI on GitHub" \
--prop link="https://github.com/iOfficeAI/OfficeCLI" --prop tooltip="Open the repo"
officecli set file.xlsx /Data/A11 --prop value="locked cell" --prop locked=true # effective once sheet is protected
officecli set file.xlsx /Data/A13 --prop value="Merged title" --prop merge="A13:C13" \
--prop alignment.horizontal=center
officecli set file.xlsx /Data/B15 --prop arrayformula="B3*2" # dynamic-array spillSheet6 — Rich-text runs
runs is an add-time property (requires --type cell and type=richtext). Each run is a JSON object with "text" plus optional font props (bold, italic, color, size, underline, strike, superscript, subscript). set does not support rich-text; use add.
# Bold+red / italic+blue / normal run in one cell
officecli add file.xlsx /RichText --type cell --prop ref=A3 \
--prop type=richtext \
--prop 'runs=[{"text":"Bold + Red ","bold":true,"color":"C00000"},{"text":"Italic + Blue","italic":true,"color":"2E75B6"},{"text":" Normal"}]'
# Chemical formula with superscript: H₂O
officecli add file.xlsx /RichText --type cell --prop ref=A5 \
--prop type=richtext \
--prop 'runs=[{"text":"H","bold":true,"color":"1F4E79","size":18},{"text":"2","superscript":true,"size":10},{"text":"O water formula","color":"1F4E79"}]'
# strike / underline / different size in one cell
officecli add file.xlsx /RichText --type cell --prop ref=A7 \
--prop type=richtext \
--prop 'runs=[{"text":"Strike","strike":true},{"text":" | "},{"text":"underline","underline":"single"},{"text":" | "},{"text":"size 14pt","size":14}]'Features: type=richtext, runs (JSON array of run objects), per-run: text, bold, italic, color, size, underline, strike, superscript, subscript
Complete Feature Coverage
| Feature | Sheet |
|---|---|
font.name, font.size, font.bold, font.italic, font.color | Sheet1 |
underline=single, underline=double | Sheet1 |
strike=true | Sheet1 |
superscript=true, subscript=true | Sheet1 |
fill (hex, named, rgb) | Sheet2 |
alignment.horizontal (left/center/right) | Sheet2 |
alignment.vertical (top/center/bottom) | Sheet2 |
alignment.wrapText | Sheet2 |
alignment.readingOrder (rtl) | Sheet2 |
alignment.textRotation (0-255) | Sheet2 |
alignment.indent | Sheet2 |
alignment.shrinkToFit | Sheet2 |
border (shorthand all sides) | Sheet3 |
border.all, border.top/bottom/left/right | Sheet3 |
border.color, border.diagonal, border.diagonalUp, border.diagonalDown, border.diagonal.color | Sheet3 |
numberformat (thousands, %, currency, date, scientific, accounting) | Sheet4 |
value, type=string | Sheet5 |
formula, arrayformula | Sheet5 |
link, tooltip | Sheet5 |
locked | Sheet5 |
merge | Sheet5 |
type=richtext, runs (per-run: bold/italic/color/size/strike/underline/superscript/subscript) | Sheet6 |
Set → Get round-trip
The script ends by reading three cells back with get … --json and printing the canonical keys, proving the values survive the write and normalize on read:
/Sheet1/B11: {'font.bold': True, 'font.italic': True, 'font.color': '#2E75B6', 'font.size': '14pt'}
/Numbers/B6: {'numberformat': '$#,##0.00'}
/Borders/B9: {'border.bottom': 'double'}Note the normalization on get: colors gain a # prefix (#2E75B6) and font sizes become unit-qualified (14pt) — the canonical output forms.
Inspect the Generated File
officecli query cell-formatting.xlsx sheet
officecli get cell-formatting.xlsx "/RichText/A3"#!/usr/bin/env python3
"""
Cell Formatting Showcase — generates cell-formatting.xlsx exercising the full
xlsx `cell` property surface (schemas/help/xlsx/cell.json).
5 sheets, one property group each:
Fonts — font.name/size/bold/italic/color, underline, strike
Fills — fill (hex/named/rgb), alignment.horizontal/vertical/wrapText/readingOrder
Borders — border shorthand, border.all, per-side styles, border.color
Numbers — numberformat codes (thousands, %, currency, date, scientific, accounting)
Data — value/type, formula, link + tooltip, locked, merge
Closes with Set -> Get round-trip readbacks proving the canonical keys come back.
`set` auto-creates the target cell, so no explicit `add` is needed per cell.
Usage:
python3 cell-formatting.py
"""
import subprocess, os, atexit, shlex
FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "cell-formatting.xlsx")
def cli(cmd):
"""Run: officecli <cmd> and echo any output."""
r = subprocess.run(f"officecli {cmd}", shell=True, capture_output=True, text=True)
out = (r.stdout or "").strip()
if out:
for line in out.split("\n"):
if line.strip():
print(f" {line.strip()}")
if r.returncode != 0:
err = (r.stderr or "").strip()
if err and "process cannot access" not in err:
print(f" ERROR: {err}")
def cell(path, **props):
"""officecli set <FILE> <path> --prop k=v ...
shlex.quote each k=v so format codes containing shell metacharacters
(e.g. ``numberformat=$#,##0.00`` — the ``$#`` would otherwise expand) survive.
"""
args = " ".join("--prop " + shlex.quote(f"{k}={v}") for k, v in props.items())
cli(f'set "{FILE}" "{path}" {args}')
if os.path.exists(FILE):
os.remove(FILE)
print("\n==========================================")
print(f"Generating cell formatting showcase: {FILE}")
print("==========================================")
_closed = False
def close_file():
global _closed
if not _closed:
_closed = True
cli(f'close "{FILE}"')
cli(f'create "{FILE}"')
cli(f'open "{FILE}"')
atexit.register(close_file) # guarantees resident close even on mid-script error
# ==========================================================================
# Sheet1: Fonts — font.* family + underline/strike
# ==========================================================================
print("\n--- Sheet1: Fonts ---")
cell("/Sheet1/A1", value="Cell font properties", **{"font.bold": "true", "font.size": "14", "fill": "1F4E79", "font.color": "FFFFFF"})
cell("/Sheet1/A2", value="Property", **{"font.bold": "true", "fill": "D9E1F2"})
cell("/Sheet1/B2", value="Rendered sample", **{"font.bold": "true", "fill": "D9E1F2"})
# (label, sample-text, {props applied to the sample cell})
FONT_ROWS = [
("font.name=Georgia", "Georgia serif", {"font.name": "Georgia"}),
("font.size=18", "18pt text", {"font.size": "18"}),
("font.bold=true", "Bold text", {"font.bold": "true"}),
("font.italic=true", "Italic text", {"font.italic": "true"}),
("font.color=C00000", "Red text", {"font.color": "C00000"}),
("underline=single", "Underlined", {"underline": "single"}),
("underline=double", "Double underline", {"underline": "double"}),
("strike=true", "Struck out", {"strike": "true"}),
("superscript=true", "Superscript cell", {"superscript": "true"}),
("subscript=true", "Subscript cell", {"subscript": "true"}),
("combined", "Bold + italic + blue + 14pt", {"font.bold": "true", "font.italic": "true", "font.color": "2E75B6", "font.size": "14"}),
]
for i, (label, sample, props) in enumerate(FONT_ROWS, start=3):
cell(f"/Sheet1/A{i}", value=label)
cell(f"/Sheet1/B{i}", value=sample, **props)
cell("/Sheet1/col[1]", width="22")
cell("/Sheet1/col[2]", width="32")
# ==========================================================================
# Sheet2: Fills & alignment
# ==========================================================================
print("\n--- Sheet2: Fills & alignment ---")
cli(f'add "{FILE}" / --type sheet --prop name=Fills')
cell("/Fills/A1", value="Fills & alignment", **{"font.bold": "true", "font.size": "14", "fill": "548235", "font.color": "FFFFFF"})
cell("/Fills/A2", value="fill=E63946 (hex)", fill="E63946", **{"font.color": "FFFFFF"})
cell("/Fills/A3", value="fill=gold (named)", fill="gold")
cell("/Fills/A4", value="fill=rgb(46,157,182)", fill="rgb(46,157,182)", **{"font.color": "FFFFFF"})
for i, h in zip((6, 7, 8), ("left", "center", "right")):
cell(f"/Fills/A{i}", value=h, fill="F2F2F2", **{"alignment.horizontal": h})
for i, v in zip((6, 7, 8), ("top", "center", "bottom")):
cell(f"/Fills/C{i}", value={"center": "middle"}.get(v, v), fill="FCE4D6", **{"alignment.vertical": v})
cell(f"/Fills/row[{i}]", height="34")
cell("/Fills/A10", value="This is a long sentence that wraps inside one cell via alignment.wrapText.", fill="E2EFDA", **{"alignment.wrapText": "true"})
cell("/Fills/A12", value="RTL reading order", fill="DDEBF7", **{"alignment.readingOrder": "rtl"})
# textRotation / indent / shrinkToFit — set directly on alignment (canonical keys).
cell("/Fills/A14", value="rotated 45deg", fill="FFF2CC", **{"alignment.textRotation": "45"})
cell("/Fills/row[14]", height="40")
cell("/Fills/A16", value="indented 3", fill="F2F2F2", **{"alignment.indent": "3"})
cell("/Fills/A18", value="ThisLongLabelShrinksToFit", fill="E2EFDA", **{"alignment.shrinkToFit": "true"})
cell("/Fills/col[1]", width="30")
cell("/Fills/col[3]", width="14")
# ==========================================================================
# Sheet3: Borders
# ==========================================================================
print("\n--- Sheet3: Borders ---")
cli(f'add "{FILE}" / --type sheet --prop name=Borders')
cell("/Borders/A1", value="Border styles", **{"font.bold": "true", "font.size": "14", "fill": "7030A0", "font.color": "FFFFFF"})
cell("/Borders/B3", value="border=thin (all)", border="thin")
cell("/Borders/B5", value="border.all=medium", **{"border.all": "medium"})
cell("/Borders/B7", value="border + color", border="thick", **{"border.color": "C00000"})
cell("/Borders/B9", value="double bottom", **{"border.bottom": "double"})
cell("/Borders/B11", value="dashed box", **{"border.top": "dashed", "border.bottom": "dashed", "border.left": "dashed", "border.right": "dashed"})
cell("/Borders/B13", value="mixed sides", **{"border.left": "thick", "border.top": "thin", "border.right": "medium", "border.bottom": "double"})
# Diagonal borders — direction via diagonalUp/Down, color requires a diagonal line.
cell("/Borders/B15", value="diagonal up", **{"border.diagonal": "thin", "border.diagonalUp": "true"})
cell("/Borders/B17", value="diagonal down + color", **{"border.diagonal": "medium", "border.diagonalDown": "true", "border.diagonal.color": "C00000"})
cell("/Borders/col[1]", width="18")
cell("/Borders/col[2]", width="24")
# ==========================================================================
# Sheet4: Number formats
# ==========================================================================
print("\n--- Sheet4: Number formats ---")
cli(f'add "{FILE}" / --type sheet --prop name=Numbers')
cell("/Numbers/A1", value="numberformat codes", **{"font.bold": "true", "font.size": "14", "fill": "C55A11", "font.color": "FFFFFF"})
cell("/Numbers/A2", value="Format code", **{"font.bold": "true", "fill": "FCE4D6"})
cell("/Numbers/B2", value="Result", **{"font.bold": "true", "fill": "FCE4D6"})
# (format code, raw value); A-label is the code itself, B-cell carries the format
NUM_ROWS = [
("#,##0", "1234567"),
("#,##0.00", "1234.5"),
("0.00%", "0.1834"),
("$#,##0.00", "29999.9"),
("yyyy-mm-dd", "45413"),
("0.00E+00", "602214"),
('_(* #,##0.00_);_(* (#,##0.00);_(* "-"??_)', "-4250"),
]
for i, (code, val) in enumerate(NUM_ROWS, start=3):
# label cell: show the (short) code as literal text — type=string keeps
# codes like "0.00E+00" from being parsed as a scientific-notation number.
cell(f"/Numbers/A{i}", value=code.split(";")[0], type="string")
cell(f"/Numbers/B{i}", value=val, numberformat=code)
cell("/Numbers/col[1]", width="28")
cell("/Numbers/col[2]", width="18")
# ==========================================================================
# Sheet5: Data — value/type, formula, link, locked, merge
# ==========================================================================
print("\n--- Sheet5: Data, formulas & links ---")
cli(f'add "{FILE}" / --type sheet --prop name=Data')
cell("/Data/A1", value="Values, formulas, links", **{"font.bold": "true", "font.size": "14", "fill": "2E75B6", "font.color": "FFFFFF"})
cell("/Data/A3", value="Qty"); cell("/Data/B3", value="12")
cell("/Data/A4", value="Price"); cell("/Data/B4", value="4.5", numberformat="$#,##0.00")
cell("/Data/A5", value="Total", **{"font.bold": "true"})
cell("/Data/B5", formula="B3*B4", numberformat="$#,##0.00", **{"font.bold": "true"})
cell("/Data/A7", value="type=string on a numeric value", type="string")
cell("/Data/B7", value="007", type="string")
cell("/Data/A9", value="OfficeCLI on GitHub", link="https://github.com/iOfficeAI/OfficeCLI",
tooltip="Open the repo", underline="single", **{"font.color": "0563C1"})
cell("/Data/A11", value="locked cell (effective when sheet is protected)", locked="true")
cell("/Data/A13", value="Merged title across A13:C13", merge="A13:C13", fill="DDEBF7",
**{"alignment.horizontal": "center", "font.bold": "true"})
# Dynamic-array formula — spills the result across the ref range.
cell("/Data/A15", value="arrayformula = B3*2", **{"font.italic": "true"})
cell("/Data/B15", arrayformula="B3*2")
cell("/Data/col[1]", width="40")
cell("/Data/col[2]", width="16")
# ==========================================================================
# Sheet6: Rich-text — runs (multi-format text within one cell)
# ==========================================================================
# `runs` is an add-time property (requires --type cell + type=richtext).
# Each run is a JSON object with "text" plus any font props (bold, italic,
# color, size, underline). `set` does not support rich-text; use `add`.
print("\n--- Sheet6: Rich-text runs ---")
cli(f'add "{FILE}" / --type sheet --prop name=RichText')
# Label
cell("/RichText/A1", value="runs — rich-text within one cell", **{"font.bold": "true", "font.size": "14", "fill": "5B2C8B", "font.color": "FFFFFF"})
# Each add creates the cell with multi-format text in a single SST entry.
# Shown once with inline --prop syntax so the example is self-documenting.
cli(f'add "{FILE}" /RichText --type cell --prop ref=A3'
f' --prop type=richtext'
f" --prop 'runs=[{{\"text\":\"Bold + Red \",\"bold\":true,\"color\":\"C00000\"}},{{\"text\":\"Italic + Blue\",\"italic\":true,\"color\":\"2E75B6\"}},{{\"text\":\" Normal\"}}]'")
cli(f'add "{FILE}" /RichText --type cell --prop ref=A5'
f' --prop type=richtext'
f" --prop 'runs=[{{\"text\":\"H\",\"bold\":true,\"color\":\"1F4E79\",\"size\":18}},{{\"text\":\"2\",\"superscript\":true,\"size\":10}},{{\"text\":\"O water formula\",\"color\":\"1F4E79\"}}]'")
cli(f'add "{FILE}" /RichText --type cell --prop ref=A7'
f' --prop type=richtext'
f" --prop 'runs=[{{\"text\":\"Strike\",\"strike\":true}},{{\"text\":\" | \"}},{{\"text\":\"underline\",\"underline\":\"single\"}},{{\"text\":\" | \"}},{{\"text\":\"size 14pt\",\"size\":14}}]'")
cell("/RichText/col[1]", width="50")
# flush resident edits to disk before reading back
close_file()
# ==========================================================================
# Set -> Get round-trip: confirm canonical keys read back
# ==========================================================================
print("\n--- Round-trip readback (Set then Get) ---")
for path, keys in [
("/Sheet1/B11", ("font.bold", "font.italic", "font.color", "font.size")),
("/Numbers/B6", ("value", "numberformat")),
("/Borders/B9", ("border.bottom",)),
]:
r = subprocess.run(f'officecli get "{FILE}" "{path}" --json', shell=True, capture_output=True, text=True)
import json
try:
fmt = json.loads(r.stdout)["data"]["results"][0]["format"]
except Exception:
fmt = {}
shown = {k: fmt.get(k) for k in keys if k in fmt}
print(f" {path}: {shown}")
cli(f'validate "{FILE}"')
print(f"\nCreated: {FILE}")
charts
TODO: rewrite script with high-level chart API, add annotated officecli commands.
See charts.sh and charts.xlsx.
#!/bin/bash
# Generate a showcase document with beautiful charts
# Contains 8 chart types: combo chart, 3D bar, scatter+trendline, 3D pie, bubble, stock OHLC, filled radar, multi-ring doughnut
# 4 Sheets: monthly sales, analysis data, stock data, capability assessment
set -e
XLSX="$(dirname "$0")/charts.xlsx"
echo ""
echo "=========================================="
echo "Generating beautiful charts document: $XLSX"
echo "=========================================="
rm -f "$XLSX"
officecli create "$XLSX"
officecli open "$XLSX"
###############################################################################
# Sheet1: Monthly sales data
###############################################################################
echo " -> Populating Sheet1: Monthly sales data"
officecli set "$XLSX" '/Sheet1/A1' --prop value="Month" --prop font.bold=true --prop fill=1F4E79 --prop font.color=FFFFFF --prop font.size=11 --prop alignment.horizontal=center
officecli set "$XLSX" '/Sheet1/B1' --prop value="East Sales" --prop font.bold=true --prop fill=2E75B6 --prop font.color=FFFFFF --prop font.size=11 --prop alignment.horizontal=center
officecli set "$XLSX" '/Sheet1/C1' --prop value="South Sales" --prop font.bold=true --prop fill=9DC3E6 --prop font.color=1F4E79 --prop font.size=11 --prop alignment.horizontal=center
officecli set "$XLSX" '/Sheet1/D1' --prop value="North Sales" --prop font.bold=true --prop fill=BDD7EE --prop font.color=1F4E79 --prop font.size=11 --prop alignment.horizontal=center
officecli set "$XLSX" '/Sheet1/E1' --prop value="Total" --prop font.bold=true --prop fill=C55A11 --prop font.color=FFFFFF --prop font.size=11 --prop alignment.horizontal=center
officecli set "$XLSX" '/Sheet1/F1' --prop value="YoY Growth %" --prop font.bold=true --prop fill=548235 --prop font.color=FFFFFF --prop font.size=11 --prop alignment.horizontal=center
MONTHS=("Jan" "Feb" "Mar" "Apr" "May" "Jun" "Jul" "Aug" "Sep" "Oct" "Nov" "Dec")
EAST=(120 135 148 162 155 178 195 210 188 172 165 198)
SOUTH=(95 108 115 128 142 155 168 175 160 148 135 158)
NORTH=(88 92 105 118 125 138 145 152 140 130 122 142)
TOTAL=(303 335 368 408 422 471 508 537 488 450 422 498)
GROWTH=(5.2 8.1 12.3 15.6 10.2 18.5 22.1 25.3 16.8 11.2 7.5 19.8)
for i in $(seq 0 11); do
row=$((i + 2))
officecli set "$XLSX" "/Sheet1/A${row}" --prop "value=${MONTHS[$i]}" --prop alignment.horizontal=center
officecli set "$XLSX" "/Sheet1/B${row}" --prop "value=${EAST[$i]}" --prop 'numFmt=#,##0' --prop alignment.horizontal=center
officecli set "$XLSX" "/Sheet1/C${row}" --prop "value=${SOUTH[$i]}" --prop 'numFmt=#,##0' --prop alignment.horizontal=center
officecli set "$XLSX" "/Sheet1/D${row}" --prop "value=${NORTH[$i]}" --prop 'numFmt=#,##0' --prop alignment.horizontal=center
officecli set "$XLSX" "/Sheet1/E${row}" --prop "value=${TOTAL[$i]}" --prop 'numFmt=#,##0' --prop font.bold=true --prop alignment.horizontal=center
officecli set "$XLSX" "/Sheet1/F${row}" --prop "value=${GROWTH[$i]}" --prop 'numFmt=0.0"%"' --prop alignment.horizontal=center
done
echo " Done: Sheet1 data"
###############################################################################
# Sheet2: Scatter/bubble chart data
###############################################################################
echo " -> Populating Sheet2: Analysis data"
officecli add "$XLSX" / --type sheet --prop name=Analysis
officecli set "$XLSX" '/Analysis/A1' --prop value="Ad Spend (10K)" --prop font.bold=true --prop fill=7030A0 --prop font.color=FFFFFF --prop alignment.horizontal=center
officecli set "$XLSX" '/Analysis/B1' --prop value="Sales (10K)" --prop font.bold=true --prop fill=7030A0 --prop font.color=FFFFFF --prop alignment.horizontal=center
officecli set "$XLSX" '/Analysis/C1' --prop value="Margin %" --prop font.bold=true --prop fill=7030A0 --prop font.color=FFFFFF --prop alignment.horizontal=center
officecli set "$XLSX" '/Analysis/D1' --prop value="Market Share %" --prop font.bold=true --prop fill=7030A0 --prop font.color=FFFFFF --prop alignment.horizontal=center
AD_SPEND=(10 15 22 28 35 42 50 58 65 72 80 88 95 105 115)
SALES_REV=(45 68 95 120 155 180 220 260 290 335 370 410 445 500 550)
PROFIT=(8.5 10.2 12.1 14.5 16.8 15.2 18.3 20.1 19.5 22.3 21.8 24.5 23.1 26.8 28.2)
MKT_SHARE=(2.1 3.2 4.5 5.8 7.2 8.5 10.1 11.8 12.5 14.2 15.8 17.5 18.2 20.5 22.1)
for i in $(seq 0 14); do
row=$((i + 2))
officecli set "$XLSX" "/Analysis/A${row}" --prop "value=${AD_SPEND[$i]}" --prop alignment.horizontal=center
officecli set "$XLSX" "/Analysis/B${row}" --prop "value=${SALES_REV[$i]}" --prop alignment.horizontal=center
officecli set "$XLSX" "/Analysis/C${row}" --prop "value=${PROFIT[$i]}" --prop alignment.horizontal=center
officecli set "$XLSX" "/Analysis/D${row}" --prop "value=${MKT_SHARE[$i]}" --prop alignment.horizontal=center
done
echo " Done: Sheet2 data"
###############################################################################
# Sheet3: Stock data (with red/green coloring)
###############################################################################
echo " -> Populating Sheet3: Stock data"
officecli add "$XLSX" / --type sheet --prop name=StockData
officecli set "$XLSX" '/StockData/A1' --prop value="Date" --prop font.bold=true --prop fill=C00000 --prop font.color=FFFFFF --prop alignment.horizontal=center
officecli set "$XLSX" '/StockData/B1' --prop value="Open" --prop font.bold=true --prop fill=C00000 --prop font.color=FFFFFF --prop alignment.horizontal=center
officecli set "$XLSX" '/StockData/C1' --prop value="High" --prop font.bold=true --prop fill=C00000 --prop font.color=FFFFFF --prop alignment.horizontal=center
officecli set "$XLSX" '/StockData/D1' --prop value="Low" --prop font.bold=true --prop fill=C00000 --prop font.color=FFFFFF --prop alignment.horizontal=center
officecli set "$XLSX" '/StockData/E1' --prop value="Close" --prop font.bold=true --prop fill=C00000 --prop font.color=FFFFFF --prop alignment.horizontal=center
officecli set "$XLSX" '/StockData/F1' --prop value="Volume (10K)" --prop font.bold=true --prop fill=C00000 --prop font.color=FFFFFF --prop alignment.horizontal=center
DATES=("3/1" "3/2" "3/3" "3/4" "3/5" "3/6" "3/7" "3/8" "3/9" "3/10" "3/11" "3/12" "3/13" "3/14" "3/15" "3/16" "3/17" "3/18" "3/19" "3/20")
OPEN=(52.3 53.1 52.8 54.2 55.1 54.5 56.2 57.8 58.5 57.2 56.8 58.3 59.5 60.2 59.8 61.5 62.3 61.8 63.5 64.2)
HIGH=(53.8 54.2 54.5 55.8 56.3 56.8 58.1 59.2 59.8 58.5 58.2 59.8 61.2 61.5 61.8 63.2 63.8 63.5 65.2 65.8)
LOW=(51.5 52.2 51.8 53.5 54.2 53.8 55.5 56.8 57.2 56.1 55.8 57.5 58.8 59.2 58.5 60.8 61.2 60.5 62.8 63.5)
CLOSE=(53.1 52.8 54.2 55.1 54.5 56.2 57.8 58.5 57.2 56.8 58.3 59.5 60.2 59.8 61.5 62.3 61.8 63.5 64.2 65.1)
VOLUME=(285 312 268 345 298 378 425 468 395 310 352 415 485 442 368 512 548 478 562 598)
for i in $(seq 0 19); do
row=$((i + 2))
open=${OPEN[$i]}
close=${CLOSE[$i]}
if (( $(echo "$close > $open" | bc -l) )); then
COLOR="FF0000"; BG="FFF2F2" # Up: red
elif (( $(echo "$close < $open" | bc -l) )); then
COLOR="008000"; BG="F2FFF2" # Down: green
else
COLOR="666666"; BG="F5F5F5" # Flat: gray
fi
officecli set "$XLSX" "/StockData/A${row}" --prop "value=${DATES[$i]}" --prop alignment.horizontal=center --prop "font.color=${COLOR}" --prop "fill=${BG}"
officecli set "$XLSX" "/StockData/B${row}" --prop "value=${OPEN[$i]}" --prop 'numFmt=0.00' --prop alignment.horizontal=center --prop "font.color=${COLOR}" --prop "fill=${BG}"
officecli set "$XLSX" "/StockData/C${row}" --prop "value=${HIGH[$i]}" --prop 'numFmt=0.00' --prop alignment.horizontal=center --prop "font.color=${COLOR}" --prop "fill=${BG}"
officecli set "$XLSX" "/StockData/D${row}" --prop "value=${LOW[$i]}" --prop 'numFmt=0.00' --prop alignment.horizontal=center --prop "font.color=${COLOR}" --prop "fill=${BG}"
officecli set "$XLSX" "/StockData/E${row}" --prop "value=${CLOSE[$i]}" --prop 'numFmt=0.00' --prop alignment.horizontal=center --prop "font.color=${COLOR}" --prop "fill=${BG}"
officecli set "$XLSX" "/StockData/F${row}" --prop "value=${VOLUME[$i]}" --prop 'numFmt=#,##0' --prop alignment.horizontal=center --prop "font.color=${COLOR}" --prop "fill=${BG}"
done
echo " Done: Sheet3 stock data (with red/green coloring)"
###############################################################################
# Sheet4: Capability radar chart data
###############################################################################
echo " -> Populating Sheet4: Capability assessment"
officecli add "$XLSX" / --type sheet --prop name=Assessment
officecli set "$XLSX" '/Assessment/A1' --prop value="Dimension" --prop font.bold=true --prop fill=002060 --prop font.color=FFFFFF --prop alignment.horizontal=center
officecli set "$XLSX" '/Assessment/B1' --prop value="Product A" --prop font.bold=true --prop fill=0070C0 --prop font.color=FFFFFF --prop alignment.horizontal=center
officecli set "$XLSX" '/Assessment/C1' --prop value="Product B" --prop font.bold=true --prop fill=00B050 --prop font.color=FFFFFF --prop alignment.horizontal=center
officecli set "$XLSX" '/Assessment/D1' --prop value="Product C" --prop font.bold=true --prop fill=FFC000 --prop font.color=000000 --prop alignment.horizontal=center
DIMS=("Performance" "Stability" "Usability" "Security" "Scalability" "Value" "Ecosystem" "Docs")
PA=(92 88 75 95 82 70 85 78)
PB=(78 92 88 80 90 85 72 82)
PC=(85 76 92 72 78 92 88 70)
for i in $(seq 0 7); do
row=$((i + 2))
officecli set "$XLSX" "/Assessment/A${row}" --prop "value=${DIMS[$i]}" --prop alignment.horizontal=center
officecli set "$XLSX" "/Assessment/B${row}" --prop "value=${PA[$i]}" --prop alignment.horizontal=center
officecli set "$XLSX" "/Assessment/C${row}" --prop "value=${PB[$i]}" --prop alignment.horizontal=center
officecli set "$XLSX" "/Assessment/D${row}" --prop "value=${PC[$i]}" --prop alignment.horizontal=center
done
echo " Done: Sheet4 data"
###############################################################################
# Chart 1: Combo chart (bar + line dual axis)
###############################################################################
echo " -> Chart 1: Combo chart (bar + line dual axis)"
CHART1_REL=$(officecli add-part "$XLSX" /Sheet1 --type chart 2>&1 | grep -o 'relId=[^ ]*' | cut -d= -f2)
officecli raw-set "$XLSX" '/Sheet1/chart[1]' --xpath "/c:chartSpace" --action replace --xml '
<c:chartSpace>
<c:chart>
<c:title>
<c:tx><c:rich><a:bodyPr rot="0" /><a:lstStyle />
<a:p><a:pPr><a:defRPr sz="1600" b="1"><a:solidFill><a:srgbClr val="1F4E79" /></a:solidFill><a:latin typeface="Microsoft YaHei" /><a:ea typeface="Microsoft YaHei" /></a:defRPr></a:pPr>
<a:r><a:rPr lang="en-US" sz="1600" b="1"><a:solidFill><a:srgbClr val="1F4E79" /></a:solidFill></a:rPr><a:t>Monthly Sales and YoY Growth Trend</a:t></a:r></a:p>
</c:rich></c:tx>
<c:overlay val="0" />
</c:title>
<c:plotArea>
<c:layout />
<c:barChart>
<c:barDir val="col" /><c:grouping val="clustered" /><c:varyColors val="0" />
<c:ser>
<c:idx val="0" /><c:order val="0" />
<c:tx><c:strRef><c:f>Sheet1!$B$1</c:f></c:strRef></c:tx>
<c:spPr>
<a:gradFill rotWithShape="1"><a:gsLst>
<a:gs pos="0"><a:srgbClr val="1F4E79" /></a:gs>
<a:gs pos="100000"><a:srgbClr val="2E75B6" /></a:gs>
</a:gsLst><a:lin ang="5400000" /></a:gradFill>
<a:ln w="0"><a:noFill /></a:ln>
<a:effectLst><a:outerShdw blurRad="40000" dist="23000" dir="5400000" rotWithShape="0"><a:srgbClr val="000000"><a:alpha val="35000" /></a:srgbClr></a:outerShdw></a:effectLst>
</c:spPr>
<c:cat><c:strRef><c:f>Sheet1!$A$2:$A$13</c:f></c:strRef></c:cat>
<c:val><c:numRef><c:f>Sheet1!$B$2:$B$13</c:f></c:numRef></c:val>
</c:ser>
<c:ser>
<c:idx val="1" /><c:order val="1" />
<c:tx><c:strRef><c:f>Sheet1!$C$1</c:f></c:strRef></c:tx>
<c:spPr>
<a:gradFill rotWithShape="1"><a:gsLst>
<a:gs pos="0"><a:srgbClr val="C55A11" /></a:gs>
<a:gs pos="100000"><a:srgbClr val="ED7D31" /></a:gs>
</a:gsLst><a:lin ang="5400000" /></a:gradFill>
<a:ln w="0"><a:noFill /></a:ln>
<a:effectLst><a:outerShdw blurRad="40000" dist="23000" dir="5400000" rotWithShape="0"><a:srgbClr val="000000"><a:alpha val="35000" /></a:srgbClr></a:outerShdw></a:effectLst>
</c:spPr>
<c:cat><c:strRef><c:f>Sheet1!$A$2:$A$13</c:f></c:strRef></c:cat>
<c:val><c:numRef><c:f>Sheet1!$C$2:$C$13</c:f></c:numRef></c:val>
</c:ser>
<c:ser>
<c:idx val="2" /><c:order val="2" />
<c:tx><c:strRef><c:f>Sheet1!$D$1</c:f></c:strRef></c:tx>
<c:spPr>
<a:gradFill rotWithShape="1"><a:gsLst>
<a:gs pos="0"><a:srgbClr val="548235" /></a:gs>
<a:gs pos="100000"><a:srgbClr val="70AD47" /></a:gs>
</a:gsLst><a:lin ang="5400000" /></a:gradFill>
<a:ln w="0"><a:noFill /></a:ln>
<a:effectLst><a:outerShdw blurRad="40000" dist="23000" dir="5400000" rotWithShape="0"><a:srgbClr val="000000"><a:alpha val="35000" /></a:srgbClr></a:outerShdw></a:effectLst>
</c:spPr>
<c:cat><c:strRef><c:f>Sheet1!$A$2:$A$13</c:f></c:strRef></c:cat>
<c:val><c:numRef><c:f>Sheet1!$D$2:$D$13</c:f></c:numRef></c:val>
</c:ser>
<c:axId val="1" /><c:axId val="2" />
</c:barChart>
<c:lineChart>
<c:grouping val="standard" /><c:varyColors val="0" />
<c:ser>
<c:idx val="3" /><c:order val="3" />
<c:tx><c:strRef><c:f>Sheet1!$F$1</c:f></c:strRef></c:tx>
<c:spPr><a:ln w="38100" cap="rnd"><a:solidFill><a:srgbClr val="FF0000" /></a:solidFill><a:prstDash val="solid" /><a:round /></a:ln></c:spPr>
<c:marker><c:symbol val="circle" /><c:size val="8" />
<c:spPr><a:solidFill><a:srgbClr val="FF0000" /></a:solidFill><a:ln w="19050"><a:solidFill><a:srgbClr val="FFFFFF" /></a:solidFill></a:ln></c:spPr>
</c:marker>
<c:dLbls>
<c:numFmt formatCode="0.0"%"" sourceLinked="0" />
<c:spPr><a:noFill /><a:ln><a:noFill /></a:ln></c:spPr>
<c:txPr><a:bodyPr /><a:lstStyle /><a:p><a:pPr><a:defRPr sz="900" b="1"><a:solidFill><a:srgbClr val="FF0000" /></a:solidFill></a:defRPr></a:pPr><a:endParaRPr lang="en-US" /></a:p></c:txPr>
<c:showLegendKey val="0" /><c:showVal val="1" /><c:showCatName val="0" /><c:showSerName val="0" /><c:showPercent val="0" />
</c:dLbls>
<c:cat><c:strRef><c:f>Sheet1!$A$2:$A$13</c:f></c:strRef></c:cat>
<c:val><c:numRef><c:f>Sheet1!$F$2:$F$13</c:f></c:numRef></c:val>
<c:smooth val="1" />
</c:ser>
<c:marker val="1" />
<c:axId val="1" /><c:axId val="3" />
</c:lineChart>
<c:catAx>
<c:axId val="1" /><c:scaling><c:orientation val="minMax" /></c:scaling><c:delete val="0" /><c:axPos val="b" />
<c:spPr><a:ln w="9525"><a:solidFill><a:srgbClr val="BFBFBF" /></a:solidFill></a:ln></c:spPr>
<c:txPr><a:bodyPr /><a:lstStyle /><a:p><a:pPr><a:defRPr sz="1000"><a:solidFill><a:srgbClr val="404040" /></a:solidFill></a:defRPr></a:pPr><a:endParaRPr lang="en-US" /></a:p></c:txPr>
<c:crossAx val="2" />
</c:catAx>
<c:valAx>
<c:axId val="2" /><c:scaling><c:orientation val="minMax" /></c:scaling><c:delete val="0" /><c:axPos val="l" />
<c:title><c:tx><c:rich><a:bodyPr rot="-5400000" /><a:lstStyle /><a:p><a:pPr><a:defRPr sz="1000"><a:solidFill><a:srgbClr val="404040" /></a:solidFill></a:defRPr></a:pPr><a:r><a:rPr lang="en-US" sz="1000" /><a:t>Sales (10K)</a:t></a:r></a:p></c:rich></c:tx></c:title>
<c:numFmt formatCode="#,##0" sourceLinked="0" />
<c:spPr><a:ln w="9525"><a:solidFill><a:srgbClr val="BFBFBF" /></a:solidFill></a:ln></c:spPr>
<c:crossAx val="1" />
</c:valAx>
<c:valAx>
<c:axId val="3" /><c:scaling><c:orientation val="minMax" /></c:scaling><c:delete val="0" /><c:axPos val="r" />
<c:title><c:tx><c:rich><a:bodyPr rot="5400000" /><a:lstStyle /><a:p><a:pPr><a:defRPr sz="1000"><a:solidFill><a:srgbClr val="FF0000" /></a:solidFill></a:defRPr></a:pPr><a:r><a:rPr lang="en-US" sz="1000" /><a:t>YoY Growth (%)</a:t></a:r></a:p></c:rich></c:tx></c:title>
<c:numFmt formatCode="0.0"%"" sourceLinked="0" />
<c:spPr><a:ln w="9525"><a:solidFill><a:srgbClr val="FF0000"><a:alpha val="50000" /></a:srgbClr></a:solidFill></a:ln></c:spPr>
<c:crossAx val="1" /><c:crosses val="max" />
</c:valAx>
</c:plotArea>
<c:legend><c:legendPos val="b" /><c:overlay val="0" />
<c:txPr><a:bodyPr /><a:lstStyle /><a:p><a:pPr><a:defRPr sz="1000"><a:solidFill><a:srgbClr val="404040" /></a:solidFill></a:defRPr></a:pPr><a:endParaRPr lang="en-US" /></a:p></c:txPr>
</c:legend>
<c:plotVisOnly val="1" />
</c:chart>
</c:chartSpace>'
officecli raw-set "$XLSX" '/Sheet1/drawing' --xpath "//xdr:wsDr" --action append --xml "
<xdr:twoCellAnchor>
<xdr:from><xdr:col>7</xdr:col><xdr:colOff>0</xdr:colOff><xdr:row>0</xdr:row><xdr:rowOff>0</xdr:rowOff></xdr:from>
<xdr:to><xdr:col>18</xdr:col><xdr:colOff>0</xdr:colOff><xdr:row>18</xdr:row><xdr:rowOff>0</xdr:rowOff></xdr:to>
<xdr:graphicFrame macro=\"\">
<xdr:nvGraphicFramePr><xdr:cNvPr id=\"2\" name=\"Chart 1\" /><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=\"http://schemas.openxmlformats.org/drawingml/2006/chart\"><c:chart r:id=\"${CHART1_REL}\" /></a:graphicData></a:graphic>
</xdr:graphicFrame>
<xdr:clientData />
</xdr:twoCellAnchor>"
echo " Done: Chart 1 combo chart"
###############################################################################
# Chart 2: 3D bar chart
###############################################################################
echo " -> Chart 2: 3D bar chart"
CHART2_REL=$(officecli add-part "$XLSX" /Sheet1 --type chart 2>&1 | grep -o 'relId=[^ ]*' | cut -d= -f2)
officecli raw-set "$XLSX" '/Sheet1/chart[2]' --xpath "/c:chartSpace" --action replace --xml '
<c:chartSpace>
<c:chart>
<c:title>
<c:tx><c:rich><a:bodyPr /><a:lstStyle />
<a:p><a:pPr><a:defRPr sz="1600" b="1"><a:solidFill><a:srgbClr val="1F4E79" /></a:solidFill></a:defRPr></a:pPr>
<a:r><a:rPr lang="en-US" sz="1600" b="1" /><a:t>3D Regional Sales Comparison</a:t></a:r></a:p>
</c:rich></c:tx>
<c:overlay val="0" />
</c:title>
<c:view3D>
<c:rotX val="15" /><c:rotY val="20" /><c:depthPercent val="100" /><c:rAngAx val="1" /><c:perspective val="30" />
</c:view3D>
<c:plotArea>
<c:layout />
<c:bar3DChart>
<c:barDir val="col" /><c:grouping val="clustered" /><c:varyColors val="0" />
<c:ser>
<c:idx val="0" /><c:order val="0" />
<c:tx><c:strRef><c:f>Sheet1!$B$1</c:f></c:strRef></c:tx>
<c:spPr>
<a:gradFill><a:gsLst>
<a:gs pos="0"><a:srgbClr val="4472C4" /></a:gs>
<a:gs pos="50000"><a:srgbClr val="5B9BD5" /></a:gs>
<a:gs pos="100000"><a:srgbClr val="9DC3E6" /></a:gs>
</a:gsLst><a:lin ang="5400000" /></a:gradFill>
</c:spPr>
<c:cat><c:strRef><c:f>Sheet1!$A$2:$A$13</c:f></c:strRef></c:cat>
<c:val><c:numRef><c:f>Sheet1!$B$2:$B$13</c:f></c:numRef></c:val>
</c:ser>
<c:ser>
<c:idx val="1" /><c:order val="1" />
<c:tx><c:strRef><c:f>Sheet1!$C$1</c:f></c:strRef></c:tx>
<c:spPr>
<a:gradFill><a:gsLst>
<a:gs pos="0"><a:srgbClr val="ED7D31" /></a:gs>
<a:gs pos="50000"><a:srgbClr val="F4B183" /></a:gs>
<a:gs pos="100000"><a:srgbClr val="F8CBAD" /></a:gs>
</a:gsLst><a:lin ang="5400000" /></a:gradFill>
</c:spPr>
<c:cat><c:strRef><c:f>Sheet1!$A$2:$A$13</c:f></c:strRef></c:cat>
<c:val><c:numRef><c:f>Sheet1!$C$2:$C$13</c:f></c:numRef></c:val>
</c:ser>
<c:ser>
<c:idx val="2" /><c:order val="2" />
<c:tx><c:strRef><c:f>Sheet1!$D$1</c:f></c:strRef></c:tx>
<c:spPr>
<a:gradFill><a:gsLst>
<a:gs pos="0"><a:srgbClr val="70AD47" /></a:gs>
<a:gs pos="50000"><a:srgbClr val="A9D18E" /></a:gs>
<a:gs pos="100000"><a:srgbClr val="C5E0B4" /></a:gs>
</a:gsLst><a:lin ang="5400000" /></a:gradFill>
</c:spPr>
<c:cat><c:strRef><c:f>Sheet1!$A$2:$A$13</c:f></c:strRef></c:cat>
<c:val><c:numRef><c:f>Sheet1!$D$2:$D$13</c:f></c:numRef></c:val>
</c:ser>
<c:shape val="cylinder" />
<c:axId val="10" /><c:axId val="20" /><c:axId val="30" />
</c:bar3DChart>
<c:catAx><c:axId val="10" /><c:scaling><c:orientation val="minMax" /></c:scaling><c:delete val="0" /><c:axPos val="b" /><c:crossAx val="20" /></c:catAx>
<c:valAx><c:axId val="20" /><c:scaling><c:orientation val="minMax" /></c:scaling><c:delete val="0" /><c:axPos val="l" /><c:numFmt formatCode="#,##0" sourceLinked="0" /><c:crossAx val="10" /></c:valAx>
<c:serAx><c:axId val="30" /><c:scaling><c:orientation val="minMax" /></c:scaling><c:delete val="0" /><c:axPos val="b" /><c:crossAx val="20" /></c:serAx>
</c:plotArea>
<c:legend><c:legendPos val="b" /><c:overlay val="0" /></c:legend>
<c:plotVisOnly val="1" />
</c:chart>
</c:chartSpace>'
officecli raw-set "$XLSX" '/Sheet1/drawing' --xpath "//xdr:wsDr" --action append --xml "
<xdr:twoCellAnchor>
<xdr:from><xdr:col>7</xdr:col><xdr:colOff>0</xdr:colOff><xdr:row>19</xdr:row><xdr:rowOff>0</xdr:rowOff></xdr:from>
<xdr:to><xdr:col>18</xdr:col><xdr:colOff>0</xdr:colOff><xdr:row>37</xdr:row><xdr:rowOff>0</xdr:rowOff></xdr:to>
<xdr:graphicFrame macro=\"\">
<xdr:nvGraphicFramePr><xdr:cNvPr id=\"3\" name=\"Chart 2\" /><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=\"http://schemas.openxmlformats.org/drawingml/2006/chart\"><c:chart r:id=\"${CHART2_REL}\" /></a:graphicData></a:graphic>
</xdr:graphicFrame>
<xdr:clientData />
</xdr:twoCellAnchor>"
echo " Done: Chart 2 3D bar chart"
###############################################################################
# Chart 3: Scatter plot + trendline (Sheet2)
###############################################################################
echo " -> Chart 3: Scatter plot + trendline"
CHART3_REL=$(officecli add-part "$XLSX" /Analysis --type chart 2>&1 | grep -o 'relId=[^ ]*' | cut -d= -f2)
officecli raw-set "$XLSX" '/Analysis/chart[1]' --xpath "/c:chartSpace" --action replace --xml '
<c:chartSpace>
<c:chart>
<c:title>
<c:tx><c:rich><a:bodyPr /><a:lstStyle />
<a:p><a:pPr><a:defRPr sz="1600" b="1"><a:solidFill><a:srgbClr val="7030A0" /></a:solidFill></a:defRPr></a:pPr>
<a:r><a:rPr lang="en-US" sz="1600" b="1" /><a:t>Ad Spend vs Sales Correlation</a:t></a:r></a:p>
</c:rich></c:tx>
<c:overlay val="0" />
</c:title>
<c:plotArea>
<c:layout />
<c:scatterChart>
<c:scatterStyle val="lineMarker" />
<c:varyColors val="0" />
<c:ser>
<c:idx val="0" /><c:order val="0" />
<c:tx><c:strRef><c:f>Analysis!$B$1</c:f></c:strRef></c:tx>
<c:spPr><a:ln w="0"><a:noFill /></a:ln></c:spPr>
<c:marker><c:symbol val="circle" /><c:size val="10" />
<c:spPr>
<a:solidFill><a:srgbClr val="7030A0"><a:alpha val="70000" /></a:srgbClr></a:solidFill>
<a:ln w="19050"><a:solidFill><a:srgbClr val="7030A0" /></a:solidFill></a:ln>
<a:effectLst><a:outerShdw blurRad="40000" dist="20000" dir="5400000"><a:srgbClr val="000000"><a:alpha val="30000" /></a:srgbClr></a:outerShdw></a:effectLst>
</c:spPr>
</c:marker>
<c:trendline>
<c:spPr><a:ln w="25400" cap="rnd"><a:solidFill><a:srgbClr val="FF0000" /></a:solidFill><a:prstDash val="dash" /><a:round /></a:ln></c:spPr>
<c:trendlineType val="linear" />
<c:dispRSqr val="1" /><c:dispEq val="1" />
</c:trendline>
<c:xVal><c:numRef><c:f>Analysis!$A$2:$A$16</c:f></c:numRef></c:xVal>
<c:yVal><c:numRef><c:f>Analysis!$B$2:$B$16</c:f></c:numRef></c:yVal>
<c:smooth val="0" />
</c:ser>
<c:axId val="100" /><c:axId val="200" />
</c:scatterChart>
<c:valAx>
<c:axId val="100" /><c:scaling><c:orientation val="minMax" /></c:scaling><c:delete val="0" /><c:axPos val="b" />
<c:title><c:tx><c:rich><a:bodyPr /><a:lstStyle /><a:p><a:pPr><a:defRPr sz="1000" /></a:pPr><a:r><a:rPr lang="en-US" sz="1000" /><a:t>Ad Spend (10K)</a:t></a:r></a:p></c:rich></c:tx></c:title>
<c:numFmt formatCode="#,##0" sourceLinked="0" />
<c:spPr><a:ln w="9525"><a:solidFill><a:srgbClr val="BFBFBF" /></a:solidFill></a:ln></c:spPr>
<c:crossAx val="200" />
</c:valAx>
<c:valAx>
<c:axId val="200" /><c:scaling><c:orientation val="minMax" /></c:scaling><c:delete val="0" /><c:axPos val="l" />
<c:title><c:tx><c:rich><a:bodyPr rot="-5400000" /><a:lstStyle /><a:p><a:pPr><a:defRPr sz="1000" /></a:pPr><a:r><a:rPr lang="en-US" sz="1000" /><a:t>Sales (10K)</a:t></a:r></a:p></c:rich></c:tx></c:title>
<c:numFmt formatCode="#,##0" sourceLinked="0" />
<c:spPr><a:ln w="9525"><a:solidFill><a:srgbClr val="BFBFBF" /></a:solidFill></a:ln></c:spPr>
<c:crossAx val="100" />
</c:valAx>
</c:plotArea>
<c:legend><c:legendPos val="b" /><c:overlay val="0" /></c:legend>
<c:plotVisOnly val="1" />
</c:chart>
</c:chartSpace>'
officecli raw-set "$XLSX" '/Analysis/drawing' --xpath "//xdr:wsDr" --action append --xml "
<xdr:twoCellAnchor>
<xdr:from><xdr:col>5</xdr:col><xdr:colOff>0</xdr:colOff><xdr:row>0</xdr:row><xdr:rowOff>0</xdr:rowOff></xdr:from>
<xdr:to><xdr:col>16</xdr:col><xdr:colOff>0</xdr:colOff><xdr:row>18</xdr:row><xdr:rowOff>0</xdr:rowOff></xdr:to>
<xdr:graphicFrame macro=\"\">
<xdr:nvGraphicFramePr><xdr:cNvPr id=\"2\" name=\"Chart 3\" /><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=\"http://schemas.openxmlformats.org/drawingml/2006/chart\"><c:chart r:id=\"${CHART3_REL}\" /></a:graphicData></a:graphic>
</xdr:graphicFrame>
<xdr:clientData />
</xdr:twoCellAnchor>"
echo " Done: Chart 3 scatter plot"
###############################################################################
# Chart 4: 3D pie chart (exploded)
###############################################################################
echo " -> Chart 4: 3D pie chart (exploded)"
CHART4_REL=$(officecli add-part "$XLSX" /Sheet1 --type chart 2>&1 | grep -o 'relId=[^ ]*' | cut -d= -f2)
officecli raw-set "$XLSX" '/Sheet1/chart[3]' --xpath "/c:chartSpace" --action replace --xml '
<c:chartSpace>
<c:chart>
<c:title>
<c:tx><c:rich><a:bodyPr /><a:lstStyle />
<a:p><a:pPr><a:defRPr sz="1600" b="1"><a:solidFill><a:srgbClr val="1F4E79" /></a:solidFill></a:defRPr></a:pPr>
<a:r><a:rPr lang="en-US" sz="1600" b="1" /><a:t>Annual Regional Sales Share (3D)</a:t></a:r></a:p>
</c:rich></c:tx>
<c:overlay val="0" />
</c:title>
<c:view3D>
<c:rotX val="30" /><c:rotY val="70" /><c:rAngAx val="0" /><c:perspective val="30" />
</c:view3D>
<c:plotArea>
<c:layout />
<c:pie3DChart>
<c:varyColors val="1" />
<c:ser>
<c:idx val="0" /><c:order val="0" />
<c:explosion val="10" />
<c:dPt><c:idx val="0" />
<c:spPr><a:gradFill><a:gsLst><a:gs pos="0"><a:srgbClr val="1F4E79" /></a:gs><a:gs pos="100000"><a:srgbClr val="4472C4" /></a:gs></a:gsLst><a:lin ang="5400000" /></a:gradFill>
<a:effectLst><a:outerShdw blurRad="50800" dist="38100" dir="5400000"><a:srgbClr val="000000"><a:alpha val="40000" /></a:srgbClr></a:outerShdw></a:effectLst></c:spPr>
</c:dPt>
<c:dPt><c:idx val="1" />
<c:spPr><a:gradFill><a:gsLst><a:gs pos="0"><a:srgbClr val="C55A11" /></a:gs><a:gs pos="100000"><a:srgbClr val="ED7D31" /></a:gs></a:gsLst><a:lin ang="5400000" /></a:gradFill>
<a:effectLst><a:outerShdw blurRad="50800" dist="38100" dir="5400000"><a:srgbClr val="000000"><a:alpha val="40000" /></a:srgbClr></a:outerShdw></a:effectLst></c:spPr>
</c:dPt>
<c:dPt><c:idx val="2" />
<c:spPr><a:gradFill><a:gsLst><a:gs pos="0"><a:srgbClr val="548235" /></a:gs><a:gs pos="100000"><a:srgbClr val="70AD47" /></a:gs></a:gsLst><a:lin ang="5400000" /></a:gradFill>
<a:effectLst><a:outerShdw blurRad="50800" dist="38100" dir="5400000"><a:srgbClr val="000000"><a:alpha val="40000" /></a:srgbClr></a:outerShdw></a:effectLst></c:spPr>
</c:dPt>
<c:dLbls>
<c:numFmt formatCode="0.0"%"" sourceLinked="0" />
<c:spPr><a:noFill /><a:ln><a:noFill /></a:ln></c:spPr>
<c:txPr><a:bodyPr /><a:lstStyle /><a:p><a:pPr><a:defRPr sz="1100" b="1"><a:solidFill><a:srgbClr val="FFFFFF" /></a:solidFill></a:defRPr></a:pPr><a:endParaRPr lang="en-US" /></a:p></c:txPr>
<c:showLegendKey val="0" /><c:showVal val="0" /><c:showCatName val="1" /><c:showSerName val="0" /><c:showPercent val="1" />
</c:dLbls>
<c:cat><c:strRef><c:f>Sheet1!$B$1:$D$1</c:f></c:strRef></c:cat>
<c:val><c:numRef><c:f>Sheet1!$B$8:$D$8</c:f></c:numRef></c:val>
</c:ser>
</c:pie3DChart>
</c:plotArea>
<c:legend><c:legendPos val="b" /><c:overlay val="0" /></c:legend>
</c:chart>
</c:chartSpace>'
officecli raw-set "$XLSX" '/Sheet1/drawing' --xpath "//xdr:wsDr" --action append --xml "
<xdr:twoCellAnchor>
<xdr:from><xdr:col>19</xdr:col><xdr:colOff>0</xdr:colOff><xdr:row>0</xdr:row><xdr:rowOff>0</xdr:rowOff></xdr:from>
<xdr:to><xdr:col>28</xdr:col><xdr:colOff>0</xdr:colOff><xdr:row>18</xdr:row><xdr:rowOff>0</xdr:rowOff></xdr:to>
<xdr:graphicFrame macro=\"\">
<xdr:nvGraphicFramePr><xdr:cNvPr id=\"4\" name=\"Chart 4\" /><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=\"http://schemas.openxmlformats.org/drawingml/2006/chart\"><c:chart r:id=\"${CHART4_REL}\" /></a:graphicData></a:graphic>
</xdr:graphicFrame>
<xdr:clientData />
</xdr:twoCellAnchor>"
echo " Done: Chart 4 3D pie chart"
###############################################################################
# Chart 5: Bubble chart (Sheet2)
###############################################################################
echo " -> Chart 5: Bubble chart"
CHART5_REL=$(officecli add-part "$XLSX" /Analysis --type chart 2>&1 | grep -o 'relId=[^ ]*' | cut -d= -f2)
officecli raw-set "$XLSX" '/Analysis/chart[2]' --xpath "/c:chartSpace" --action replace --xml '
<c:chartSpace>
<c:chart>
<c:title>
<c:tx><c:rich><a:bodyPr /><a:lstStyle />
<a:p><a:pPr><a:defRPr sz="1600" b="1"><a:solidFill><a:srgbClr val="7030A0" /></a:solidFill></a:defRPr></a:pPr>
<a:r><a:rPr lang="en-US" sz="1600" b="1" /><a:t>Spend-Revenue-Market Share Bubble</a:t></a:r></a:p>
</c:rich></c:tx>
<c:overlay val="0" />
</c:title>
<c:plotArea>
<c:layout />
<c:bubbleChart>
<c:varyColors val="0" />
<c:ser>
<c:idx val="0" /><c:order val="0" />
<c:tx><c:strRef><c:f>Analysis!$D$1</c:f></c:strRef></c:tx>
<c:spPr>
<a:solidFill><a:srgbClr val="7030A0"><a:alpha val="60000" /></a:srgbClr></a:solidFill>
<a:ln w="19050"><a:solidFill><a:srgbClr val="7030A0" /></a:solidFill></a:ln>
<a:effectLst><a:outerShdw blurRad="40000" dist="23000" dir="5400000"><a:srgbClr val="000000"><a:alpha val="25000" /></a:srgbClr></a:outerShdw></a:effectLst>
</c:spPr>
<c:xVal><c:numRef><c:f>Analysis!$A$2:$A$16</c:f></c:numRef></c:xVal>
<c:yVal><c:numRef><c:f>Analysis!$B$2:$B$16</c:f></c:numRef></c:yVal>
<c:bubbleSize><c:numRef><c:f>Analysis!$D$2:$D$16</c:f></c:numRef></c:bubbleSize>
<c:bubble3D val="1" />
</c:ser>
<c:axId val="300" /><c:axId val="400" />
</c:bubbleChart>
<c:valAx>
<c:axId val="300" /><c:scaling><c:orientation val="minMax" /></c:scaling><c:delete val="0" /><c:axPos val="b" />
<c:title><c:tx><c:rich><a:bodyPr /><a:lstStyle /><a:p><a:pPr><a:defRPr sz="1000" /></a:pPr><a:r><a:rPr lang="en-US" sz="1000" /><a:t>Ad Spend (10K)</a:t></a:r></a:p></c:rich></c:tx></c:title>
<c:numFmt formatCode="#,##0" sourceLinked="0" /><c:crossAx val="400" />
</c:valAx>
<c:valAx>
<c:axId val="400" /><c:scaling><c:orientation val="minMax" /></c:scaling><c:delete val="0" /><c:axPos val="l" />
<c:title><c:tx><c:rich><a:bodyPr rot="-5400000" /><a:lstStyle /><a:p><a:pPr><a:defRPr sz="1000" /></a:pPr><a:r><a:rPr lang="en-US" sz="1000" /><a:t>Sales (10K)</a:t></a:r></a:p></c:rich></c:tx></c:title>
<c:numFmt formatCode="#,##0" sourceLinked="0" /><c:crossAx val="300" />
</c:valAx>
</c:plotArea>
<c:legend><c:legendPos val="b" /><c:overlay val="0" /></c:legend>
<c:plotVisOnly val="1" />
</c:chart>
</c:chartSpace>'
officecli raw-set "$XLSX" '/Analysis/drawing' --xpath "//xdr:wsDr" --action append --xml "
<xdr:twoCellAnchor>
<xdr:from><xdr:col>5</xdr:col><xdr:colOff>0</xdr:colOff><xdr:row>19</xdr:row><xdr:rowOff>0</xdr:rowOff></xdr:from>
<xdr:to><xdr:col>16</xdr:col><xdr:colOff>0</xdr:colOff><xdr:row>37</xdr:row><xdr:rowOff>0</xdr:rowOff></xdr:to>
<xdr:graphicFrame macro=\"\">
<xdr:nvGraphicFramePr><xdr:cNvPr id=\"3\" name=\"Chart 5\" /><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=\"http://schemas.openxmlformats.org/drawingml/2006/chart\"><c:chart r:id=\"${CHART5_REL}\" /></a:graphicData></a:graphic>
</xdr:graphicFrame>
<xdr:clientData />
</xdr:twoCellAnchor>"
echo " Done: Chart 5 bubble chart"
###############################################################################
# Chart 6: Stock OHLC candlestick chart (red up, green down)
###############################################################################
echo " -> Chart 6: Stock OHLC chart"
CHART6_REL=$(officecli add-part "$XLSX" /StockData --type chart 2>&1 | grep -o 'relId=[^ ]*' | cut -d= -f2)
officecli raw-set "$XLSX" '/StockData/chart[1]' --xpath "/c:chartSpace" --action replace --xml '
<c:chartSpace>
<c:chart>
<c:title>
<c:tx><c:rich><a:bodyPr /><a:lstStyle />
<a:p><a:pPr><a:defRPr sz="1600" b="1"><a:solidFill><a:srgbClr val="C00000" /></a:solidFill></a:defRPr></a:pPr>
<a:r><a:rPr lang="en-US" sz="1600" b="1" /><a:t>Stock Candlestick Chart (OHLC)</a:t></a:r></a:p>
</c:rich></c:tx>
<c:overlay val="0" />
</c:title>
<c:plotArea>
<c:layout />
<c:stockChart>
<c:ser>
<c:idx val="0" /><c:order val="0" />
<c:tx><c:strRef><c:f>StockData!$B$1</c:f></c:strRef></c:tx>
<c:spPr><a:ln w="0"><a:noFill /></a:ln></c:spPr>
<c:marker><c:symbol val="none" /></c:marker>
<c:cat><c:strRef><c:f>StockData!$A$2:$A$21</c:f></c:strRef></c:cat>
<c:val><c:numRef><c:f>StockData!$B$2:$B$21</c:f></c:numRef></c:val>
</c:ser>
<c:ser>
<c:idx val="1" /><c:order val="1" />
<c:tx><c:strRef><c:f>StockData!$C$1</c:f></c:strRef></c:tx>
<c:spPr><a:ln w="0"><a:noFill /></a:ln></c:spPr>
<c:marker><c:symbol val="none" /></c:marker>
<c:cat><c:strRef><c:f>StockData!$A$2:$A$21</c:f></c:strRef></c:cat>
<c:val><c:numRef><c:f>StockData!$C$2:$C$21</c:f></c:numRef></c:val>
</c:ser>
<c:ser>
<c:idx val="2" /><c:order val="2" />
<c:tx><c:strRef><c:f>StockData!$D$1</c:f></c:strRef></c:tx>
<c:spPr><a:ln w="0"><a:noFill /></a:ln></c:spPr>
<c:marker><c:symbol val="none" /></c:marker>
<c:cat><c:strRef><c:f>StockData!$A$2:$A$21</c:f></c:strRef></c:cat>
<c:val><c:numRef><c:f>StockData!$D$2:$D$21</c:f></c:numRef></c:val>
</c:ser>
<c:ser>
<c:idx val="3" /><c:order val="3" />
<c:tx><c:strRef><c:f>StockData!$E$1</c:f></c:strRef></c:tx>
<c:spPr><a:ln w="0"><a:noFill /></a:ln></c:spPr>
<c:marker><c:symbol val="none" /></c:marker>
<c:cat><c:strRef><c:f>StockData!$A$2:$A$21</c:f></c:strRef></c:cat>
<c:val><c:numRef><c:f>StockData!$E$2:$E$21</c:f></c:numRef></c:val>
</c:ser>
<c:hiLowLines>
<c:spPr><a:ln w="9525"><a:solidFill><a:srgbClr val="404040" /></a:solidFill></a:ln></c:spPr>
</c:hiLowLines>
<c:upDownBars>
<c:gapWidth val="100" />
<c:upBars><c:spPr><a:solidFill><a:srgbClr val="FF0000" /></a:solidFill><a:ln w="9525"><a:solidFill><a:srgbClr val="C00000" /></a:solidFill></a:ln></c:spPr></c:upBars>
<c:downBars><c:spPr><a:solidFill><a:srgbClr val="00B050" /></a:solidFill><a:ln w="9525"><a:solidFill><a:srgbClr val="006400" /></a:solidFill></a:ln></c:spPr></c:downBars>
</c:upDownBars>
<c:axId val="500" /><c:axId val="600" />
</c:stockChart>
<c:catAx>
<c:axId val="500" /><c:scaling><c:orientation val="minMax" /></c:scaling><c:delete val="0" /><c:axPos val="b" />
<c:txPr><a:bodyPr rot="-5400000" /><a:lstStyle /><a:p><a:pPr><a:defRPr sz="800" /></a:pPr><a:endParaRPr lang="en-US" /></a:p></c:txPr>
<c:crossAx val="600" />
</c:catAx>
<c:valAx>
<c:axId val="600" /><c:scaling><c:orientation val="minMax" /></c:scaling><c:delete val="0" /><c:axPos val="l" />
<c:numFmt formatCode="0.00" sourceLinked="0" />
<c:crossAx val="500" />
</c:valAx>
</c:plotArea>
<c:legend><c:legendPos val="b" /><c:overlay val="0" /></c:legend>
<c:plotVisOnly val="1" />
</c:chart>
</c:chartSpace>'
officecli raw-set "$XLSX" '/StockData/drawing' --xpath "//xdr:wsDr" --action append --xml "
<xdr:twoCellAnchor>
<xdr:from><xdr:col>7</xdr:col><xdr:colOff>0</xdr:colOff><xdr:row>0</xdr:row><xdr:rowOff>0</xdr:rowOff></xdr:from>
<xdr:to><xdr:col>20</xdr:col><xdr:colOff>0</xdr:colOff><xdr:row>22</xdr:row><xdr:rowOff>0</xdr:rowOff></xdr:to>
<xdr:graphicFrame macro=\"\">
<xdr:nvGraphicFramePr><xdr:cNvPr id=\"2\" name=\"Chart 6\" /><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=\"http://schemas.openxmlformats.org/drawingml/2006/chart\"><c:chart r:id=\"${CHART6_REL}\" /></a:graphicData></a:graphic>
</xdr:graphicFrame>
<xdr:clientData />
</xdr:twoCellAnchor>"
echo " Done: Chart 6 stock OHLC chart"
###############################################################################
# Chart 7: Filled radar chart (Sheet4)
###############################################################################
echo " -> Chart 7: Filled radar chart"
CHART7_REL=$(officecli add-part "$XLSX" /Assessment --type chart 2>&1 | grep -o 'relId=[^ ]*' | cut -d= -f2)
officecli raw-set "$XLSX" '/Assessment/chart[1]' --xpath "/c:chartSpace" --action replace --xml '
<c:chartSpace>
<c:chart>
<c:title>
<c:tx><c:rich><a:bodyPr /><a:lstStyle />
<a:p><a:pPr><a:defRPr sz="1600" b="1"><a:solidFill><a:srgbClr val="002060" /></a:solidFill></a:defRPr></a:pPr>
<a:r><a:rPr lang="en-US" sz="1600" b="1" /><a:t>Product Capability Radar Comparison</a:t></a:r></a:p>
</c:rich></c:tx>
<c:overlay val="0" />
</c:title>
<c:plotArea>
<c:layout />
<c:radarChart>
<c:radarStyle val="filled" /><c:varyColors val="0" />
<c:ser>
<c:idx val="0" /><c:order val="0" />
<c:tx><c:strRef><c:f>Assessment!$B$1</c:f></c:strRef></c:tx>
<c:spPr>
<a:solidFill><a:srgbClr val="4472C4"><a:alpha val="40000" /></a:srgbClr></a:solidFill>
<a:ln w="28575"><a:solidFill><a:srgbClr val="4472C4" /></a:solidFill></a:ln>
</c:spPr>
<c:cat><c:strRef><c:f>Assessment!$A$2:$A$9</c:f></c:strRef></c:cat>
<c:val><c:numRef><c:f>Assessment!$B$2:$B$9</c:f></c:numRef></c:val>
</c:ser>
<c:ser>
<c:idx val="1" /><c:order val="1" />
<c:tx><c:strRef><c:f>Assessment!$C$1</c:f></c:strRef></c:tx>
<c:spPr>
<a:solidFill><a:srgbClr val="00B050"><a:alpha val="40000" /></a:srgbClr></a:solidFill>
<a:ln w="28575"><a:solidFill><a:srgbClr val="00B050" /></a:solidFill></a:ln>
</c:spPr>
<c:cat><c:strRef><c:f>Assessment!$A$2:$A$9</c:f></c:strRef></c:cat>
<c:val><c:numRef><c:f>Assessment!$C$2:$C$9</c:f></c:numRef></c:val>
</c:ser>
<c:ser>
<c:idx val="2" /><c:order val="2" />
<c:tx><c:strRef><c:f>Assessment!$D$1</c:f></c:strRef></c:tx>
<c:spPr>
<a:solidFill><a:srgbClr val="FFC000"><a:alpha val="40000" /></a:srgbClr></a:solidFill>
<a:ln w="28575"><a:solidFill><a:srgbClr val="FFC000" /></a:solidFill></a:ln>
</c:spPr>
<c:cat><c:strRef><c:f>Assessment!$A$2:$A$9</c:f></c:strRef></c:cat>
<c:val><c:numRef><c:f>Assessment!$D$2:$D$9</c:f></c:numRef></c:val>
</c:ser>
<c:axId val="700" /><c:axId val="800" />
</c:radarChart>
<c:catAx><c:axId val="700" /><c:scaling><c:orientation val="minMax" /></c:scaling><c:delete val="0" /><c:axPos val="b" /><c:crossAx val="800" /></c:catAx>
<c:valAx><c:axId val="800" /><c:scaling><c:orientation val="minMax" /><c:max val="100" /><c:min val="0" /></c:scaling><c:delete val="0" /><c:axPos val="l" /><c:crossAx val="700" /></c:valAx>
</c:plotArea>
<c:legend><c:legendPos val="b" /><c:overlay val="0" /></c:legend>
</c:chart>
</c:chartSpace>'
officecli raw-set "$XLSX" '/Assessment/drawing' --xpath "//xdr:wsDr" --action append --xml "
<xdr:twoCellAnchor>
<xdr:from><xdr:col>5</xdr:col><xdr:colOff>0</xdr:colOff><xdr:row>0</xdr:row><xdr:rowOff>0</xdr:rowOff></xdr:from>
<xdr:to><xdr:col>16</xdr:col><xdr:colOff>0</xdr:colOff><xdr:row>20</xdr:row><xdr:rowOff>0</xdr:rowOff></xdr:to>
<xdr:graphicFrame macro=\"\">
<xdr:nvGraphicFramePr><xdr:cNvPr id=\"2\" name=\"Chart 7\" /><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=\"http://schemas.openxmlformats.org/drawingml/2006/chart\"><c:chart r:id=\"${CHART7_REL}\" /></a:graphicData></a:graphic>
</xdr:graphicFrame>
<xdr:clientData />
</xdr:twoCellAnchor>"
echo " Done: Chart 7 radar chart"
###############################################################################
# Chart 8: Multi-ring doughnut chart (2 nested series)
###############################################################################
echo " -> Chart 8: Multi-ring doughnut chart"
CHART8_REL=$(officecli add-part "$XLSX" /Sheet1 --type chart 2>&1 | grep -o 'relId=[^ ]*' | cut -d= -f2)
officecli raw-set "$XLSX" '/Sheet1/chart[4]' --xpath "/c:chartSpace" --action replace --xml '
<c:chartSpace>
<c:chart>
<c:title>
<c:tx><c:rich><a:bodyPr /><a:lstStyle />
<a:p><a:pPr><a:defRPr sz="1600" b="1"><a:solidFill><a:srgbClr val="1F4E79" /></a:solidFill></a:defRPr></a:pPr>
<a:r><a:rPr lang="en-US" sz="1600" b="1" /><a:t>Q3 vs Q4 Regional Sales Multi-Ring</a:t></a:r></a:p>
</c:rich></c:tx>
<c:overlay val="0" />
</c:title>
<c:plotArea>
<c:layout />
<c:doughnutChart>
<c:varyColors val="1" />
<c:ser>
<c:idx val="0" /><c:order val="0" />
<c:tx><c:v>Q3</c:v></c:tx>
<c:dPt><c:idx val="0" /><c:spPr><a:solidFill><a:srgbClr val="1F4E79" /></a:solidFill></c:spPr></c:dPt>
<c:dPt><c:idx val="1" /><c:spPr><a:solidFill><a:srgbClr val="C55A11" /></a:solidFill></c:spPr></c:dPt>
<c:dPt><c:idx val="2" /><c:spPr><a:solidFill><a:srgbClr val="548235" /></a:solidFill></c:spPr></c:dPt>
<c:dLbls>
<c:numFmt formatCode="0.0"%"" sourceLinked="0" />
<c:spPr><a:noFill /><a:ln><a:noFill /></a:ln></c:spPr>
<c:txPr><a:bodyPr /><a:lstStyle /><a:p><a:pPr><a:defRPr sz="900" b="1"><a:solidFill><a:srgbClr val="FFFFFF" /></a:solidFill></a:defRPr></a:pPr><a:endParaRPr lang="en-US" /></a:p></c:txPr>
<c:showLegendKey val="0" /><c:showVal val="0" /><c:showCatName val="0" /><c:showSerName val="0" /><c:showPercent val="1" />
</c:dLbls>
<c:cat><c:strRef><c:f>Sheet1!$B$1:$D$1</c:f></c:strRef></c:cat>
<c:val><c:numRef><c:f>Sheet1!$B$9:$D$9</c:f></c:numRef></c:val>
</c:ser>
<c:ser>
<c:idx val="1" /><c:order val="1" />
<c:tx><c:v>Q4</c:v></c:tx>
<c:dPt><c:idx val="0" /><c:spPr><a:solidFill><a:srgbClr val="4472C4" /></a:solidFill></c:spPr></c:dPt>
<c:dPt><c:idx val="1" /><c:spPr><a:solidFill><a:srgbClr val="ED7D31" /></a:solidFill></c:spPr></c:dPt>
<c:dPt><c:idx val="2" /><c:spPr><a:solidFill><a:srgbClr val="70AD47" /></a:solidFill></c:spPr></c:dPt>
<c:dLbls>
<c:numFmt formatCode="0.0"%"" sourceLinked="0" />
<c:spPr><a:noFill /><a:ln><a:noFill /></a:ln></c:spPr>
<c:txPr><a:bodyPr /><a:lstStyle /><a:p><a:pPr><a:defRPr sz="900" b="1"><a:solidFill><a:srgbClr val="FFFFFF" /></a:solidFill></a:defRPr></a:pPr><a:endParaRPr lang="en-US" /></a:p></c:txPr>
<c:showLegendKey val="0" /><c:showVal val="0" /><c:showCatName val="1" /><c:showSerName val="0" /><c:showPercent val="1" />
</c:dLbls>
<c:cat><c:strRef><c:f>Sheet1!$B$1:$D$1</c:f></c:strRef></c:cat>
<c:val><c:numRef><c:f>Sheet1!$B$13:$D$13</c:f></c:numRef></c:val>
</c:ser>
<c:holeSize val="40" />
</c:doughnutChart>
</c:plotArea>
<c:legend><c:legendPos val="b" /><c:overlay val="0" /></c:legend>
</c:chart>
</c:chartSpace>'
officecli raw-set "$XLSX" '/Sheet1/drawing' --xpath "//xdr:wsDr" --action append --xml "
<xdr:twoCellAnchor>
<xdr:from><xdr:col>19</xdr:col><xdr:colOff>0</xdr:colOff><xdr:row>19</xdr:row><xdr:rowOff>0</xdr:rowOff></xdr:from>
<xdr:to><xdr:col>28</xdr:col><xdr:colOff>0</xdr:colOff><xdr:row>37</xdr:row><xdr:rowOff>0</xdr:rowOff></xdr:to>
<xdr:graphicFrame macro=\"\">
<xdr:nvGraphicFramePr><xdr:cNvPr id=\"5\" name=\"Chart 8\" /><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=\"http://schemas.openxmlformats.org/drawingml/2006/chart\"><c:chart r:id=\"${CHART8_REL}\" /></a:graphicData></a:graphic>
</xdr:graphicFrame>
<xdr:clientData />
</xdr:twoCellAnchor>"
echo " Done: Chart 8 multi-ring doughnut chart"
###############################################################################
# Validation
###############################################################################
officecli close "$XLSX"
echo ""
echo "=========================================="
echo "Validating file"
echo "=========================================="
officecli validate "$XLSX"
officecli view "$XLSX" outline
echo ""
ls -lh "$XLSX"
echo ""
echo "All done! 8 chart types generated"
Advanced Charts Showcase
This demo consists of three files that work together:
- charts-advanced.py — Python script that calls
officeclicommands to generate the workbook. Each chart command is shown as a copyable shell command in the comments. - charts-advanced.xlsx — The generated workbook with 3 sheets (12 charts total).
- charts-advanced.md — This file. Maps each sheet to the features it demonstrates.
Regenerate
cd examples/excel
python3 charts-advanced.py
# → charts-advanced.xlsxChart Sheets
Sheet: 1-Scatter & Bubble
Four charts covering scatter plot and bubble chart fundamentals.
# Scatter with circle markers and connecting lines
officecli add data.xlsx /Sheet --type chart \
--prop chartType=scatter \
--prop categories=1,2,3,4,5,6 \
--prop series1="SeriesA:10,25,15,40,30,50" \
--prop series2="SeriesB:5,18,22,35,28,42" \
--prop colors=4472C4,ED7D31 \
--prop marker=circle --prop markerSize=8 \
--prop lineWidth=1.5 --prop legend=bottom
# Scatter with smooth curve and reference line
officecli add data.xlsx /Sheet --type chart \
--prop chartType=scatter \
--prop smooth=true --prop marker=diamond --prop markerSize=7 \
--prop referenceLine=25:FF0000:Target:dash \
--prop axisTitle=Value --prop catTitle=Period
# Scatter with per-series marker styles
officecli add data.xlsx /Sheet --type chart \
--prop chartType=scatter \
--prop series1.marker=square --prop series2.marker=triangle \
--prop series3.marker=star --prop markerSize=9 \
--prop lineWidth=1 --prop gridlines=D9D9D9:0.5:dot
# Bubble chart with scale control
officecli add data.xlsx /Sheet --type chart \
--prop chartType=bubble \
--prop bubbleScale=80 --prop legend=right \
--prop axisTitle=Revenue --prop catTitle=Market SizeFeatures: scatter, bubble, marker (circle, diamond, square, triangle, star), markerSize, series{N}.marker (per-series), smooth, lineWidth, referenceLine, bubbleScale, catTitle, axisTitle, gridlines, legend
Sheet: 2-Combo & Radar
Four charts covering combo (bar+line) and radar (spider) charts.
# Combo chart with comboSplit (bar+line split)
officecli add data.xlsx /Sheet --type chart \
--prop chartType=combo \
--prop comboSplit=2 \
--prop series1="Revenue:120,145,132,168,155,180" \
--prop series2="Expenses:80,92,85,98,90,105" \
--prop series3="Growth:8,12,6,15,10,16" \
--prop legend=bottom --prop axisTitle=Amount --prop catTitle=Month
# Combo with secondary axis
officecli add data.xlsx /Sheet --type chart \
--prop chartType=combo \
--prop comboSplit=1 --prop secondaryAxis=2 \
--prop series1="Volume:1200,1450,1320,1680" \
--prop series2="AvgPrice:45,52,48,58"
# Combo with per-series type control (combotypes)
officecli add data.xlsx /Sheet --type chart \
--prop chartType=combo \
--prop combotypes=column,column,line,area
# Radar chart with radarStyle=marker
officecli add data.xlsx /Sheet --type chart \
--prop chartType=radar \
--prop radarStyle=marker \
--prop categories=Speed,Strength,Stamina,Agility,Accuracy \
--prop series1="AthleteA:80,65,90,75,85" \
--prop series2="AthleteB:70,85,60,90,70"Features: combo, comboSplit (bar/line split point), combotypes (per-series type: column/line/area), secondaryAxis, radar, radarStyle (marker/filled/standard), categories as spoke labels
Sheet: 3-Stock & Radar
Four charts covering stock (OHLC) and additional radar/bubble variants.
# Stock OHLC chart with 4 series (Open/High/Low/Close)
officecli add data.xlsx /Sheet --type chart \
--prop chartType=stock \
--prop categories=Mon,Tue,Wed,Thu,Fri \
--prop series1="Open:145,148,150,147,152" \
--prop series2="High:152,155,157,153,160" \
--prop series3="Low:143,146,148,144,150" \
--prop series4="Close:148,150,147,152,158" \
--prop catTitle=Day --prop axisTitle=Price
# Stock chart — weekly OHLC with gridlines
officecli add data.xlsx /Sheet --type chart \
--prop chartType=stock \
--prop gridlines=E0E0E0:0.75
# Radar — filled style with transparency
officecli add data.xlsx /Sheet --type chart \
--prop chartType=radar \
--prop radarStyle=filled \
--prop transparency=40 --prop legend=bottom
# Bubble with single series and axis titles
officecli add data.xlsx /Sheet --type chart \
--prop chartType=bubble \
--prop bubbleScale=100 --prop legend=none \
--prop axisTitle=Revenue --prop catTitle=Market SizeFeatures: stock (OHLC format: 4 series = Open/High/Low/Close), radarStyle=filled, transparency (fill alpha on radar), bubbleScale=100, legend=none, gridlines styling
Complete Feature Coverage
| Feature | Sheet |
|---|---|
| Chart types: scatter, bubble, combo, radar, stock | 1, 2, 3 |
| Scatter: marker styles, smooth, lineWidth | 1 |
| Bubble: bubbleScale, single/multi-series | 1, 3 |
| Combo: comboSplit, combotypes, secondaryAxis | 2 |
| Radar: radarStyle (marker, filled, standard), transparency | 2, 3 |
| Stock: OHLC (4 series), gridlines | 3 |
| Markers: circle, diamond, square, triangle, star, per-series | 1 |
| Data input: inline series, categories | 1, 2, 3 |
| Axis: catTitle, axisTitle | 1, 2, 3 |
| Legend: position (bottom, right, none) | 1, 2, 3 |
| Reference line: value:color:label:dash | 1 |
| Gridlines: color:width:dash | 1, 3 |
Inspect the Generated File
officecli query charts-advanced.xlsx chart
officecli get charts-advanced.xlsx "/1-Scatter & Bubble/chart[1]"Bubble Charts Showcase
This demo consists of three files that work together:
- charts-bubble.py — Python script that calls
officeclicommands to generate the workbook. Each chart command is shown as a copyable shell command in the comments. - charts-bubble.xlsx — The generated workbook with 4 sheets (4 chart sheets, 14 charts total).
- charts-bubble.md — This file. Maps each sheet to the features it demonstrates.
Regenerate
cd examples/excel
python3 charts-bubble.py
# -> charts-bubble.xlsxChart Sheets
Sheet: 1-Bubble Fundamentals
Four bubble charts covering basic rendering, bubble scale, size representation, and data labels.
# Basic bubble with 2 series (X,Y,Size triplets separated by semicolons)
officecli add data.xlsx /Sheet --type chart \
--prop chartType=bubble \
--prop series1="Enterprise:50,12,80;120,8,45;200,15,60" \
--prop series2="Consumer:30,25,50;80,18,35;150,22,70" \
--prop catTitle=Market Size ($M) --prop axisTitle=Growth Rate (%)
# bubbleScale=100 with center data labels
officecli add data.xlsx /Sheet --type chart \
--prop chartType=bubble \
--prop bubbleScale=100 \
--prop dataLabels=true --prop labelPos=center
# Small bubbles with bubbleScale=50
officecli add data.xlsx /Sheet --type chart \
--prop chartType=bubble \
--prop bubbleScale=50
# Size proportional to diameter (width) instead of area
officecli add data.xlsx /Sheet --type chart \
--prop chartType=bubble \
--prop sizeRepresents=widthFeatures: bubble, X;Y;Size triplet format, catTitle, axisTitle, bubbleScale, dataLabels, labelPos=center, labelFont, sizeRepresents=width
Sheet: 2-Bubble Styling
Four styled bubble charts with title fonts, transparency, grid styling, and shadow effects.
# Title and legend styling
officecli add data.xlsx /Sheet --type chart \
--prop chartType=bubble \
--prop title.font=Georgia --prop title.size=16 \
--prop title.color=1F4E79 --prop title.bold=true \
--prop legend=right --prop legendfont=10:333333:Calibri
# Transparent overlapping bubbles (ARGB with alpha)
officecli add data.xlsx /Sheet --type chart \
--prop chartType=bubble \
--prop colors=804472C4,80ED7D31 \
--prop bubbleScale=120
# Grid and axis line styling
officecli add data.xlsx /Sheet --type chart \
--prop chartType=bubble \
--prop gridlines=D9D9D9:0.5 --prop axisfont=9:666666 \
--prop axisLine=333333-1
# Shadow and fill effects
officecli add data.xlsx /Sheet --type chart \
--prop chartType=bubble \
--prop plotFill=F0F4F8 --prop chartFill=FAFAFA \
--prop series.shadow=000000-4-315-2-30Features: title.font/size/color/bold, legend=right, legendfont, ARGB transparency (80RRGGBB), bubbleScale, gridlines, axisfont, axisLine, plotFill, chartFill, series.shadow
Sheet: 3-Bubble Advanced
Four advanced bubble charts with secondary axis, reference lines, log scale, and trendlines.
# Secondary axis for second series
officecli add data.xlsx /Sheet --type chart \
--prop chartType=bubble \
--prop secondaryAxis=2
# Reference line (growth threshold)
officecli add data.xlsx /Sheet --type chart \
--prop chartType=bubble \
--prop referenceLine=18:Target Growth:C00000
# Logarithmic scale with axis range
officecli add data.xlsx /Sheet --type chart \
--prop chartType=bubble \
--prop axisMin=1 --prop axisMax=50 \
--prop logBase=10
# Borders and trendline
officecli add data.xlsx /Sheet --type chart \
--prop chartType=bubble \
--prop chartArea.border=333333-1.5 \
--prop plotArea.border=999999-0.75 \
--prop trendline=linearFeatures: secondaryAxis, referenceLine, axisMin/Max, logBase, chartArea.border, plotArea.border, trendline=linear
Sheet: 4-Bubble Series Data
Two charts demonstrating bubble-series-specific data properties: negative bubble rendering and linking bubble sizes to worksheet cell ranges.
# shownegbubbles — render bubbles whose size value is negative
officecli add charts-bubble.xlsx "/4-Bubble Series Data" --type chart \
--prop chartType=bubble \
--prop title="shownegbubbles — negative sizes visible" \
--prop series1="Data:60,30,90" \
--prop series2="Neg:40,50,70" \
--prop colors=4472C4,C00000 \
--prop shownegbubbles=true \
--prop bubbleScale=80 \
--prop legend=bottom
# series1.bubbleSize — link bubble sizes to worksheet cells
# First populate size data in cells A1:A3, then reference it:
officecli add charts-bubble.xlsx "/4-Bubble Series Data" --type cell --prop ref=A1 --prop value=10
officecli add charts-bubble.xlsx "/4-Bubble Series Data" --type cell --prop ref=A2 --prop value=25
officecli add charts-bubble.xlsx "/4-Bubble Series Data" --type cell --prop ref=A3 --prop value=40
officecli add charts-bubble.xlsx "/4-Bubble Series Data" --type chart \
--prop chartType=bubble \
--prop title="series1.bubbleSize — range ref" \
--prop series1="Sizes:80,45,60" \
--prop 'series1.bubbleSize=4-Bubble Series Data!$A$1:$A$3' \
--prop colors=70AD47 \
--prop bubbleScale=100 --prop legend=bottomFeatures: shownegbubbles=true (Excel hides negative-size bubbles by default; set true to reflect and display them), series1.bubbleSize=<range> (link bubble sizes to a worksheet cell range so Excel recomputes when source data changes; bubbleSizeRef is emitted on Get alongside the cached literal values)
Complete Feature Coverage
| Feature | Sheet |
|---|---|
bubble chart type | 1, 2, 3, 4 |
catTitle, axisTitle | 1 |
bubbleScale (50/80/100/120) | 1, 2, 3, 4 |
sizeRepresents=width | 1 |
dataLabels, labelPos=center, labelFont | 1 |
title.font/size/color/bold | 2 |
legend, legendfont | 1, 2, 3, 4 |
ARGB transparency (80RRGGBB) | 2 |
gridlines, axisfont, axisLine | 2 |
plotFill, chartFill | 2, 3 |
series.shadow | 2 |
secondaryAxis | 3 |
referenceLine | 3 |
axisMin/Max, logBase | 3 |
chartArea.border, plotArea.border | 3 |
trendline=linear | 3 |
shownegbubbles | 4 |
series1.bubbleSize (range ref) | 4 |
Inspect the Generated File
officecli query charts-bubble.xlsx chart
officecli get charts-bubble.xlsx "/1-Bubble Fundamentals/chart[1]"charts-demo
TODO: rewrite script with high-level chart API, add annotated officecli commands.
See charts-demo.sh and charts-demo.xlsx.