diagram-02-branch-timeline
diagram-01-layer-stack
Pile de couches isométriques légendées
Montrer qu'un objet — un produit, une marque, un système logiciel — se décompose en couches ORDONNÉES dont le rang est l'information : des losanges isométriques empilés, d'opacité décroissante, chacun relié par un filet au nom de sa couche et à une ligne d'explication.
diagramslideweb1040×420px15 benchmarks
Le fragment RÉEL, dans son propre document, sur le sol de sa charte — pas une image. Ce que vous voyez est exactement ce que vous collerez.
Employer quand
Trois à cinq couches, un ordre qui compte (ce qu'on touche en haut, ce qui porte en bas), et une phrase à dire sur chacune : interface / services / données, essence / valeur / vision, produit / méthode / recherche.
Éviter quand
Les éléments sont de même rang (une pile dit une hiérarchie, même quand on ne la pense pas), leur nombre dépasse cinq (les losanges du bas s'effacent avant d'être comptés), ou une grandeur doit se lire : ici rien n'est proportionnel à rien — c'est un schéma, pas un graphique.
Notes
Le pas à 0,57 hauteur de plan n'est pas un goût : sous 0,5, le centre d'une couche passe sous le plan qui la surplombe et son filet ne peut plus en sortir — le schéma perd sa correspondance avant de perdre sa lisibilité, et rien ne le signale. C'est le benchmark « le centre échappe au plan du dessus » qui tient ce seuil. L'opacité suit une progression GÉOMÉTRIQUE (× 0,42) et non une soustraction régulière : à pas soustractif constant, la troisième couche reste trop dense et la pile paraît plate. Les recouvrements ne sont jamais dessinés — ils sont composités par le navigateur, et c'est ce qui rend le schéma thémable en ne changeant qu'UNE couleur. Écart assumé avec la référence : elle laisse les filets DÉRIVER (les blocs de texte coulent avec un écart constant, la correspondance repose sur l'ordre de lecture) ; ici chaque filet est verrouillé sur le centre de sa couche, parce que « c'est le troisième donc c'est celui du bas » n'est pas une correspondance dès que le schéma est technique. Pour passer à 4 ou 5 couches : un plan et une rangée de plus avec le `--i` suivant, un alpha qui poursuit la même raison géométrique, et une hauteur de cadre = hauteur de plan + (n − 1) × pas + 2 × padding.
Mots-clés
Benchmarks — la géométrie, en assertions vérifiables
Rejouables par node bin/check.mjs diagram-01-layer-stack : le pattern se prouve, il ne se croit pas.
| assertion | mesure | attendu |
|---|---|---|
| un plan est un carré en projection isométrique : deux fois plus large que haut | box('.vl-layers__plane--1').h / box('.vl-layers__plane--1').w | 0.53 ± 0.04 |
| le pas est le même entre toutes les couches (une pile irrégulière se lit comme un accident) | Math.abs((box('.vl-layers__plane--2').top - box('.vl-layers__plane--1').top) - (box('.vl-layers__plane--3').top - box('.vl-layers__plane--2').top)) | ≤ 1 |
| le pas vaut 0,57 hauteur de plan : assez pour que les plans se distinguent, assez peu pour qu'ils fassent pile | (box('.vl-layers__plane--2').top - box('.vl-layers__plane--1').top) / box('.vl-layers__plane--1').h | 0.571 ± 0.05 |
| le centre d'une couche échappe au plan qui la surplombe — sans quoi son filet ne peut pas en sortir | Math.min(...[[1,2],[2,3]].map(([a,b]) => (box('.vl-layers__plane--'+b).top + box('.vl-layers__plane--'+b).h/2) - box('.vl-layers__plane--'+a).bottom)) | ≥ 2 |
| l'opacité décroît d'un rapport CONSTANT d'une couche à la suivante (progression géométrique) | (num('.vl-layers__plane--2','opacity') / num('.vl-layers__plane--1','opacity')) / (num('.vl-layers__plane--3','opacity') / num('.vl-layers__plane--2','opacity')) | 1 ± 0.15 |
| la couche de tête est la seule pleine | num('.vl-layers__plane--1','opacity') | 1 ± 0 |
| la dernière couche est un fantôme : au moins 4 fois plus effacée que la première | num('.vl-layers__plane--1','opacity') / num('.vl-layers__plane--3','opacity') | ≥ 4 |
| chaque filet est à la hauteur du centre de SA couche : la correspondance est géométrique, pas déduite de l'ordre | Math.max(...[1,2,3].map(i => Math.abs(box('.vl-layers__row--'+i+' .vl-layers__rule').top - (box('.vl-layers__plane--'+i).top + box('.vl-layers__plane--'+i).h/2)))) | ≤ 2 |
| chaque filet touche le milieu de la ligne de titre, pas l'interligne sous elle | Math.max(...[1,2,3].map(i => Math.abs(box('.vl-layers__row--'+i+' .vl-layers__rule').top - (box('.vl-layers__row--'+i+' .vl-layers__name').top + box('.vl-layers__row--'+i+' .vl-layers__name').h/2)))) | ≤ 2 |
| le filet naît SOUS le losange (à 0,655 de sa largeur) : sa longueur visible est dictée par la forme | (box('.vl-layers__row--1 .vl-layers__rule').left - box('.vl-layers__plane--1').left) / box('.vl-layers__plane--1').w | 0.655 ± 0.05 |
| le filet s'arrête avant le texte : il désigne, il ne souligne pas | (box('.vl-layers__row--1 .vl-layers__name').left - box('.vl-layers__row--1 .vl-layers__rule').right) / W | 0.027 ± 0.008 |
| la colonne de libellés et la pile occupent la même bande verticale | Math.abs((box('.vl-layers__row--1').top + box('.vl-layers__row--3').bottom) / 2 - (box('.vl-layers__plane--1').top + box('.vl-layers__plane--3').bottom) / 2) / H | ≤ 0.05 |
| plancher de lisibilité : le corps ne descend pas sous 1,2 % de la largeur du schéma | num('.vl-layers__note','fontSize') / W | ≥ 0.012 |
| contraste de la ligne d'explication sur le fond ≥ 4,5 | contrast('.vl-layers__note') | ≥ 4.5 |
| aucun débordement hors du cadre du schéma | overflow('.vl-layers') | ≤ 0 |
Le code
patterns/diagram-01-layer-stack.html
Télécharger<!-- diagram-01-layer-stack — fragment autonome. Couleurs via --vl-* uniquement, aucune valeur en dur. -->
<figure class="vl-layers">
<div class="vl-layers__stack" aria-hidden="true">
<div class="vl-layers__plane vl-layers__plane--1"></div>
<div class="vl-layers__plane vl-layers__plane--2"></div>
<div class="vl-layers__plane vl-layers__plane--3"></div>
</div>
<div class="vl-layers__row vl-layers__row--1">
<span class="vl-layers__rule"></span>
<h3 class="vl-layers__name">Interface</h3>
<p class="vl-layers__note">Every surface a customer touches — web, mobile, in-store screen</p>
</div>
<div class="vl-layers__row vl-layers__row--2">
<span class="vl-layers__rule"></span>
<h3 class="vl-layers__name">Services</h3>
<p class="vl-layers__note">The rules that make the product behave the same way on all three</p>
</div>
<div class="vl-layers__row vl-layers__row--3">
<span class="vl-layers__rule"></span>
<h3 class="vl-layers__name">Data</h3>
<p class="vl-layers__note">One store of record — nothing above it keeps a copy of its own</p>
</div>
</figure>
<style>
/* Un objet décomposé en COUCHES ORDONNÉES, où le rang est l'information : la couche du dessus
est celle qu'on voit, les suivantes la portent. Deux mécanismes, et pas un de plus —
l'empilement (chaque plan décalé d'un pas constant, le plus haut au premier plan) et
l'OPACITÉ, qui décroît d'un rapport CONSTANT d'une couche à la suivante. Ni ombre, ni
perspective, ni seconde teinte : c'est ce qui permet de rejouer le schéma sur n'importe
quelle charte en ne changeant qu'une couleur.
Le pas vaut 0,57 × la hauteur d'un plan. Au-dessus de 0,5, le CENTRE d'une couche échappe au
plan qui la surplombe — condition nécessaire pour que son filet en sorte ; en dessous, les
plans se noient les uns dans les autres et la pile devient une tache.
Le filet de rappel naît SOUS le losange et n'apparaît qu'en en sortant : sa longueur visible
est dictée par la forme, jamais posée à la main. Il est calé sur le centre de SA couche —
la référence d'origine le laisse dériver et confie la correspondance à l'ordre de lecture,
ce qui est un pari dès que le schéma est technique. */
.vl-layers {
--vl-plane-w: 330px;
--vl-plane-h: 175px;
--vl-pitch: 100px; /* 0,57 × la hauteur d'un plan */
--vl-rule-x: 216px; /* 0,655 × la largeur d'un plan : le filet naît sous le losange */
--vl-label-x: 430px;
--vl-pad-y: 22px;
--vl-name-line: 26px; /* la ligne du titre : c'est sur SON milieu que le filet se cale */
position: relative;
width: 1040px;
height: 420px;
margin: 0;
font-family: Archivo, -apple-system, "Helvetica Neue", Arial, sans-serif;
}
.vl-layers__stack {
position: absolute; left: 0; top: var(--vl-pad-y);
width: var(--vl-plane-w);
height: calc(var(--vl-plane-h) + 2 * var(--vl-pitch));
}
/* Le losange : un carré vu en projection isométrique, donc deux fois plus large que haut. */
.vl-layers__plane {
position: absolute;
left: 0;
width: var(--vl-plane-w);
height: var(--vl-plane-h);
background: var(--vl-coral);
clip-path: polygon(50% 0, 100% 50%, 50% 100%, 0 50%);
}
/* Rang, décalage et opacité au même endroit : ajouter une couche, c'est ajouter UNE règle
(un pas de plus, un z-index de moins, l'alpha suivant de la raison géométrique). */
.vl-layers__plane--1 { top: 0; opacity: var(--vl-layer-alpha-1); z-index: 3; }
.vl-layers__plane--2 { top: var(--vl-pitch); opacity: var(--vl-layer-alpha-2); z-index: 2; }
.vl-layers__plane--3 { top: calc(2 * var(--vl-pitch)); opacity: var(--vl-layer-alpha-3); z-index: 1; }
/* Une rangée par couche. Ce qui se cale sur le centre du plan n'est PAS le centre de la rangée
mais le milieu de la ligne de TITRE : le filet doit toucher le mot qui nomme la couche, pas
l'interligne entre le titre et son explication. La rangée passe SOUS les plans (z-index 0) :
seul le filet les rencontre, et c'est exactement l'effet recherché. */
.vl-layers__row {
position: absolute;
z-index: 0;
left: var(--vl-label-x);
width: calc(100% - var(--vl-label-x));
/* le centre du plan de rang 1, moins la demi-ligne de titre */
top: calc(var(--vl-pad-y) + var(--vl-plane-h) / 2 - var(--vl-name-line) / 2);
}
.vl-layers__row--2 { margin-top: var(--vl-pitch); }
.vl-layers__row--3 { margin-top: calc(2 * var(--vl-pitch)); }
.vl-layers__rule {
position: absolute;
top: calc(var(--vl-name-line) / 2);
left: calc(var(--vl-rule-x) - var(--vl-label-x));
width: calc(var(--vl-label-x) - var(--vl-rule-x) - 28px);
height: 1px;
background: var(--vl-hairline);
}
.vl-layers__name {
margin: 0;
font-size: 22px; font-weight: 300; line-height: var(--vl-name-line);
letter-spacing: var(--vl-track-title); text-transform: uppercase;
color: var(--vl-ink);
}
.vl-layers__note {
margin: 9px 0 0;
font-size: 14px; font-weight: 400; line-height: 1.7;
letter-spacing: var(--vl-track-body); text-transform: uppercase;
color: var(--vl-ink-muted);
}
</style>
systems/ref-14-layer-stack-coral.json
Télécharger:root {
--vl-bg: #F1EFEC;
--vl-coral: #E4886A;
--vl-layer-alpha-1: 1;
--vl-layer-alpha-2: 0.44;
--vl-layer-alpha-3: 0.18;
--vl-ink: #3A3835;
--vl-ink-muted: #6E6863;
--vl-hairline: #97918C;
--vl-track-title: 0.13em;
--vl-track-body: 0.05em;
}
les métadonnées — patterns/diagram-01-layer-stack.json (télécharger)
{
"name": "Pile de couches isométriques légendées",
"family": "diagram",
"ref": "ref-14-layer-stack-coral",
"intent": "Montrer qu'un objet — un produit, une marque, un système logiciel — se décompose en couches ORDONNÉES dont le rang est l'information : des losanges isométriques empilés, d'opacité décroissante, chacun relié par un filet au nom de sa couche et à une ligne d'explication.",
"when_to_use": "Trois à cinq couches, un ordre qui compte (ce qu'on touche en haut, ce qui porte en bas), et une phrase à dire sur chacune : interface / services / données, essence / valeur / vision, produit / méthode / recherche.",
"avoid_when": "Les éléments sont de même rang (une pile dit une hiérarchie, même quand on ne la pense pas), leur nombre dépasse cinq (les losanges du bas s'effacent avant d'être comptés), ou une grandeur doit se lire : ici rien n'est proportionnel à rien — c'est un schéma, pas un graphique.",
"media": [
"slide",
"web"
],
"tags": [
"schema",
"diagramme",
"couches",
"layers",
"pile",
"empilement",
"isometrique",
"losange",
"architecture",
"stack",
"frontend",
"backend",
"hierarchie",
"filet",
"legende",
"opacite",
"profondeur"
],
"vars": [
{
"name": "--vl-coral",
"role": "la teinte UNIQUE des couches — c'est l'opacité, pas la couleur, qui distingue les rangs",
"default": "#E4886A"
},
{
"name": "--vl-layer-alpha-1",
"role": "opacité de la couche de tête (la seule pleine)",
"default": "1"
},
{
"name": "--vl-layer-alpha-2",
"role": "opacité de la deuxième couche",
"default": "0.44"
},
{
"name": "--vl-layer-alpha-3",
"role": "opacité de la troisième couche — même rapport à la deuxième que la deuxième à la première",
"default": "0.18"
},
{
"name": "--vl-ink",
"role": "le nom de la couche",
"default": "#3A3835"
},
{
"name": "--vl-ink-muted",
"role": "la ligne d'explication",
"default": "#6E6863"
},
{
"name": "--vl-hairline",
"role": "le filet de rappel entre un plan et son libellé",
"default": "#97918C"
},
{
"name": "--vl-track-title",
"role": "approche des titres en capitales",
"default": "0.13em"
},
{
"name": "--vl-track-body",
"role": "approche du corps en capitales",
"default": "0.05em"
}
],
"slots": [
"name",
"note"
],
"geometry": {
"root": ".vl-layers",
"frame": [
1040,
420
],
"ratios": {
"plane_h_over_w": 0.53,
"pitch_over_plane_h": 0.571,
"plane_w_over_width": 0.317,
"rule_start_over_plane_w": 0.655,
"rule_gap_over_width": 0.027,
"label_x_over_width": 0.413
},
"type_px": {
"name": 22,
"note": 14
},
"pad_ratio": {
"top": 0.0212,
"x": 0,
"bottom": 0.0212
}
},
"benchmarks": [
{
"name": "un plan est un carré en projection isométrique : deux fois plus large que haut",
"measure": "box('.vl-layers__plane--1').h / box('.vl-layers__plane--1').w",
"expect": 0.53,
"tol": 0.04
},
{
"name": "le pas est le même entre toutes les couches (une pile irrégulière se lit comme un accident)",
"measure": "Math.abs((box('.vl-layers__plane--2').top - box('.vl-layers__plane--1').top) - (box('.vl-layers__plane--3').top - box('.vl-layers__plane--2').top))",
"max": 1
},
{
"name": "le pas vaut 0,57 hauteur de plan : assez pour que les plans se distinguent, assez peu pour qu'ils fassent pile",
"measure": "(box('.vl-layers__plane--2').top - box('.vl-layers__plane--1').top) / box('.vl-layers__plane--1').h",
"expect": 0.571,
"tol": 0.05
},
{
"name": "le centre d'une couche échappe au plan qui la surplombe — sans quoi son filet ne peut pas en sortir",
"measure": "Math.min(...[[1,2],[2,3]].map(([a,b]) => (box('.vl-layers__plane--'+b).top + box('.vl-layers__plane--'+b).h/2) - box('.vl-layers__plane--'+a).bottom))",
"min": 2
},
{
"name": "l'opacité décroît d'un rapport CONSTANT d'une couche à la suivante (progression géométrique)",
"measure": "(num('.vl-layers__plane--2','opacity') / num('.vl-layers__plane--1','opacity')) / (num('.vl-layers__plane--3','opacity') / num('.vl-layers__plane--2','opacity'))",
"expect": 1,
"tol": 0.15
},
{
"name": "la couche de tête est la seule pleine",
"measure": "num('.vl-layers__plane--1','opacity')",
"expect": 1,
"tol": 0
},
{
"name": "la dernière couche est un fantôme : au moins 4 fois plus effacée que la première",
"measure": "num('.vl-layers__plane--1','opacity') / num('.vl-layers__plane--3','opacity')",
"min": 4
},
{
"name": "chaque filet est à la hauteur du centre de SA couche : la correspondance est géométrique, pas déduite de l'ordre",
"measure": "Math.max(...[1,2,3].map(i => Math.abs(box('.vl-layers__row--'+i+' .vl-layers__rule').top - (box('.vl-layers__plane--'+i).top + box('.vl-layers__plane--'+i).h/2))))",
"max": 2
},
{
"name": "chaque filet touche le milieu de la ligne de titre, pas l'interligne sous elle",
"measure": "Math.max(...[1,2,3].map(i => Math.abs(box('.vl-layers__row--'+i+' .vl-layers__rule').top - (box('.vl-layers__row--'+i+' .vl-layers__name').top + box('.vl-layers__row--'+i+' .vl-layers__name').h/2))))",
"max": 2
},
{
"name": "le filet naît SOUS le losange (à 0,655 de sa largeur) : sa longueur visible est dictée par la forme",
"measure": "(box('.vl-layers__row--1 .vl-layers__rule').left - box('.vl-layers__plane--1').left) / box('.vl-layers__plane--1').w",
"expect": 0.655,
"tol": 0.05
},
{
"name": "le filet s'arrête avant le texte : il désigne, il ne souligne pas",
"measure": "(box('.vl-layers__row--1 .vl-layers__name').left - box('.vl-layers__row--1 .vl-layers__rule').right) / W",
"expect": 0.027,
"tol": 0.008
},
{
"name": "la colonne de libellés et la pile occupent la même bande verticale",
"measure": "Math.abs((box('.vl-layers__row--1').top + box('.vl-layers__row--3').bottom) / 2 - (box('.vl-layers__plane--1').top + box('.vl-layers__plane--3').bottom) / 2) / H",
"max": 0.05
},
{
"name": "plancher de lisibilité : le corps ne descend pas sous 1,2 % de la largeur du schéma",
"measure": "num('.vl-layers__note','fontSize') / W",
"min": 0.012
},
{
"name": "contraste de la ligne d'explication sur le fond ≥ 4,5",
"measure": "contrast('.vl-layers__note')",
"min": 4.5
},
{
"name": "aucun débordement hors du cadre du schéma",
"measure": "overflow('.vl-layers')",
"max": 0
}
],
"pptx": {
"primitive": "diamond_stack",
"emitter": "kit/vl_pptx.py:layer_stack",
"pattern": "diagram-01-layer-stack",
"note": "Le losange est un `MSO_SHAPE.DIAMOND` — pour une fois la forme native de PowerPoint EST exactement le `clip-path` du fragment, elle n'a aucun ajustement qui dérive avec la proportion (contrairement au snip du chanfrein, cf. `notched_rect`). Le point dur est ailleurs : python-pptx n'expose AUCUNE transparence, or ici l'opacité N'EST PAS un effet mais l'information elle-même — sans elle les trois plans sortent identiques et le rang disparaît. Elle s'écrit donc dans le XML du remplissage (`<a:alpha>`), et le moteur de rendu composite les recouvrements comme le navigateur. La hauteur n'est pas un argument de l'émetteur : elle se déduit du nombre de couches et du pas, sinon on pourrait écraser la pile depuis l'appel."
},
"notes": "Le pas à 0,57 hauteur de plan n'est pas un goût : sous 0,5, le centre d'une couche passe sous le plan qui la surplombe et son filet ne peut plus en sortir — le schéma perd sa correspondance avant de perdre sa lisibilité, et rien ne le signale. C'est le benchmark « le centre échappe au plan du dessus » qui tient ce seuil. L'opacité suit une progression GÉOMÉTRIQUE (× 0,42) et non une soustraction régulière : à pas soustractif constant, la troisième couche reste trop dense et la pile paraît plate. Les recouvrements ne sont jamais dessinés — ils sont composités par le navigateur, et c'est ce qui rend le schéma thémable en ne changeant qu'UNE couleur. Écart assumé avec la référence : elle laisse les filets DÉRIVER (les blocs de texte coulent avec un écart constant, la correspondance repose sur l'ordre de lecture) ; ici chaque filet est verrouillé sur le centre de sa couche, parce que « c'est le troisième donc c'est celui du bas » n'est pas une correspondance dès que le schéma est technique. Pour passer à 4 ou 5 couches : un plan et une rangée de plus avec le `--i` suivant, un alpha qui poursuit la même raison géométrique, et une hauteur de cadre = hauteur de plan + (n − 1) × pas + 2 × padding.",
"id": "diagram-01-layer-stack"
}
À côté
diagram-03-time-scrubber