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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ env/
ENV/
.venv

# Local planning notes (not shipped with the repo)
docs/plans/

# IDE / local config
.vscode/
.idea/
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ shell:
$(DOCKER_RUN) -it -v $(OUTPUT_DIR):/app/output -w /app --entrypoint /bin/bash $(IMAGE)

test:
$(DOCKER_RUN) -v $(PWD):/app -w /app --entrypoint /bin/bash $(IMAGE) -c "pip install pytest -q && pytest -q tests/smoke/test_overhaul_phase1_smoke.py tests/smoke/test_layout_strategy_smoke.py tests/smoke/test_create_cli_phase3_smoke.py tests/smoke/test_create_story_regression_phase7_smoke.py tests/smoke/test_export_phase4_smoke.py tests/smoke/test_cli_migration_phase5_smoke.py tests/smoke/test_export_presets_phase6_smoke.py"
$(DOCKER_RUN) -v $(PWD):/app -w /app --entrypoint /bin/bash $(IMAGE) -c "pip install pytest -q && pytest -q tests/smoke/test_overhaul_phase1_smoke.py tests/smoke/test_layout_strategy_smoke.py tests/smoke/test_create_cli_phase3_smoke.py tests/smoke/test_create_story_regression_phase7_smoke.py tests/smoke/test_story_tile_pretext_smoke.py tests/smoke/test_export_phase4_smoke.py tests/smoke/test_cli_migration_phase5_smoke.py tests/smoke/test_export_presets_phase6_smoke.py"

mcp:
@mkdir -p $(OUTPUT_DIR)
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,10 @@ Full docs map: [`docs/README.md`](docs/README.md)
- Contribution guide: [`docs/CONTRIBUTING.md`](docs/CONTRIBUTING.md)
- Historical plans and notes: [`docs/archive/`](docs/archive/)

## Acknowledgments

- [@chenglou/pretext](https://github.com/chenglou/pretext) — pixel-perfect text measurement and layout by [Cheng Lou](https://github.com/chenglou), used for optional SVG text rendering (`--text-render pretext`)

## License

MIT License.
10 changes: 10 additions & 0 deletions docs/SVG_COMPOSITION.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,16 @@ generator = ModernGraphicsGenerator("My Diagram")
html = generator.generate_diagram("my_diagram", title="Items", items=["A", "B", "C"])
```

## Pretext, wireframes, and SVG `foreignObject`

When `ModernGraphicsGenerator(..., use_pretext=True)` or CLI `--text-render pretext`, the HTML page includes a bootstrap script that replaces `.pretext-slot` placeholders with measured SVG text (`modern_graphics/pretext_utils.py`, `modern_graphics/pretext_renderer.py`).

- **Open hero + `freeform_canvas`:** Pretext runs only where `pretext_slot` is emitted (headline, subhead, insight callout, etc.). Wireframe or diagram SVG passed as `freeform_canvas` is separate markup; Pretext does not process it unless you add your own `.pretext-slot` nodes inside that snippet. Typography and diagram are siblings in the hero body, not nested SVG text.
- **Story slide mini-tile:** Headline and subline can use Pretext inside SVG `<foreignObject>`. Those regions use fixed widths and reserved heights so multi-line SVG output is not clipped; if you change copy or font sizes drastically, re-check layout in the mini-tile generator (`modern_graphics/diagrams/story_slide.py`, `_build_static_mini_tile`).
- **Hero clipping:** Open hero CSS uses `overflow: hidden` on `.hero`. Unusually tall header stacks or canvases can clip; adjust padding or hero height if needed.

Export pipelines wait on `window.__pretextReady` when any `.pretext-slot` is present (`modern_graphics/export.py`).

## Deep Dive

For wireframe scene schema details, see [Wireframe Scene Spec](WIREFRAME_SCENE_SPEC.md).
Expand Down
108 changes: 108 additions & 0 deletions examples/pretext_mini_tile_refinement_demo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
#!/usr/bin/env python3
"""Generate side-by-side HTML: legacy vs refined story mini-tile Pretext foreignObject bands.

Open the output file in a browser (network needed for Pretext CDN and fonts).

Run from repo root:

python3 examples/pretext_mini_tile_refinement_demo.py

Or with PYTHONPATH if needed:

PYTHONPATH=. python3 examples/pretext_mini_tile_refinement_demo.py
"""

import sys
from pathlib import Path

REPO_ROOT = Path(__file__).resolve().parent.parent
if str(REPO_ROOT) not in sys.path:
sys.path.insert(0, str(REPO_ROOT))

from modern_graphics import ModernGraphicsGenerator, Attribution
from modern_graphics.diagrams.story_slide import _build_static_mini_tile

OUT = REPO_ROOT / "output" / "pretext-mini-tile-refinement-demo.html"

_PALETTE = {
"background": "#FFFFFF",
"border": "#E5E5E5",
"text_primary": "#111111",
"text_secondary": "#555555",
"metric_bg": "#F0F4F8",
"metric_text": "#111111",
"accent_primary": "#0071E3",
}

_DATA = {
"tile_headline": "Revenue acceleration outpaced linear forecast",
"subline": "Pipeline quality improved as cycle times compressed across regions.",
"pill": "FY 2025",
"metric": "Decision quality drives outcomes",
"chart": [72, 98, 68, 105],
}


def main() -> None:
gen = ModernGraphicsGenerator(
"Pretext mini-tile refinement demo",
Attribution(copyright="© Demo", show=False),
use_pretext=True,
)
left = _build_static_mini_tile(
640,
400,
_PALETTE,
_DATA,
use_pretext=True,
pretext_foreign_object_layout="legacy",
)
right = _build_static_mini_tile(
640,
400,
_PALETTE,
_DATA,
use_pretext=True,
pretext_foreign_object_layout="refined",
)
body = f"""
<div class="page">
<h1>Mini-tile Pretext: legacy vs refined <code>foreignObject</code></h1>
<p class="lede">Same headline and subline. Left: pre-refinement band heights (44px / 36px) with Pretext—multi-line SVG text is clipped. Right: reserved bands (88px / 52px) with the chart and pill shifted down.</p>
<div class="row">
<figure>
<figcaption>Legacy bands (clips)</figcaption>
<div class="tile-wrap">{left}</div>
</figure>
<figure>
<figcaption>Refined (current default)</figcaption>
<div class="tile-wrap">{right}</div>
</figure>
</div>
</div>
"""
css = """
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; padding: 32px 48px;
background: #f4f4f5; color: #18181b; }
.page { max-width: 1400px; margin: 0 auto; }
h1 { font-size: 1.35rem; font-weight: 650; margin: 0 0 12px; }
.lede { max-width: 72ch; line-height: 1.5; color: #52525b; margin: 0 0 28px; }
.row { display: flex; flex-wrap: wrap; gap: 32px; align-items: flex-start; }
figure { margin: 0; flex: 1 1 420px; background: #fff; padding: 20px;
border-radius: 16px; border: 1px solid #e4e4e7; }
figcaption { font-size: 0.8rem; font-weight: 600; text-transform: uppercase;
letter-spacing: 0.06em; color: #71717a; margin-bottom: 12px; }
.tile-wrap { max-width: 100%; }
.tile-wrap svg { width: 100%; height: auto; display: block; }
code { font-size: 0.9em; }
"""
html = gen._wrap_html(body, css)
OUT.parent.mkdir(parents=True, exist_ok=True)
OUT.write_text(html, encoding="utf-8")
print(f"Wrote {OUT}")
print("Open in a browser (network for Pretext CDN + Google Fonts).")


if __name__ == "__main__":
main()
34 changes: 32 additions & 2 deletions modern_graphics/diagrams/comparison.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,15 @@ def generate_comparison_diagram(
step_size = max(tokens.typography.caption, 13)
outcome_size = max(tokens.typography.body, 17)
vs_size = max(tokens.typography.caption, 13)
use_pretext = getattr(generator, "use_pretext", False)
display_font = "'SF Pro Display', -apple-system, BlinkMacSystemFont, sans-serif"
if color_scheme is not None:
display_font = (
getattr(color_scheme, "font_family_display", None)
or getattr(color_scheme, "font_family", display_font)
)
if use_pretext:
from ..pretext_renderer import pretext_slot
section_gap = tokens.spacing.xl
card_padding = tokens.spacing.lg + tokens.spacing.xs
step_gap = tokens.spacing.sm
Expand All @@ -47,11 +56,21 @@ def generate_column_html(column_data: Dict, column_class: str, is_positive: bool
title = column_data['title']
steps = column_data['steps']
icon = "✕" if not is_positive else "✓"
if use_pretext:
title_html = pretext_slot(
text=title,
font=f"{column_title_size}px {display_font}",
max_width=350,
line_height=1.2,
css_class="column-title",
)
else:
title_html = f'<div class="column-title">{title}</div>'

html = f''' <div class="column {column_class}">
<div class="column-header">
<div class="column-icon">{icon}</div>
<div class="column-title">{title}</div>
{title_html}
</div>
<div class="steps-container">
'''
Expand Down Expand Up @@ -265,7 +284,18 @@ def generate_column_html(column_data: Dict, column_class: str, is_positive: bool
{generate_column_html(left_column, left_class, False)}

<div class="vs">
<div class="vs-circle">{vs_text}</div>
{"".join([
'<div class="vs-circle">',
pretext_slot(
text=vs_text,
font=f"{vs_size}px {display_font}",
max_width=36,
line_height=1.0,
css_class="vs-label",
text_anchor="middle",
) if use_pretext else vs_text,
"</div>",
])}
</div>

{generate_column_html(right_column, right_class, True)}
Expand Down
33 changes: 31 additions & 2 deletions modern_graphics/diagrams/funnel.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,15 @@ def generate_funnel_diagram(
]

theme = extract_theme_colors(color_scheme)
use_pretext = getattr(generator, "use_pretext", False)
display_font = "'Inter', -apple-system, BlinkMacSystemFont, sans-serif"
if color_scheme is not None:
display_font = (
getattr(color_scheme, "font_family_display", None)
or getattr(color_scheme, "font_family", display_font)
)
if use_pretext:
from ..pretext_renderer import pretext_slot
max_value = max(stage.get("value", 0) for stage in stages) or 1
min_width = 45
max_width = 92
Expand All @@ -81,7 +90,27 @@ def generate_funnel_diagram(
""")

conversion = (stages[-1].get("value", 0) / stages[0].get("value", 1)) * 100
conversion_text = f"{conversion:.1f}%"
template = _get_template(generator)
if use_pretext:
title_html = pretext_slot(
text=generator.title,
font=f"28px {display_font}",
max_width=520,
line_height=1.2,
css_class="funnel-title",
)
metric_value_html = pretext_slot(
text=conversion_text,
font=f"26px {display_font}",
max_width=180,
line_height=1.2,
css_class="value metric-value",
text_anchor="end",
)
else:
title_html = f'<div class="funnel-title">{generator.title}</div>'
metric_value_html = f'<div class="value">{conversion_text}</div>'
css_content = f"""
{generate_css_variables(theme)}

Expand Down Expand Up @@ -182,10 +211,10 @@ def generate_funnel_diagram(
html_content = f"""
<div class="funnel-wrapper">
<div class="funnel-header">
<div class="funnel-title">{generator.title}</div>
{title_html}
<div class="funnel-metric">
<div class="label">Overall Conversion</div>
<div class="value">{conversion:.1f}%</div>
{metric_value_html}
</div>
</div>
<div class="funnel-stages">
Expand Down
42 changes: 39 additions & 3 deletions modern_graphics/diagrams/grid.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,24 +29,49 @@ def generate_grid_diagram(
HTML string
"""
theme = extract_theme_colors(color_scheme)
use_pretext = getattr(generator, "use_pretext", False)
display_font = "'Inter', -apple-system, BlinkMacSystemFont, sans-serif"
body_font = display_font
if color_scheme is not None:
display_font = (
getattr(color_scheme, "font_family_display", None)
or getattr(color_scheme, "font_family", display_font)
)
body_font = (
getattr(color_scheme, "font_family_body", None)
or getattr(color_scheme, "font_family", body_font)
)
if use_pretext:
from ..pretext_renderer import pretext_slot

items_html = []
for item in items:
number = item.get("number")
text = item["text"]
if use_pretext:
item_text_html = pretext_slot(
text=text,
font=f"16px {body_font}",
max_width=130,
line_height=1.5,
css_class="md-subtitle1",
text_anchor="middle",
)
else:
item_text_html = f'<div class="md-subtitle1">{text}</div>'
if number:
items_html.append(f"""
<div class="md-card">
<div class="md-card-badge" aria-hidden="true">{number}</div>
<div class="md-card-content">
<div class="md-subtitle1">{text}</div>
{item_text_html}
</div>
</div>""")
else:
items_html.append(f"""
<div class="md-card">
<div class="md-card-content">
<div class="md-subtitle1">{text}</div>
{item_text_html}
</div>
</div>""")

Expand Down Expand Up @@ -212,11 +237,22 @@ def generate_grid_diagram(

{ATTRIBUTION_STYLES}
"""
if use_pretext:
headline_html = pretext_slot(
text=generator.title,
font=f"24px {display_font}",
max_width=860,
line_height=1.2,
css_class="md-headline",
text_anchor="middle",
)
else:
headline_html = f'<div class="md-headline">{generator.title}</div>'

html_content = f"""
<div class="wrapper">
<div class="container">
<div class="md-headline">{generator.title}</div>
{headline_html}
<div class="md-grid">
{''.join(items_html)}
</div>{convergence_html}
Expand Down
Loading
Loading