Files

499 lines
17 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
wireframes.py — Vignettes SVG des layouts (Sliding Design System)
=================================================================
Schémas grosse maille, aux couleurs de la charte, FIDÈLES à la
structure de chaque _render_* du moteur (cartes, bandeaux navy/coral,
cercles, colonnes, barres, badges…). Pur Python/SVG : aucune
dépendance, aucun LibreOffice, régénérable en une fraction de seconde.
API : svg_for(layout_name, mode) -> str (SVG inline, viewBox 320x180).
Un layout inconnu retombe sur un gabarit générique titre + lignes.
Consommé par build_gallery.py (vignette dans chaque fiche).
"""
# ── Charte ───────────────────────────────────────────────────────────────────
NAVY = "#061033"
NAVY2 = "#10204D"
CORAL = "#F4795B"
GLACIER = "#8FA9D0"
SLATE = "#46505A"
CARD = "#F4F1EC"
BODY = "#2B3440"
MUTED = "#C9CDD3"
WHITE = "#FFFFFF"
W, H = 320, 180
MX = 20 # marge horizontale de la zone utile
# ── Primitives ───────────────────────────────────────────────────────────────
def _r(x, y, w, h, fill, rad=0, op=1.0, stroke=None):
s = f' stroke="{stroke}" stroke-width="1"' if stroke else ""
return (f'<rect x="{x:.0f}" y="{y:.0f}" width="{w:.0f}" '
f'height="{h:.0f}" rx="{rad}" fill="{fill}" '
f'opacity="{op}"{s}/>')
def _c(cx, cy, rad, fill, op=1.0):
return (f'<circle cx="{cx:.0f}" cy="{cy:.0f}" r="{rad:.0f}" '
f'fill="{fill}" opacity="{op}"/>')
def _title_bar(y=16, w=150, c=NAVY):
return _r(MX, y, w, 12, c, rad=2)
def _lines(x, y, w, n, gap=13, c=MUTED, first=None, fw=None):
out = []
for i in range(n):
cc = first if (first and i == 0) else c
ww = (fw or w) if (fw and i == 0) else w
out.append(_r(x, y + i * gap, ww, 7, cc, rad=3))
return "".join(out)
def _img_zone(x, y, w, h, mode="light"):
"""Zone image : aplat glacier clair + diagonale (convention wireframe)."""
fill = GLACIER
return (_r(x, y, w, h, fill, rad=4, op=0.5)
+ f'<line x1="{x:.0f}" y1="{y+h:.0f}" x2="{x+w:.0f}" '
f'y2="{y:.0f}" stroke="{GLACIER}" stroke-width="1.5"/>'
+ f'<line x1="{x:.0f}" y1="{y:.0f}" x2="{x+w:.0f}" '
f'y2="{y+h:.0f}" stroke="{GLACIER}" stroke-width="1.5"/>')
def _frame(mode, body):
bg = NAVY if mode == "dark" else ("#EEF1F6" if mode == "panel"
else WHITE)
return (f'<svg xmlns="http://www.w3.org/2000/svg" '
f'viewBox="0 0 {W} {H}" class="wf">'
f'{_r(0, 0, W, H, bg, rad=8)}'
f'<rect x="0.5" y="0.5" width="{W-1}" height="{H-1}" rx="8" '
f'fill="none" stroke="#E3E0D9"/>{body}</svg>')
def _title_light(mode):
return WHITE if mode == "dark" else NAVY
# ── Archétypes (fidèles aux _render_*) ───────────────────────────────────────
def _wf_cover(mode):
# cover_split : fond navy, cercles décoratifs coin, filet coral, titre
b = [_c(300, 20, 46, NAVY2), _c(288, 150, 30, CORAL, 0.9),
_c(250, 150, 14, GLACIER),
_r(MX, 74, 40, 4, CORAL, rad=1),
_r(MX, 88, 150, 16, WHITE, rad=2),
_r(MX, 120, 96, 8, GLACIER, rad=2)]
return _frame("dark", "".join(b))
def _wf_section(mode):
# section_divider : navy, gros numéro fantôme, titre
b = [_c(300, 150, 40, NAVY2),
_r(MX, 78, 20, 20, CORAL, rad=2),
_r(MX, 104, 130, 14, WHITE, rad=2)]
return _frame("dark", "".join(b))
def _wf_end(mode):
b = [_c(300, 24, 40, CORAL, 0.9), _c(30, 150, 46, NAVY2),
_r(96, 74, 128, 14, WHITE, rad=2),
_r(120, 100, 80, 8, GLACIER, rad=2)]
return _frame("dark", "".join(b))
def _wf_big_stat(mode):
tl = _title_light(mode)
# titre en haut ; chiffre coral CENTRÉ (H et V) ; desc + source
# centrées dessous — cf _render_big_stat (align CENTER, _cy).
b = [_title_bar(w=120, c=tl),
_r(90, 66, 140, 40, CORAL, rad=4), # chiffre héros, centré
_r(110, 116, 100, 8, MUTED, rad=3), # description centrée
_r(134, 132, 52, 5, MUTED, rad=2, op=0.6)] # source centrée
return _frame(mode, "".join(b))
def _wf_key_message(mode):
# key_message : fond navy, grande phrase centrée
b = [_r(40, 70, 240, 14, WHITE, rad=2),
_r(70, 92, 180, 14, GLACIER, rad=2),
_r(130, 120, 60, 6, CORAL, rad=2)]
return _frame("dark", "".join(b))
def _wf_bullets(mode):
tl = _title_light(mode)
# titre en haut ; puces CARRÉES coral + texte, bloc centré vertical.
# Pas de barre latérale (cf _render_default_bullets).
b = [_title_bar(c=tl)]
for y in (66, 94, 122):
b.append(_r(MX, y, 8, 8, CORAL, rad=1)) # square_mark coral
b.append(_r(MX + 18, y, 250, 8, MUTED, rad=3))
return _frame(mode, "".join(b))
def _wf_two_cols(mode):
b = [_title_bar(w=170)]
cw, xs, acc = 132, (MX, 168), (NAVY, CORAL)
for i in range(2):
x = xs[i]
b.append(_r(x, 44, cw, 120, CARD, rad=6))
b.append(_r(x, 44, cw, 26, acc[i], rad=6))
b.append(_r(x, 58, cw, 12, acc[i]))
b.append(_r(x + 12, 52, 70, 10, WHITE, rad=3))
b.append(_lines(x + 12, 84, 100, 4, gap=15))
return _frame("light", "".join(b))
def _wf_from_to(mode):
b = [_title_bar(w=150)]
for i, (x, c) in enumerate(((MX, NAVY), (176, SLATE))):
b.append(_r(x, 50, 120, 100, CARD, rad=6))
b.append(_r(x, 50, 120, 6, c, rad=3))
b.append(_lines(x + 12, 70, 90, 4, gap=16))
b.append(_c(160, 100, 10, CORAL)) # flèche/pivot central
return _frame("light", "".join(b))
def _wf_exec_summary(mode):
b = [_title_bar(w=160)]
for i, x in enumerate((MX, 112, 204)):
b.append(_r(x, 54, 96, 108, CARD, rad=6))
b.append(_c(x + 16, 74, 9, NAVY))
b.append(_r(x + 12, 96, 72, 7, SLATE, rad=3))
b.append(_lines(x + 12, 112, 72, 3, gap=12))
b.append(_r(MX, 54, 96, 4, CORAL, rad=2)) # 1re carte accentuée
return _frame("light", "".join(b))
def _wf_kpi_grid(mode):
b = [_title_bar(w=120)]
xs = (MX, 112, 204)
for i, x in enumerate(xs):
b.append(_r(x, 56, 96, 100, CARD, rad=6))
b.append(_r(x + 12, 74, 50, 22, CORAL if i == 0 else NAVY,
rad=3))
b.append(_r(x + 12, 104, 70, 6, SLATE, rad=3))
b.append(_r(x + 12, 118, 56, 6, MUTED, rad=3))
return _frame("light", "".join(b))
def _wf_numbered_steps(mode):
b = [_title_bar(w=150)]
for i, y in enumerate((52, 86, 120)):
b.append(_r(MX, y, 280, 28, CARD, rad=6))
b.append(_c(MX + 18, y + 14, 11, NAVY))
b.append(_r(MX + 40, y + 10, 200, 8, MUTED, rad=3))
return _frame("light", "".join(b))
def _wf_agenda(mode):
b = [_title_bar(w=140)]
for i, y in enumerate((52, 84, 116, 148)):
active = (i == 1)
b.append(_c(MX + 12, y, 10, CORAL if active else NAVY))
b.append(_r(MX + 30, y - 5, 180, 9,
NAVY if active else MUTED, rad=3))
b.append(_r(230, y - 5, 60, 9, MUTED, rad=3, op=0.6))
return _frame("light", "".join(b))
def _wf_circular(mode):
tl = _title_light(mode)
b = [_title_bar(w=130, c=tl)]
cx, cy = 110, 108
for ang, c in ((0, NAVY), (120, SLATE), (240, GLACIER)):
import math
x = cx + 34 * math.cos(math.radians(ang))
y = cy + 34 * math.sin(math.radians(ang))
b.append(_c(x, y, 22, c))
b.append(_c(cx, cy, 14, CORAL))
# légende
for i, y in enumerate((70, 100, 130)):
b.append(_c(210, y, 6, (NAVY, SLATE, GLACIER)[i]))
b.append(_r(224, y - 4, 76, 7, MUTED, rad=3))
return _frame(mode, "".join(b))
def _wf_recommendation(mode):
b = [_title_bar(w=150),
_r(MX, 48, 190, 116, CARD, rad=6),
_r(MX, 48, 190, 6, CORAL, rad=3),
_c(MX + 20, 72, 10, NAVY),
_lines(MX + 40, 66, 130, 4, gap=16),
_r(228, 48, 72, 116, NAVY, rad=6), # panneau latéral navy
_r(240, 70, 48, 8, GLACIER, rad=3),
_lines(240, 92, 48, 3, gap=13, c="#33436A")]
return _frame("light", "".join(b))
def _wf_phases_timeline(mode):
b = [_title_bar(w=150)]
for i, x in enumerate((MX, 112, 204)):
c = (NAVY, SLATE, GLACIER)[i]
b.append(_r(x, 74, 96, 44, c, rad=6))
if i < 2:
b.append(_r(x + 96, 94, 16, 4, MUTED, rad=2))
b.append(_r(x + 20, 126, 56, 6, MUTED, rad=3))
return _frame("light", "".join(b))
def _wf_gantt(mode):
b = [_title_bar(w=140)]
rows = ((40, 120, NAVY), (90, 90, SLATE), (150, 100, NAVY),
(70, 130, GLACIER))
for i, (x, w, c) in enumerate(rows):
y = 56 + i * 26
b.append(_r(MX, y, 60, 10, MUTED, rad=2)) # label
b.append(_r(90 + x, y, w, 12, c, rad=3)) # barre
return _frame("light", "".join(b))
def _wf_yearly(mode):
b = [_title_bar(w=130),
_r(MX, 100, 280, 3, NAVY)] # ligne de temps
for i, x in enumerate((40, 120, 200, 280)):
b.append(_c(x, 101, 7, CORAL if i == 1 else NAVY))
b.append(_r(x - 20, 116, 40, 6, MUTED, rad=2))
return _frame("light", "".join(b))
def _wf_comparison(mode):
b = [_title_bar(w=150), _r(MX, 48, 280, 20, NAVY, rad=4)] # entête
for j in range(3):
b.append(_r(MX + 4 + j * 92, 52, 84, 12, WHITE, rad=2,
op=0.5))
for i in range(4):
y = 72 + i * 22
b.append(_r(MX, y, 280, 18, CARD if i % 2 else WHITE, rad=2,
stroke="#E3E0D9"))
for j in range(3):
b.append(_r(MX + 10 + j * 92, y + 5, 64, 7, MUTED, rad=2))
return _frame("light", "".join(b))
def _wf_raci(mode):
b = [_title_bar(w=120), _r(MX, 48, 280, 18, NAVY, rad=4)]
letters = (NAVY, CORAL, SLATE, GLACIER)
for i in range(4):
y = 70 + i * 22
b.append(_r(MX, y, 80, 16, CARD, rad=2))
for j in range(4):
b.append(_c(130 + j * 44, y + 8, 7, letters[j], 0.85))
return _frame("light", "".join(b))
def _wf_process_arrow(mode):
b = [_title_bar(w=140)]
for i, x in enumerate((MX, 120, 220)):
c = CORAL if i == 1 else NAVY
b.append(f'<polygon points="{x},80 {x+70},80 {x+86},104 '
f'{x+70},128 {x},128 {x+16},104" fill="{c}"/>')
return _frame("light", "".join(b))
def _wf_org_chart(mode):
b = [_title_bar(w=120),
_r(130, 52, 60, 26, NAVY, rad=4)]
for x in (60, 140, 220):
b.append(_r(x, 120, 56, 26, CARD, rad=4, stroke="#E3E0D9"))
b.append(f'<line x1="160" y1="78" x2="{x+28}" y2="120" '
f'stroke="{NAVY}" stroke-width="1.5"/>')
return _frame("light", "".join(b))
def _wf_matrix(mode):
tl = _title_light(mode)
b = [_title_bar(w=120, c=tl),
f'<line x1="160" y1="46" x2="160" y2="170" stroke="{SLATE}" '
f'stroke-width="1.5"/>',
f'<line x1="24" y1="108" x2="296" y2="108" stroke="{SLATE}" '
f'stroke-width="1.5"/>']
quad = ((92, 78, NAVY), (228, 78, CORAL),
(92, 140, SLATE), (228, 140, GLACIER))
for cx, cy, c in quad:
b.append(_c(cx, cy, 16, c, 0.9))
return _frame(mode, "".join(b))
def _wf_image_split(mode):
b = [_img_zone(MX, 44, 130, 120),
_r(168, 44, 130, 14, NAVY, rad=2),
_c(176, 78, 3, CORAL),
_lines(184, 74, 108, 4, gap=17)]
return _frame("light", "".join(b))
def _wf_image_full(mode):
b = [_img_zone(0, 0, W, H),
_r(0, 120, W, 60, NAVY, rad=0, op=0.82),
_r(MX, 134, 150, 12, WHITE, rad=2),
_r(MX, 152, 90, 7, GLACIER, rad=2)]
return _frame("light", "".join(b))
def _wf_bar_chart(mode):
b = [_title_bar(w=120)]
vals = (40, 70, 55, 90, 62)
for i, v in enumerate(vals):
x = 40 + i * 52
b.append(_r(x, 150 - v, 32, v, NAVY, rad=2))
b.append(_r(30, 150, 270, 2, SLATE))
return _frame("light", "".join(b))
def _wf_line_chart(mode):
b = [_title_bar(w=120), _r(30, 150, 270, 2, SLATE)]
pts = [(40, 130), (100, 96), (160, 110), (220, 70), (288, 84)]
path = " ".join(f"{x},{y}" for x, y in pts)
b.append(f'<polyline points="{path}" fill="none" stroke="{NAVY}" '
f'stroke-width="2.5"/>')
b.append(_c(288, 84, 5, CORAL))
return _frame("light", "".join(b))
def _wf_donut(mode):
tl = _title_light(mode)
b = [_title_bar(w=110, c=tl)]
b.append(f'<circle cx="100" cy="106" r="40" fill="none" '
f'stroke="{NAVY}" stroke-width="18"/>')
b.append(f'<path d="M100 66 A40 40 0 0 1 138 118" fill="none" '
f'stroke="{CORAL}" stroke-width="18"/>')
for i, y in enumerate((84, 108, 132)):
b.append(_c(210, y, 6, (NAVY, CORAL, GLACIER)[i]))
b.append(_r(224, y - 4, 76, 7, MUTED, rad=3))
return _frame(mode, "".join(b))
def _wf_waterfall(mode):
b = [_title_bar(w=140), _r(30, 150, 270, 2, SLATE)]
# colonnes cumulées : base navy, deltas coral, total navy
cols = ((40, 150 - 70, 70, NAVY), (94, 80, 34, CORAL),
(148, 60, 40, CORAL), (202, 44, 30, CORAL),
(256, 150 - 90, 90, NAVY))
for x, y, h, c in cols:
b.append(_r(x, y, 34, h, c, rad=2))
return _frame("light", "".join(b))
def _wf_heatmap(mode):
b = [_title_bar(w=120)]
import random
random.seed(3)
shades = ["#D6DCEA", "#9FB0D0", GLACIER, NAVY2, NAVY]
for r in range(4):
for cix in range(5):
b.append(_r(60 + cix * 46, 54 + r * 26, 42, 22,
shades[(r + cix) % 5], rad=2))
b.append(_r(MX, 58 + r * 26, 32, 12, MUTED, rad=2))
return _frame("light", "".join(b))
def _wf_funnel(mode):
b = [_title_bar(w=110)]
widths = (240, 180, 120, 70)
cols = (NAVY, NAVY2, GLACIER, CORAL)
for i, w in enumerate(widths):
x = (W - w) / 2
b.append(f'<polygon points="{x},{58+i*26} {x+w},{58+i*26} '
f'{x+w-14},{80+i*26} {x+14},{80+i*26}" '
f'fill="{cols[i]}"/>')
return _frame("light", "".join(b))
def _wf_pyramid(mode):
b = [_title_bar(w=110)]
widths = (90, 150, 210, 270)
cols = (CORAL, GLACIER, SLATE, NAVY)
for i, w in enumerate(widths):
x = (W - w) / 2
b.append(_r(x, 132 - i * 24, w, 20, cols[i], rad=2))
return _frame("light", "".join(b))
def _wf_generic(mode):
tl = _title_light(mode)
return _frame(mode, _title_bar(c=tl) + _lines(MX, 50, 240, 5,
gap=18))
# ── Mapping layout → archétype ───────────────────────────────────────────────
_MAP = {
"cover_split": _wf_cover,
"section_divider": _wf_section,
"end_slide": _wf_end,
"big_stat": _wf_big_stat,
"key_message": _wf_key_message,
"default_bullets": _wf_bullets,
"two_cols_text": _wf_two_cols,
"from_to_pairs": _wf_from_to,
"executive_summary": _wf_exec_summary,
"kpi_grid": _wf_kpi_grid,
"numbered_steps": _wf_numbered_steps,
"agenda": _wf_agenda,
"circular_diagram": _wf_circular,
"recommendation_card": _wf_recommendation,
"phases_timeline": _wf_phases_timeline,
"gantt_timeline": _wf_gantt,
"yearly_timeline": _wf_yearly,
"comparison_table": _wf_comparison,
"raci_table": _wf_raci,
"process_arrow": _wf_process_arrow,
"org_chart": _wf_org_chart,
"matrix_2x2": _wf_matrix,
"image_split": _wf_image_split,
"image_full": _wf_image_full,
"bar_chart": _wf_bar_chart,
"line_chart": _wf_line_chart,
"donut_split": _wf_donut,
"waterfall": _wf_waterfall,
"heatmap_table": _wf_heatmap,
"funnel": _wf_funnel,
"pyramid": _wf_pyramid,
# freeform : pas de structure fixe → gabarit générique
"freeform": _wf_generic,
}
def svg_for(layout_name, mode="light"):
fn = _MAP.get(layout_name, _wf_generic)
try:
return fn(mode)
except Exception:
return _wf_generic(mode)
def known():
return set(_MAP)
if __name__ == "__main__":
# Auto-test : rend tous les archétypes dans un HTML de contrôle
import yaml
import os
try:
L = yaml.safe_load(open("layouts_v2.yaml",
encoding="utf-8"))["layouts"]
names = list(L)
except Exception:
names = list(_MAP)
cells = "".join(
f'<figure style="margin:8px;display:inline-block;width:240px">'
f'{svg_for(n)}<figcaption style="font:12px sans-serif;'
f'text-align:center">{n}</figcaption></figure>'
for n in names)
open("_wireframes_preview.html", "w").write(
f"<!doctype html><meta charset=utf-8><body "
f"style='background:#FAF9F7'>{cells}</body>")
missing = [n for n in names if n not in _MAP]
print("layouts:", len(names), "| mappés:",
len([n for n in names if n in _MAP]),
"| génériques:", missing or "aucun")
print("Contrôle visuel : _wireframes_preview.html")