Files
formation-arduino/schemas/outils.py
T
ArtemiaandClaude Opus 5.5 170588dc2c Corrige le texte qui débordait des boîtes dans les schémas, et l'en-tête des PDF
toile() laissait les marges par défaut de matplotlib : une unité valait ~0,78 cm au lieu de 1 cm, les boîtes étaient donc trop petites pour leur texte. Les axes occupent désormais toute la figure, et boite() réduit la police si le texte dépasse encore (ajuster_texte). En-tête et pied de page : grille et align() à la place de #h(1fr), neutralisé par le préambule de Quarto (titre renvoyé à la ligne, numéro de page à gauche).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 16:25:29 +02:00

153 lines
7.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Briques graphiques communes : toile de dessin, boîtes, flèches, chronogrammes, séquences."""
import matplotlib.pyplot as plt
from matplotlib.patches import FancyBboxPatch
from style import BLEU, CM, ENCRE, GRIS, GRIS_CLAIR
CLAIR = { # fonds pâles associés aux couleurs de la charte
BLEU: "#e8f0fe", "#e07a1f": "#fdf0e3", "#2e8b57": "#e6f4ea",
"#c0392b": "#fce8e6", GRIS: "#f3f4f6",
}
def toile(largeur_cm, hauteur_cm, xmax=None, ymax=None):
"""Figure sans axes, en coordonnées « cm » (1 unité = 1 cm à l'échelle d'origine)."""
fig = plt.figure(figsize=(largeur_cm * CM, hauteur_cm * CM))
ax = fig.add_axes((0, 0, 1, 1)) # pas de marges : 1 unité = 1 cm, police et boîtes à la même échelle
ax.set_xlim(0, xmax or largeur_cm)
ax.set_ylim(0, ymax or hauteur_cm)
ax.set_aspect("equal")
ax.axis("off")
return fig, ax
def boite(ax, x, y, l, h, texte="", couleur=BLEU, taille=9, gras=False, fond=None, style="round", alpha=1):
"""Boîte arrondie centrée sur (x, y). Renvoie un dict des points d'ancrage."""
fond = fond if fond is not None else CLAIR.get(couleur, "white")
ax.add_patch(FancyBboxPatch((x - l / 2, y - h / 2), l, h,
boxstyle=f"{style},pad=0,rounding_size=0.15" if style == "round" else style,
facecolor=fond, edgecolor=couleur, linewidth=1.4, alpha=alpha))
if texte:
t = ax.text(x, y, texte, ha="center", va="center", fontsize=taille, color=ENCRE,
fontweight="bold" if gras else "normal", linespacing=1.3)
ajuster_texte(ax, t, l, h)
return {"c": (x, y), "g": (x - l / 2, y), "d": (x + l / 2, y),
"h": (x, y + h / 2), "b": (x, y - h / 2)}
def ajuster_texte(ax, t, l, h, marge=0.86, taille_min=5.5):
"""Réduit la police du texte t jusqu'à ce qu'il tienne dans une boîte l × h (unités de données)."""
rendu = ax.figure.canvas.get_renderer()
ax.apply_aspect() # sinon la mise à l'échelle « equal » n'est appliquée qu'au rendu final
(x0, y0), (x1, y1) = ax.transData.transform([(0, 0), (l, h)])
largeur_max, hauteur_max = abs(x1 - x0) * marge, abs(y1 - y0) * marge
while t.get_fontsize() > taille_min:
ext = t.get_window_extent(rendu)
if ext.width <= largeur_max and ext.height <= hauteur_max:
break
t.set_fontsize(t.get_fontsize() * 0.95)
def fleche(ax, depart, arrivee, texte="", couleur=ENCRE, taille=8, double=False, pointille=False,
cote="haut", decalage=0.18, courbe=0, fond=False):
"""Flèche entre deux points, avec étiquette optionnelle au milieu.
fond=True place l'étiquette sur fond blanc (utile quand elle croise des traits).
"""
ax.annotate("", xy=arrivee, xytext=depart,
arrowprops=dict(arrowstyle="<|-|>" if double else "-|>", color=couleur, lw=1.3,
linestyle="--" if pointille else "-", shrinkA=0, shrinkB=0,
mutation_scale=11, connectionstyle=f"arc3,rad={courbe}"))
if texte:
cadre = dict(boxstyle="square,pad=0.1", fc="white", ec="none") if fond else None
mx, my = (depart[0] + arrivee[0]) / 2, (depart[1] + arrivee[1]) / 2
vertical = abs(arrivee[0] - depart[0]) < abs(arrivee[1] - depart[1])
if vertical:
ax.text(mx + decalage, my, texte, ha="left", va="center", fontsize=taille, color=couleur,
bbox=cadre)
else:
dy = decalage if cote == "haut" else -decalage
ax.text(mx, my + dy, texte, ha="center", va="bottom" if cote == "haut" else "top",
fontsize=taille, color=couleur, bbox=cadre)
def chronogramme(voies, duree, largeur_cm=16, hauteur_voie_cm=1.5, graduations=None, unite="",
couleurs=None):
"""Chronogramme logique.
voies : liste de (nom, [(t, niveau), ...]) — niveau 0/1, maintenu jusqu'au point suivant.
Renvoie (fig, ax, y_voie) où y_voie(i) donne l'ordonnée de base de la voie i.
"""
n = len(voies)
fig, ax = plt.subplots(figsize=(largeur_cm * CM, (n * hauteur_voie_cm + 1.2) * CM))
ax.set_xlim(-duree * 0.02, duree * 1.01)
ax.set_ylim(-0.6, n * 1.6)
ax.spines[["left", "top", "right"]].set_visible(False)
ax.set_yticks([])
ax.grid(axis="y", visible=False)
if graduations is not None:
ax.set_xticks(graduations)
if unite:
ax.set_xlabel(unite)
y_voie = lambda i: (n - 1 - i) * 1.6
for i, (nom, points) in enumerate(voies):
base = y_voie(i)
xs, ys = [], []
for k, (t, niv) in enumerate(points):
fin = points[k + 1][0] if k + 1 < len(points) else duree
xs += [t, fin]
ys += [base + niv, base + niv]
c = (couleurs or {}).get(nom, BLEU)
ax.plot(xs, ys, color=c, lw=1.8, solid_joinstyle="miter")
ax.fill_between(xs, base, ys, color=c, alpha=0.08)
ax.text(-duree * 0.025, base + 0.5, nom, ha="right", va="center", fontsize=9,
color=ENCRE, fontweight="bold")
return fig, ax, y_voie
def sequence(acteurs, messages, largeur_cm=16, pas_cm=0.85, couleurs=None, notes=()):
"""Diagramme de séquence.
acteurs : liste de noms. messages : liste de (source, cible, texte[, pointillé]).
notes : liste de (index_message, acteur, texte) affichées à droite de la ligne de vie.
"""
n = len(messages)
hauteur = n * pas_cm + 2.2
fig, ax = toile(largeur_cm, hauteur)
xs = {a: largeur_cm * (i + 0.5) / len(acteurs) for i, a in enumerate(acteurs)}
haut = hauteur - 0.6
for a, x in xs.items():
c = (couleurs or {}).get(a, BLEU)
largeur = min(largeur_cm / len(acteurs) - 0.3, 0.2 * len(a) + 0.9)
boite(ax, x, haut, largeur, 0.8, a, couleur=c, taille=8.5, gras=True)
ax.plot([x, x], [0.55, haut - 0.4], color=GRIS, lw=1, ls=(0, (3, 3)), zorder=0)
for k, msg in enumerate(messages):
src, dst, texte = msg[:3]
pointille = len(msg) > 3 and msg[3]
y = haut - 1.2 - k * pas_cm
if src == dst:
# Boucle vers soi-même : petit rectangle ouvert à droite de la ligne de vie
x = xs[src]
ax.plot([x, x + 0.45, x + 0.45], [y + 0.12, y + 0.12, y - 0.22], color=ENCRE, lw=1.1)
fleche(ax, (x + 0.45, y - 0.22), (x + 0.02, y - 0.22))
ax.text(x + 0.6, y - 0.05, texte, fontsize=7.8, va="center", color=ENCRE,
bbox=dict(boxstyle="square,pad=0.1", fc="white", ec="none"))
continue
fleche(ax, (xs[src], y), (xs[dst], y), texte, pointille=pointille, taille=7.8, decalage=0.08,
fond=True)
for k, acteur, texte in notes:
y = haut - 1.2 - k * pas_cm
ax.text(xs[acteur] + 0.2, y, texte, fontsize=7.5, color=GRIS, style="italic", va="center")
return fig, ax
def legende_couleurs(ax, elements, x, y, taille=7.5):
"""Petite légende manuelle : elements = [(couleur, texte), ...]."""
for i, (c, t) in enumerate(elements):
ax.add_patch(plt.Rectangle((x, y - i * 0.45 - 0.12), 0.35, 0.25, color=c))
ax.text(x + 0.5, y - i * 0.45, t, fontsize=taille, va="center", color=ENCRE)
__all__ = ["toile", "boite", "fleche", "chronogramme", "sequence", "legende_couleurs",
"CLAIR", "GRIS_CLAIR"]