Files
wehub-resource-sync 8cb1f9f479
Publish SDK (PyPI) / publish (push) Has been cancelled
Publish SDK (npm) / publish (@aionui/officecli-sdk) (push) Has been cancelled
SDK smoke / smoke (windows-latest) (push) Has been cancelled
Publish SDK (npm) / publish (@officecli/officecli-sdk) (push) Has been cancelled
Publish SDK (npm) / publish (@officecli/sdk) (push) Has been cancelled
Publish SDK (npm) / publish (officecli-sdk) (push) Has been cancelled
SDK smoke / smoke (macos-latest) (push) Has been cancelled
SDK smoke / smoke (ubuntu-latest) (push) Has been cancelled
Skill parity / diff (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:09:29 +08:00

306 lines
17 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Advanced Charts Showcase — properties not covered by the per-type decks.
Generates: charts-advanced.pptx
Coverage of the long tail of chart properties (cross-handler / niche / axis-level):
Slide 1 RTL & anchor direction=rtl, anchor named-token, anchor cm-form
Slide 2 Axis-level shortcuts axisvisible / valaxisvisible / catAxisVisible,
axisorientation, axisposition,
cataxisline / valaxisline
Slide 3 Crossings crossBetween (between/midCat), crosses (autoZero/max/min), crossesAt
Slide 4 Categories axis labeloffset, ticklabelskip
Slide 5 Marker size & fills markersize (standalone), areafill, chartFill, plotvisonly
Slide 6 Built-in style + blanks style=1..48, dispBlanksAs (gap / zero / span)
Slide 7 chart-axis Set dispUnits, logBase, minorUnit, visible, labelRotation (per-axis)
Slide 8 chart-series mutation values=, categories= (per-series range), + get-readback round-trip
SDK twin of charts-advanced.sh (officecli CLI). Both produce an equivalent
charts-advanced.pptx. This one drives the **officecli Python SDK**
(`pip install officecli-sdk`): one resident is started and every add/set is
shipped over the named pipe. The bulk of the deck goes in a single
`doc.batch(...)` round-trip (items applied in order, so an Add followed by a
Set on the same chart works); the two get-readback round-trips that feed text
back onto slide 8 use `doc.send(...)` so their JSON can be captured mid-stream.
Each item is the same `{"command","parent","type","props"}` dict you'd put in
an `officecli batch` list. batch is run with force=True so a prop the running
binary doesn't yet support is skipped (forward-compat) rather than aborting the
deck — per-item failures are surfaced afterwards so silent gaps stay visible.
Usage:
pip install officecli-sdk # plus the `officecli` binary on PATH
python3 charts-advanced.py
"""
import os
import sys
import json
# --- locate the SDK: prefer an installed `officecli-sdk`, else the in-repo copy
try:
import officecli # pip install officecli-sdk
except ImportError:
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)),
"..", "..", "..", "sdk", "python"))
import officecli
FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "charts-advanced.pptx")
# Four quadrant boxes (named exactly as in the .sh twin).
TL = {"x": "0.3in", "y": "1.05in", "width": "6.1in", "height": "3in"}
TR = {"x": "6.95in", "y": "1.05in", "width": "6.1in", "height": "3in"}
BL = {"x": "0.3in", "y": "4.25in", "width": "6.1in", "height": "3in"}
BR = {"x": "6.95in", "y": "4.25in", "width": "6.1in", "height": "3in"}
CATS = "Q1,Q2,Q3,Q4"
D = "A:60,90,140,180"
D2 = "A:60,90,140,180;B:50,75,110,150"
_slide = 0
def new_slide(t):
"""Add a slide + its title shape; returns the items list."""
global _slide
_slide += 1
return [
{"command": "add", "parent": "/", "type": "slide", "props": {}},
{"command": "add", "parent": f"/slide[{_slide}]", "type": "shape",
"props": {"text": t, "size": 24, "bold": "true", "autoFit": "normal",
"x": "0.5in", "y": "0.3in", "width": "12.3in", "height": "0.6in"}},
]
def ch(box, p):
"""One `add chart` item in the current slide, box + chart props merged."""
return {"command": "add", "parent": f"/slide[{_slide}]", "type": "chart",
"props": {**box, **p}}
def note(x, y, text):
"""One small italic caption shape."""
return {"command": "add", "parent": f"/slide[{_slide}]", "type": "shape",
"props": {"text": text, "size": 10, "italic": "true", "color": "666666",
"x": x, "y": y, "width": "6in", "height": "0.4in"}}
def cset(path, props):
"""One `set` item."""
return {"command": "set", "path": path, "props": props}
def _fmt_from(envelope):
"""Pull a node's `format` dict out of a get envelope, tolerant of shape."""
obj = envelope
if isinstance(obj, dict) and "data" in obj:
obj = obj["data"]
if isinstance(obj, dict) and "results" in obj and obj["results"]:
obj = obj["results"][0]
if isinstance(obj, dict) and "format" in obj:
return obj["format"]
return obj
print(f"Building {FILE} ...")
with officecli.create(FILE, "--force") as doc:
items = []
# -----------------------------------------------------------------------
# Slide 1 — RTL + anchor variants
# -----------------------------------------------------------------------
items += new_slide("RTL + anchor — direction=rtl, named-token anchor, cm-form anchor")
items += [
ch(TL, {"chartType": "column", "title": "default (LTR)", "legend": "bottom",
"categories": CATS, "data": D2}),
# RTL must be Set after Add (direction is set-only)
ch(TR, {"chartType": "column", "title": "direction=rtl (Set after Add)", "legend": "bottom",
"categories": "Q1,Q2,Q3,Q4", "data": D2}),
cset(f"/slide[{_slide}]/chart[2]", {"direction": "rtl"}),
# Anchor cm-form: x,y,w,h
ch({"anchor": "0.3cm,11cm,15.5cm,7cm"},
{"chartType": "column", "title": "anchor=0.3cm,11cm,15.5cm,7cm", "legend": "bottom",
"categories": CATS, "data": D}),
]
# -----------------------------------------------------------------------
# Slide 2 — axis-level shortcuts
# -----------------------------------------------------------------------
items += new_slide("Axis shortcuts — axisvisible / valaxisvisible / catAxisVisible, orientation, position, lines")
items += [
ch(TL, {"chartType": "column", "title": "axisvisible=false (both axes hidden)",
"legend": "none", "axisvisible": "false", "categories": CATS, "data": D}),
ch(TR, {"chartType": "column", "title": "valaxisvisible=false (Y hidden, X shown)",
"legend": "none", "valaxisvisible": "false", "categories": CATS, "data": D}),
ch(BL, {"chartType": "column", "title": "catAxisVisible=false (X hidden)",
"legend": "none", "catAxisVisible": "false", "categories": CATS, "data": D}),
ch(BR, {"chartType": "column", "title": "axisorientation=true (reversed) + axisposition=top",
"legend": "none", "axisorientation": "true", "axisposition": "top",
"cataxisline": "333333:1", "valaxisline": "333333:1",
"categories": CATS, "data": D}),
]
# -----------------------------------------------------------------------
# Slide 3 — Crossings
# -----------------------------------------------------------------------
items += new_slide("Crossings — crossBetween / crosses / crossesAt")
items += [
ch(TL, {"chartType": "column", "title": "crossBetween=between (default)",
"legend": "none", "crossBetween": "between", "categories": CATS, "data": D}),
ch(TR, {"chartType": "column", "title": "crossBetween=midCat", "legend": "none",
"crossBetween": "midCat", "categories": CATS, "data": D}),
ch(BL, {"chartType": "column", "title": "crosses=max (Y crosses at top)", "legend": "none",
"crosses": "max", "categories": CATS, "data": D}),
# crossesAt is the overriding form of crosses in CT_ValAx (they are a
# mutually-exclusive schema choice). crosses=autoZero is the OOXML
# default that crossesAt supersedes, so we set crossesAt alone — the
# axis crosses the category axis at value 100.
ch(BR, {"chartType": "column", "title": "crossesAt=100 + crosses=autoZero",
"legend": "none", "crossesAt": "100",
"categories": CATS, "data": "A:60,-30,140,180"}),
]
# -----------------------------------------------------------------------
# Slide 4 — Category axis layout
# -----------------------------------------------------------------------
items += new_slide("Category axis — labeloffset, ticklabelskip")
items += [
ch(TL, {"chartType": "column", "title": "labeloffset=100 (default)",
"labeloffset": "100", "legend": "none",
"categories": "January,February,March,April,May,June",
"data": "A:60,90,140,180,160,210"}),
ch(TR, {"chartType": "column", "title": "labeloffset=300 (push labels down)",
"labeloffset": "300", "legend": "none",
"categories": "January,February,March,April,May,June",
"data": "A:60,90,140,180,160,210"}),
ch(BL, {"chartType": "column", "title": "ticklabelskip=2 (every other label)",
"ticklabelskip": "2", "legend": "none",
"categories": "Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec",
"data": "A:60,90,140,180,160,210,200,190,170,150,130,170"}),
ch(BR, {"chartType": "column", "title": "ticklabelskip=3", "ticklabelskip": "3", "legend": "none",
"categories": "Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec",
"data": "A:60,90,140,180,160,210,200,190,170,150,130,170"}),
]
# -----------------------------------------------------------------------
# Slide 5 — Marker size, area/chart fills, plotvisonly
# -----------------------------------------------------------------------
items += new_slide("Marker size & fills — markersize (standalone), areafill, chartFill, plotvisonly")
items += [
ch(TL, {"chartType": "line", "title": "markersize=12 (standalone key)",
"showMarker": "true", "markersize": "12", "legend": "none",
"categories": CATS, "data": D}),
ch(TR, {"chartType": "column", "title": "areafill (applies to every series shape)",
"areafill": "4472C4-A5C8FF:90", "legend": "none", "categories": CATS, "data": D2}),
ch(BL, {"chartType": "column", "title": "chartFill=#FFF8E7 (chart-level fill)",
"chartFill": "#FFF8E7", "legend": "none", "categories": CATS, "data": D}),
ch(BR, {"chartType": "column", "title": "plotvisonly=true (skip hidden rows when bound to a sheet)",
"plotvisonly": "true", "legend": "none", "categories": CATS, "data": D}),
]
# -----------------------------------------------------------------------
# Slide 6 — Built-in style id + dispBlanksAs
# -----------------------------------------------------------------------
items += new_slide("Built-in style & blank handling — style=1..48, dispBlanksAs, dataRange")
items += [
ch(TL, {"chartType": "column", "style": "2", "title": "style=2", "legend": "bottom",
"categories": CATS, "data": D2}),
ch(TR, {"chartType": "column", "style": "42", "title": "style=42", "legend": "bottom",
"categories": CATS, "data": D2}),
# dispBlanksAs is Set/Get only — Add first, then Set.
ch(BL, {"chartType": "line", "title": "dispBlanksAs=gap (Set after Add)", "showMarker": "true",
"legend": "bottom", "categories": CATS, "data": "A:60,90,140,180"}),
cset(f"/slide[{_slide}]/chart[3]", {"dispBlanksAs": "gap"}),
# dataRange is Add-time alternative to data= for sheet-backed sources;
# in a standalone pptx this is largely symbolic — we still demonstrate the syntax,
# then fall back to inline data so the chart renders.
ch(BR, {"chartType": "column", "title": "dataRange syntax demo (fallback inline)",
"dataRange": "Sheet1!A1:D5", "legend": "bottom", "catTitle": "Quarter",
"categories": CATS, "data": D2}),
]
# -----------------------------------------------------------------------
# Slide 7 — chart-axis Set (per-axis post-Add)
# -----------------------------------------------------------------------
items += new_slide("chart-axis Set — dispUnits, logBase, minorUnit, visible, labelRotation per-axis")
items += [
ch(TL, {"chartType": "column", "title": "after: dispUnits=thousands (Set on value axis)",
"legend": "none", "categories": CATS, "data": "Rev:120000,135000,148000,162000"}),
cset(f"/slide[{_slide}]/chart[1]/axis[@role=value]",
{"dispUnits": "thousands", "format": "#,##0", "minorUnit": "10000",
"labelRotation": "0", "visible": "true"}),
ch(TR, {"chartType": "line", "title": "after: logBase=10 (Set on value axis)",
"legend": "none", "categories": CATS, "data": "A:5,50,500,5000"}),
cset(f"/slide[{_slide}]/chart[2]/axis[@role=value]",
{"logBase": "10", "min": "1", "max": "10000", "majorGridlines": "true"}),
ch(BL, {"chartType": "column", "title": "after: visible=false on value axis",
"legend": "none", "categories": CATS, "data": D}),
cset(f"/slide[{_slide}]/chart[3]/axis[@role=value]", {"visible": "false"}),
ch(BR, {"chartType": "column", "title": "after: labelRotation=-45 on category axis",
"legend": "none", "categories": "January,February,March,April", "data": D}),
cset(f"/slide[{_slide}]/chart[4]/axis[@role=category]",
{"labelRotation": "-45", "title": "Month", "visible": "true"}),
]
# -----------------------------------------------------------------------
# Slide 8 — chart-series values=/categories= Set + Get readback round-trip
# -----------------------------------------------------------------------
items += new_slide("chart-series mutation — values=, categories= + get-readback round-trip")
s8 = _slide
items += [
ch(TL, {"chartType": "column", "title": "before: A=60,90,140,180", "legend": "bottom",
"categories": CATS, "data": D}),
# Mutate the values after add
cset(f"/slide[{s8}]/chart[1]/series[1]", {"values": "200,150,100,80"}),
note("0.3in", "4in", "After Set values=200,150,100,80 the series flips downward."),
ch(TR, {"chartType": "column", "title": "per-series categories= (range)", "legend": "bottom",
"categories": CATS, "data": D}),
# Per-series category override is range-only — note that it requires sheet backing
# so this is a demonstration of the syntax only; effective result depends on workbook.
]
# Ship the bulk of the deck in one round-trip (items applied in order, so the
# Add-then-Set pairs above resolve correctly). force=True → a prop the binary
# doesn't support is skipped rather than aborting the whole batch.
result = doc.batch(items, force=True)
# Forward-compat: surface any per-item failures so silent prop gaps stay visible.
fails = []
if isinstance(result, dict):
for r in (result.get("data", result).get("results", []) or []):
if isinstance(r, dict) and r.get("success") is False:
fails.append(r.get("error") or r.get("message") or str(r))
if fails:
print(f" ⚠ {len(fails)} batch item(s) reported failure (forward-compat skip):",
file=sys.stderr)
for f in fails[:12]:
print(f" ⚠ {str(f)[:160]}", file=sys.stderr)
print(f" added {len(items)} chart/shape/set operations across {_slide} slides")
# ---- chart-series get-readback round-trip (slide 8, chart 1, series 1) ----
# Change one series, then read it back and stamp the JSON onto the slide.
doc.send(cset(f"/slide[{s8}]/chart[1]/series[1]",
{"name": "Readback Demo", "color": "C00000"}))
doc.send({"command": "get", "path": f"/slide[{s8}]/chart[1]/series[1]"}) # readback round-trip
doc.send({"command": "add", "parent": f"/slide[{s8}]", "type": "shape",
"props": {"text": "chart-series get --json: readback fields alpha/outlineColor/scatterStyle/...",
"size": 9, "color": "222222", "x": "0.3in", "y": "4.25in",
"width": "6.1in", "height": "3in"}})
# ---- chart-axis get-readback — surfaces axisFont/axisMax/axisMin/axisNumFmt/
# axisOrientation/axisTitle/labelOffset/tickLabelSkip read-only fields.
doc.send(cset(f"/slide[{s8}]/chart[1]/axis[@role=value]",
{"title": "Readback Y", "format": "$#,##0", "min": "0", "max": "300", "majorUnit": "75"}))
doc.send({"command": "get", "path": f"/slide[{s8}]/chart[1]/axis[@role=value]"}) # readback round-trip
doc.send({"command": "add", "parent": f"/slide[{s8}]", "type": "shape",
"props": {"text": "chart-axis get --json: readback axisFont/axisMax/axisMin/axisNumFmt/axisOrientation/axisTitle/labelOffset/tickLabelSkip",
"size": 9, "color": "222222", "x": "6.95in", "y": "4.25in",
"width": "6.1in", "height": "3in"}})
doc.send({"command": "save"})
# context exit closes the resident, flushing the deck to disk.
print(f"Generated: {FILE} ({_slide} slides)")