Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,33 @@ jobs:
. .venv/bin/activate
python -m pytest -q

canary:
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: pip

- name: Install
shell: bash
run: |
python -m venv .venv
. .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install -e ".[test]"

- name: Canary deck suite
shell: bash
run: |
. .venv/bin/activate
python -m pytest -q tests/test_canary_decks.py

lint:
runs-on: ubuntu-latest

Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,8 @@ python -m pytest -q
pyright clean_slides/
```

Release gate checklist: [docs/RELEASE-CHECKLIST.md](docs/RELEASE-CHECKLIST.md)

## Why this exists

I spent six years writing slides in consulting. Most AI slide tools focus on imagery and visual polish — which has its place, but isn't what makes a consulting slide useful. A good slide is a unit of structured argumentation that happens to be visual. You figure out the narrative first, decompose it into pages, then fill each page with the component parts of your argument.
Expand Down
73 changes: 28 additions & 45 deletions docs/CHART-CELLS.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Chart Cells — Design Document

> **Status**: Implemented (alpha). Behavior and schema may still evolve.
> **Status**: Implemented and covered by regression/canary tests.

## Summary

Expand All @@ -14,6 +14,16 @@ consistent output. Rows with chart cells get equal height. Columns with chart
cells get equal width. The tool enforces the constraint; the author focuses on
the data.

### Current behavior highlights

- Horizontal chart-cell labels use manual offsets (`dLblPos=ctr` + manual layout)
to preserve visible spacing from bar ends.
- Waterfall chart-cells support connector overlays and formatted value labels.
- Grouped row headers support `sub` text on a second line, rendered non-bold and
in default body color.
- Chart template copy mode (`chart_template_copy`) is supported through the
chart-cell rendering path when bar template options are set.

---

## Motivating example
Expand Down Expand Up @@ -300,55 +310,28 @@ table:

---

## Implementation plan

### Phase 1: Parsing and validation

1. **`spec.py`** — Parse `charts:` top-level key into `ChartDef` dataclass.
Detect `chartname-N` references in cells during `TableSpec.from_dict()`.
Store as `ChartRef(name, index)` in the cell grid.

2. **`placeholder.py`** — Skip chart-ref cells when filling placeholders.

3. **`validate.py`** — Add all checks from the validation table above.
## Implementation notes

### Phase 2: Sizing
The chart-cell pipeline is fully integrated in the standard YAML flow:

4. **`sizing.py`** — When chart refs are present in a column, that column
participates in equal-width sizing. When chart refs span rows, those rows
participate in equal-height sizing.
- Parsing and validation: `clean_slides/spec.py`
- Placeholder handling: `clean_slides/placeholder.py`
- Layout/sizing constraints: `clean_slides/sizing.py`
- Rendering + chart placement: `clean_slides/renderer.py`
- Chart internals and overlays: `clean_slides/chart_engine/*`

5. **`measure.py`** — Chart cells have zero text width (no text to measure).
Min width comes from the label format string at the configured font size.
The behavior is covered by both targeted regressions and canary tests:

### Phase 3: Rendering

6. **`renderer.py`** — After placing all text boxes, iterate chart groups.
For each group of merged chart refs:
- Compute bounding box from the cell positions
- Create a python-pptx chart shape (`slide.shapes.add_chart`)
- Configure: hide axes, set gap width, add data labels, apply fill color
- Position and size to the bounding box

### Phase 4: Integration

7. **`cli.py`** — No changes needed; `pptx generate` and `pptx validate`
pick up chart cells automatically through the existing pipeline.

8. **Tests** — Unit tests for parsing, validation, sizing, and rendering.
Integration test generating a full chart-cell slide and verifying shape
count and positions.
- `tests/test_chart_cells.py`
- `tests/test_chart_cells_golden.py`
- `tests/test_chart_engine_smoke.py`
- `tests/test_canary_decks.py`

---

## Out of scope (for now)

- **Waterfall charts in cells** — waterfall bar positions must align with row
boundaries, requiring coordination between the chart's internal layout and
the table's row heights. Complex; defer to a later phase.

- **Stacked bars in cells** — each cell currently maps to one bar. Stacked
bars (multiple values per cell) would need a different cell reference syntax.
## Current known limits

- **Chart-only slides** — this design is for charts embedded in tables. The
existing `pptx charts` command handles full-slide charts from JSON.
- **No multi-value cell syntax for stacked segments** — each chart-cell ref
still maps to a single point (`chart-name-index`).
- **Chart-cells are table-embedded only** — full-slide charts should use
`pptx charts` JSON specs.
55 changes: 35 additions & 20 deletions docs/CHARTS.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
# Charts (JSON) — alpha
# Charts (JSON)

> **Alpha**: the chart generator works but the JSON schema and CLI flags may change.
The `pptx charts` command generates native PowerPoint charts from JSON specs.

The `pptx charts` command generates bar/stacked/waterfall charts from JSON specs using
python-pptx plus optional overlay labels.
Supported chart families:
- clustered bars/columns
- stacked bars/columns
- waterfall (overlay-driven)

## Usage

Expand Down Expand Up @@ -31,45 +33,58 @@ The chart engine is bundled with `clean-slides`; no external module path is requ
"categories": ["A", "B", "C"],
"series": [
{"name": "BU1", "values": [1, 2, 3], "color": "#4472C4"},
{"name": "BU2", "values": [4, 5, 6], "color": "#ED7D31"}
{"name": "BU2", "values": [4, 5, 6], "color": "accent2"}
],
"show_data_labels": true,
"add_overlay_labels": true
}
```

## Waterfall config
## Bar options

```json
"bar": {
"orientation": "horizontal",
"chart_template": "templates/chart-style.pptx",
"chart_template_copy": true,
"chart_template_slide": 1,
"chart_template_chart_index": 0
}
```

Notes:
- `orientation` can be `horizontal` or `vertical`.
- `chart_template_copy: true` applies an OPC-level chart XML/relationship replacement,
preserving template internals.
- Template paths are resolved from the spec base directory when relative.

## Waterfall options

```json
"waterfall": {
"orientation": "horizontal",
"decrease_categories": ["Costs"],
"total_categories": ["Net"],
"total_series": ["Totals"],
"range_series": ["Range"],
"reuse_start_base": true,
"label_gap": 25600,
"connector_style": "gap",
"connector_value": "totals",
"connector_style": "gap", // "gap" | "step"
"connector_dash_style": "long_dash", // "solid" | "long_dash" | "dot"
"connector_value": "totals", // "totals" | "tops"
"connector_overlap": 6000,
"connector_inset": 10000,
"total_override": false
}
```

Notes:
- `range_series` marks series that render visually but do **not** affect running totals.
- Set `connector_value: "tops"` for legacy connector anchoring.
- `total_override: true` restores legacy behavior where any series value in a total category overrides
computed totals.

## Horizontal charts

```json
"bar": { "orientation": "horizontal" }
"waterfall": { "orientation": "horizontal" }
```
- `range_series` renders visually but does **not** affect running totals.
- `connector_value: "tops"` restores legacy connector anchoring.
- `total_override: true` restores legacy behavior where explicit values in total
categories override computed totals.

## Multichart decks
## Multi-chart decks

Use a top-level `charts` list to generate multiple charts in one deck:

Expand Down
11 changes: 9 additions & 2 deletions docs/INPUT-SCHEMA.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ table:

col_headers: # Optional, used if has_col_header=true (body cols only)
- "Header 1"
- "Header 2"
- { text: "Header 2", sub: "(units)" } # optional second line, non-bold
- "Header 3"
- "Header 4"

Expand Down Expand Up @@ -127,6 +127,7 @@ Paragraph object fields:

```yaml
- text: "Paragraph text"
sub: "(optional second line)" # optional subtitle/unit line in default body color
lvl: 0 # 0 = no bullet, 1 = bullet, 2 = nested
size: 14 # font size in pt
color: accent1 # theme name (tx1, accent1, dk2, ...) or hex (#RRGGBB / RRGGBB)
Expand All @@ -136,6 +137,12 @@ Paragraph object fields:
underline: false
```

`sub` behavior:
- rendered on a new line (line break inside the same paragraph block)
- keeps `lvl`/`font`/`size`
- forced non-bold
- uses default body color (`tx1`)

Example with bullets and overrides:

```yaml
Expand Down Expand Up @@ -238,7 +245,7 @@ Each entry uses the same format as cell content: plain strings, paragraph object

## Row Groups (Superheaders)

Use `row_groups` instead of `rows` + `row_headers` to create category-grouped tables with bold superheader rows spanning the full width. Each group has a `header` label and a list of `rows` beneath it.
Use `row_groups` instead of `rows` + `row_headers` to create category-grouped tables with bold superheader rows spanning the full width. Each group has a `header` label and a list of `rows` beneath it. `header` can be either a string or an object like `{ text: "Group", sub: "(units)" }`.

When using `row_groups`, omit `rows` and `row_headers` — the row count and row-header column are derived from the groups. You still need `cols` (total columns including the superheader column).

Expand Down
57 changes: 57 additions & 0 deletions docs/RELEASE-CHECKLIST.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Release Checklist

Use this checklist before cutting a release or tagging a stable snapshot.

## 1) Local quality gate

Run from repo root:

```bash
.venv/bin/ruff check clean_slides tests
.venv/bin/pyright
.venv/bin/pytest -q
.venv/bin/pre-commit run --all-files
```

Expected:
- no lint/type errors
- all tests green
- no formatting drift

## 2) Canary behavior gate

Run the dedicated canary suite:

```bash
.venv/bin/pytest -q tests/test_canary_decks.py
```

This verifies representative end-to-end behavior:
- mixed YAML deck generation
- waterfall chart-cell generation with connectors
- multi-chart JSON deck generation
- chart template copy roundtrip (`chart_template_copy`)

## 3) CI gate

Ensure GitHub Actions `ci` is green:
- `test (3.9)`
- `test (3.12)`
- `canary`
- `lint`

## 4) Manual openability check

Generate at least one deck from YAML and one from JSON charts, then open in PowerPoint.

Confirm:
- no repair dialog
- expected chart counts and labels
- expected connector overlays on waterfall canaries

## 5) Docs sanity

Verify docs are consistent with shipped behavior:
- `docs/INPUT-SCHEMA.md`
- `docs/CHART-CELLS.md`
- `docs/CHARTS.md`