diagram-02-branch-timeline

Timeline à branches en équerre

Accrocher des objets de niveaux différents à une chronologie sans jamais tracer une diagonale : un rail horizontal daté, des jalons ronds accentués posés DESSUS, et des dérivations en équerre arrondie — descente verticale puis départ horizontal — qui mènent aux objets de la période. La diagonale dirait « transition » ; l'équerre dit « appartenance », et c'est la seule chose que ce diagramme a à dire.

diagramwebslide1330×300px18 benchmarks

Télécharger le .zip

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

Historique d'un dossier où chaque période porte plusieurs objets hétérogènes (un traitement, une mesure, un examen) : la position sur le rail donne la date, l'équerre donne le rattachement, et la taille de l'objet dérivé donne son rang. Deux à quatre branches par vue.

Éviter quand

Les objets sont tous de même nature et de même rang (une simple liste datée est plus lisible), les dates doivent être proportionnelles (rien ici n'est à l'échelle du temps : c'est une structure, pas un graphique), ou il y a plus de cinq branches — les équerres se croisent et l'appartenance redevient illisible.

Notes

Trois assertions portent l'idée, et elles sont écrites sur le TRACÉ SVG lui-même plutôt que sur des boîtes : l'équerre descend avant de partir à l'horizontale (`v` avant `h`), elle ne contient aucun segment oblique (`l`/`L` interdits), et son coude est un arc (`a`). Une mesure de boîtes n'aurait rien vu — une diagonale reliant les deux mêmes points a exactement la même boîte englobante que l'équerre. C'est le cas d'école où la géométrie mesurable est dans la donnée, pas dans le rendu. Rien n'est proportionnel au temps ici, et c'est assumé : la position d'un jalon dit l'ORDRE, pas la durée. C'est ce qui range ce pattern dans `diagram` et non dans `chart` — un `chart` laisserait une valeur mesurée décider de la géométrie, ce qu'aucune mesure ne fait dans ce fragment. Le jalon d'action (ajouter une période) est le seul objet sombre du diagramme, et le benchmark le compte : deux jalons sombres rendraient impossible de distinguer une période d'une commande.

Mots-clés

timelinechronologiebrancheequerrerailjalonhistoriquedossierstructurederivationdiagramme

Benchmarks — la géométrie, en assertions vérifiables

Rejouables par node bin/check.mjs diagram-02-branch-timeline : le pattern se prouve, il ne se croit pas.

assertionmesureattendu
le rail est un FILET horizontal, pas une barrebox('.vl-branchline__rail').h≤ 3
… et il traverse la vue : au moins 90 % de la largeurbox('.vl-branchline__rail').w / W≥ 0.9
les jalons sont posés SUR le rail : leur centre coïncide avec lui(() => { const r = box('.vl-branchline__rail'); return Math.max(...[...document.querySelectorAll('.vl-branchline__pin')].map((e) => { const b = e.getBoundingClientRect(); const c = b.top + b.height / 2 - (window.scrollY || 0); return Math.abs((c - $(window.__VL_ROOT__).getBoundingClientRect().top) - (r.top + r.h / 2)); })); })()≤ 2.5
un jalon est un disquebox('.vl-branchline__pin').w / box('.vl-branchline__pin').h1 ± 0.02
les jalons de période portent l'accent — ce qu'il faut compter(() => { const c = parseRgb(cs('.vl-branchline__pin','backgroundColor')); return Math.max(...c) - Math.min(...c); })()≥ 60
UN SEUL jalon d'action, et il est sombre[...document.querySelectorAll('.vl-branchline__pin')].filter((e) => lum(parseRgb(getComputedStyle(e).backgroundColor)) < 0.2).length1 ± 0
L'ÉQUERRE DESCEND D'ABORD, PART À L'HORIZONTALE ENSUITE(() => { const d = $('.vl-branchline__elbow path').getAttribute('d'); return /v/.test(d) && /h/.test(d) && d.indexOf('v') < d.indexOf('h') ? 1 : 0; })()1
… et JAMAIS en diagonale : aucun segment oblique dans le tracé/[lL]/.test($('.vl-branchline__elbow path').getAttribute('d')) ? 1 : 00 ± 0
le coude est ARRONDI : le tracé porte un arc/[aA]/.test($('.vl-branchline__elbow path').getAttribute('d')) ? 1 : 01
l'équerre part du jalon : leurs abscisses coïncident à quelques pixels prèsMath.abs((box('.vl-branchline__elbow').left + 12) - (box('.vl-branchline__pin').left + box('.vl-branchline__pin').w / 2))≤ 14
tout ce qui est dérivé est SOUS le rail(() => { const r = box('.vl-branchline__rail'); return Math.min(...[...document.querySelectorAll('.vl-branchline__obj')].map((e) => box('#' + e.id).top)) - (r.top + r.h); })()≥ 20
l'objet dérivé commence APRÈS le départ horizontal de son équerrebox('#obj-a').left - box('.vl-branchline__elbow').right≥ -4
l'objet de premier rang est plus large que celui de second rangbox('#obj-b').w / box('#obj-a').w≥ 1.3
le sous-libellé est collé sous le libellé de période(box('.vl-branchline__week').top - box('.vl-branchline__month').bottom) / num('.vl-branchline__week','fontSize')≤ 0.6
contraste du libellé de période ≥ 4,5contrast('.vl-branchline__month')≥ 4.5
contraste du sous-libellé ≥ 4,5contrast('.vl-branchline__week')≥ 4.5
les objets dérivés sont PLUS CLAIRS que le sol qui les portelum(parseRgb(cs('#obj-b','backgroundColor'))) - lum(parseRgb(cs('.vl-branchline','backgroundColor')))≥ 0.05
aucun débordementoverflow('.vl-branchline')≤ 0

Le code

fragment — patterns/diagram-02-branch-timeline.html Télécharger
<!-- diagram-02-branch-timeline — une chronologie qui accroche des objets de rangs différents.
     Couleurs via --vl-* uniquement.

     AUCUNE DIAGONALE. La dérivation est une ÉQUERRE ARRONDIE : descente verticale, coude en
     arc, puis départ horizontal. Une diagonale reliant les deux mêmes points aurait la même
     boîte englobante — c'est pourquoi les benchmarks lisent le TRACÉ SVG (`v` avant `h`,
     aucun `l`, un `a`) et non des positions de boîtes.

     Rien n'est proportionnel au temps : la position d'un jalon dit l'ORDRE, pas la durée.
     C'est ce qui range ce fragment dans `diagram` et non dans `chart`. -->
<div class="vl-branchline">

  <span class="vl-branchline__rail"></span>

  <!-- branche A -->
  <span class="vl-branchline__pin" style="left:18px">
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"><rect x="5" y="4.5" width="14" height="16" rx="2.4"/><path d="M8.5 9h7M8.5 13h7M9 2.6v3.6M15 2.6v3.6"/></svg>
  </span>
  <div class="vl-branchline__label" style="left:18px">
    <p class="vl-branchline__month">Aug</p>
    <p class="vl-branchline__week">I Week</p>
  </div>
  <svg class="vl-branchline__elbow" style="left:18px" viewBox="0 0 120 96" aria-hidden="true">
    <path d="M12 0v66a14 14 0 0 0 14 14h94"/>
  </svg>

  <span class="vl-branchline__obj vl-branchline__pill" id="obj-a" style="left:150px; top:126px">
    <span class="vl-branchline__well">
      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"><rect x="3.4" y="8.6" width="17" height="7" rx="3.5" transform="rotate(-45 12 12)"/><path d="m9.5 9.5 5 5"/></svg>
    </span>
    Aspirin <b>x2</b>
  </span>

  <article class="vl-branchline__obj vl-branchline__card" id="obj-b" style="left:360px; top:104px">
    <h4>Blood Pressure</h4>
    <p>Friday · average 160/110</p>
  </article>

  <!-- branche B -->
  <span class="vl-branchline__pin" style="left:700px">
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"><rect x="5" y="4.5" width="14" height="16" rx="2.4"/><path d="M8.5 9h7M8.5 13h7M9 2.6v3.6M15 2.6v3.6"/></svg>
  </span>
  <div class="vl-branchline__label" style="left:700px">
    <p class="vl-branchline__month">Sep</p>
    <p class="vl-branchline__week">I Week</p>
  </div>
  <svg class="vl-branchline__elbow" style="left:700px" viewBox="0 0 120 96" aria-hidden="true">
    <path d="M12 0v66a14 14 0 0 0 14 14h94"/>
  </svg>

  <span class="vl-branchline__obj vl-branchline__pill" id="obj-c" style="left:832px; top:126px">
    <span class="vl-branchline__well">
      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"><rect x="3.4" y="8.6" width="17" height="7" rx="3.5" transform="rotate(-45 12 12)"/><path d="m9.5 9.5 5 5"/></svg>
    </span>
    bisoprolol <b>x3</b>
  </span>

  <article class="vl-branchline__obj vl-branchline__card" id="obj-d" style="left:1010px; top:104px">
    <h4>Blood Pressure</h4>
    <p>Thursday · average 130/90</p>
  </article>

  <!-- le SEUL jalon sombre : c'est une commande, pas une période -->
  <span class="vl-branchline__pin vl-branchline__pin--add" style="right:0">
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M12 6v12M6 12h12"/></svg>
  </span>

</div>

<style>
.vl-branchline {
  width: 1330px; height: 300px;
  box-sizing: border-box;
  position: relative;
  /* Le fond de feuille fait partie du pattern : les objets dérivés sont BLANCS, et un blanc
     sur blanc ne monte pas. C'est la même règle de rang que partout ailleurs — le sol est
     gris pour que ce qu'on y pose existe sans liseré. */
  background: var(--vl-sheet);
  padding: 0 0 0 0;
  color: var(--vl-ink);
  font-family: Montserrat, Poppins, Inter, "Helvetica Neue", Arial, sans-serif;
}
/* Un FILET, pas une barre : le rail porte la chronologie, il ne la représente pas. */
.vl-branchline__rail {
  position: absolute; left: 40px; right: 60px; top: 22px; height: 2px;
  background: linear-gradient(90deg, var(--vl-alert) 0%, var(--vl-ink-soft) 46%, var(--vl-recover) 100%);
}
/* Posé SUR le rail, centré dessus : un jalon décalé de quelques pixels se met à flotter et la
   chronologie cesse de se lire comme un axe. */
.vl-branchline__pin {
  position: absolute; top: 0;
  width: 46px; height: 46px; border-radius: 50%;
  background: var(--vl-accent); color: var(--vl-ink);
  display: flex; align-items: center; justify-content: center;
}
.vl-branchline__pin svg { width: 20px; height: 20px; }
.vl-branchline__pin--add { background: var(--vl-veil-deep); color: var(--vl-module-hi); }

.vl-branchline__label { position: absolute; top: 46px; }
.vl-branchline__month { margin: 26px 0 0; font-size: 22px; font-weight: 500; }
.vl-branchline__week { margin: 2px 0 0; font-size: 13px; color: var(--vl-ink-soft); }

/* L'ÉQUERRE. `v` d'abord, `a` pour le coude, `h` ensuite — dans cet ordre et sans un seul
   segment oblique. C'est le tracé qui porte le sens : « ceci appartient à cette période »,
   là où une diagonale dirait « ceci mène à cela ». */
.vl-branchline__elbow { position: absolute; top: 46px; width: 120px; height: 96px; }
.vl-branchline__elbow path { fill: none; stroke: var(--vl-ink-soft); stroke-width: 1.6; opacity: 0.55; }

.vl-branchline__pill {
  position: absolute;
  height: 54px; padding: 0 20px 0 6px;
  border-radius: var(--vl-radius-pill, 999px);
  background: var(--vl-module);
  display: flex; align-items: center; gap: 12px;
  font-size: 17px; font-weight: 500; white-space: nowrap;
}
.vl-branchline__well {
  width: 42px; height: 42px; border-radius: 50%;
  background: var(--vl-module-hi);
  display: flex; align-items: center; justify-content: center;
}
.vl-branchline__well svg { width: 19px; height: 19px; }
.vl-branchline__pill b { font-weight: 600; }
/* Le rang de l'objet dérivé se lit à sa TAILLE et à son aplat : la carte de premier rang est
   plus large et plus claire que la pilule de second rang. */
.vl-branchline__card {
  position: absolute;
  width: 300px; margin: 0; padding: 18px 22px;
  background: var(--vl-module-hi);
  border-radius: var(--vl-radius-module, 22px);
}
.vl-branchline__card h4 { margin: 0; font-size: 24px; font-weight: 500; letter-spacing: -0.01em; }
.vl-branchline__card p { margin: 6px 0 0; font-size: 15px; color: var(--vl-ink-soft); }
</style>
tokens de la charte — systems/ref-19-acid-clinical-timeline.json Télécharger
:root {
  --vl-veil: #4A4A48;
  --vl-veil-deep: #1B1D1D;
  --vl-sheet: #DFDFD7;
  --vl-module: #F0EFEB;
  --vl-module-hi: #FFFFFF;
  --vl-accent: #F5FE49;
  --vl-alert: #E8756B;
  --vl-recover: #7FA88C;
  --vl-ink: #262C2C;
  --vl-ink-soft: #5F615E;
  --vl-on-veil: #FFFFFF;
  --vl-on-veil-soft: rgba(255,255,255,0.72);
  --vl-radius-sheet: 34px;
  --vl-radius-module: 22px;
  --vl-radius-pill: 999px;
  --vl-notch: 26px;
  --vl-bleed: 20px;
  --vl-tabbar-h: 106px;
  --vl-ribbon-h: 74px;
  --vl-gutter: 18px;
  --vl-margin: 34px;
}
les métadonnées — patterns/diagram-02-branch-timeline.json (télécharger)
{
  "name": "Timeline à branches en équerre",
  "family": "diagram",
  "ref": "ref-19-acid-clinical-timeline",
  "intent": "Accrocher des objets de niveaux différents à une chronologie sans jamais tracer une diagonale : un rail horizontal daté, des jalons ronds accentués posés DESSUS, et des dérivations en équerre arrondie — descente verticale puis départ horizontal — qui mènent aux objets de la période. La diagonale dirait « transition » ; l'équerre dit « appartenance », et c'est la seule chose que ce diagramme a à dire.",
  "when_to_use": "Historique d'un dossier où chaque période porte plusieurs objets hétérogènes (un traitement, une mesure, un examen) : la position sur le rail donne la date, l'équerre donne le rattachement, et la taille de l'objet dérivé donne son rang. Deux à quatre branches par vue.",
  "avoid_when": "Les objets sont tous de même nature et de même rang (une simple liste datée est plus lisible), les dates doivent être proportionnelles (rien ici n'est à l'échelle du temps : c'est une structure, pas un graphique), ou il y a plus de cinq branches — les équerres se croisent et l'appartenance redevient illisible.",
  "media": [
    "web",
    "slide"
  ],
  "tags": [
    "timeline",
    "chronologie",
    "branche",
    "equerre",
    "rail",
    "jalon",
    "historique",
    "dossier",
    "structure",
    "derivation",
    "diagramme"
  ],
  "vars": [
    {
      "name": "--vl-sheet",
      "role": "le SOL du diagramme — gris, pour que les objets blancs qu'on y pose montent sans liseré",
      "default": "#DFDFD7"
    },
    {
      "name": "--vl-accent",
      "role": "aplat des jalons — ce qu'il faut compter, jamais ce qui va mal",
      "default": "#F5FE49"
    },
    {
      "name": "--vl-veil-deep",
      "role": "aplat du seul jalon d'action (ajouter)",
      "default": "#1B1D1D"
    },
    {
      "name": "--vl-ink",
      "role": "libellé de période",
      "default": "#262C2C"
    },
    {
      "name": "--vl-ink-soft",
      "role": "sous-libellé, tracé des équerres",
      "default": "#5F615E"
    },
    {
      "name": "--vl-module",
      "role": "aplat d'un objet dérivé de second rang (pilule)",
      "default": "#F0EFEB"
    },
    {
      "name": "--vl-module-hi",
      "role": "aplat d'un objet dérivé de premier rang (carte)",
      "default": "#FFFFFF"
    },
    {
      "name": "--vl-recover",
      "role": "extrémité « amélioration » du dégradé de rail — le pendant de --vl-alert",
      "default": "#7FA88C"
    },
    {
      "name": "--vl-alert",
      "role": "extrémité « dégradation » du dégradé de rail",
      "default": "#E8756B"
    }
  ],
  "slots": [
    "rail",
    "pins",
    "branches",
    "objects"
  ],
  "geometry": {
    "root": ".vl-branchline",
    "frame": [
      1330,
      300
    ],
    "ratios": {
      "rail_thickness_over_height": 0.0067,
      "pin_over_height": 0.153,
      "elbow_drop_over_height": 0.32,
      "elbow_radius_over_drop": 0.146
    },
    "type_px": {
      "month": 22,
      "week": 13
    },
    "pad_ratio": {
      "top": 0,
      "x": 0,
      "bottom": 0
    }
  },
  "benchmarks": [
    {
      "name": "le rail est un FILET horizontal, pas une barre",
      "measure": "box('.vl-branchline__rail').h",
      "max": 3
    },
    {
      "name": "… et il traverse la vue : au moins 90 % de la largeur",
      "measure": "box('.vl-branchline__rail').w / W",
      "min": 0.9
    },
    {
      "name": "les jalons sont posés SUR le rail : leur centre coïncide avec lui",
      "measure": "(() => { const r = box('.vl-branchline__rail'); return Math.max(...[...document.querySelectorAll('.vl-branchline__pin')].map((e) => { const b = e.getBoundingClientRect(); const c = b.top + b.height / 2 - (window.scrollY || 0); return Math.abs((c - $(window.__VL_ROOT__).getBoundingClientRect().top) - (r.top + r.h / 2)); })); })()",
      "max": 2.5
    },
    {
      "name": "un jalon est un disque",
      "measure": "box('.vl-branchline__pin').w / box('.vl-branchline__pin').h",
      "expect": 1,
      "tol": 0.02
    },
    {
      "name": "les jalons de période portent l'accent — ce qu'il faut compter",
      "measure": "(() => { const c = parseRgb(cs('.vl-branchline__pin','backgroundColor')); return Math.max(...c) - Math.min(...c); })()",
      "min": 60
    },
    {
      "name": "UN SEUL jalon d'action, et il est sombre",
      "measure": "[...document.querySelectorAll('.vl-branchline__pin')].filter((e) => lum(parseRgb(getComputedStyle(e).backgroundColor)) < 0.2).length",
      "expect": 1,
      "tol": 0
    },
    {
      "name": "L'ÉQUERRE DESCEND D'ABORD, PART À L'HORIZONTALE ENSUITE",
      "measure": "(() => { const d = $('.vl-branchline__elbow path').getAttribute('d'); return /v/.test(d) && /h/.test(d) && d.indexOf('v') < d.indexOf('h') ? 1 : 0; })()",
      "expect": 1
    },
    {
      "name": "… et JAMAIS en diagonale : aucun segment oblique dans le tracé",
      "measure": "/[lL]/.test($('.vl-branchline__elbow path').getAttribute('d')) ? 1 : 0",
      "expect": 0,
      "tol": 0
    },
    {
      "name": "le coude est ARRONDI : le tracé porte un arc",
      "measure": "/[aA]/.test($('.vl-branchline__elbow path').getAttribute('d')) ? 1 : 0",
      "expect": 1
    },
    {
      "name": "l'équerre part du jalon : leurs abscisses coïncident à quelques pixels près",
      "measure": "Math.abs((box('.vl-branchline__elbow').left + 12) - (box('.vl-branchline__pin').left + box('.vl-branchline__pin').w / 2))",
      "max": 14
    },
    {
      "name": "tout ce qui est dérivé est SOUS le rail",
      "measure": "(() => { const r = box('.vl-branchline__rail'); return Math.min(...[...document.querySelectorAll('.vl-branchline__obj')].map((e) => box('#' + e.id).top)) - (r.top + r.h); })()",
      "min": 20
    },
    {
      "name": "l'objet dérivé commence APRÈS le départ horizontal de son équerre",
      "measure": "box('#obj-a').left - box('.vl-branchline__elbow').right",
      "min": -4
    },
    {
      "name": "l'objet de premier rang est plus large que celui de second rang",
      "measure": "box('#obj-b').w / box('#obj-a').w",
      "min": 1.3
    },
    {
      "name": "le sous-libellé est collé sous le libellé de période",
      "measure": "(box('.vl-branchline__week').top - box('.vl-branchline__month').bottom) / num('.vl-branchline__week','fontSize')",
      "max": 0.6
    },
    {
      "name": "contraste du libellé de période ≥ 4,5",
      "measure": "contrast('.vl-branchline__month')",
      "min": 4.5
    },
    {
      "name": "contraste du sous-libellé ≥ 4,5",
      "measure": "contrast('.vl-branchline__week')",
      "min": 4.5
    },
    {
      "name": "les objets dérivés sont PLUS CLAIRS que le sol qui les porte",
      "measure": "lum(parseRgb(cs('#obj-b','backgroundColor'))) - lum(parseRgb(cs('.vl-branchline','backgroundColor')))",
      "min": 0.05
    },
    {
      "name": "aucun débordement",
      "measure": "overflow('.vl-branchline')",
      "max": 0
    }
  ],
  "notes": "Trois assertions portent l'idée, et elles sont écrites sur le TRACÉ SVG lui-même plutôt que sur des boîtes : l'équerre descend avant de partir à l'horizontale (`v` avant `h`), elle ne contient aucun segment oblique (`l`/`L` interdits), et son coude est un arc (`a`). Une mesure de boîtes n'aurait rien vu — une diagonale reliant les deux mêmes points a exactement la même boîte englobante que l'équerre. C'est le cas d'école où la géométrie mesurable est dans la donnée, pas dans le rendu.\n\nRien n'est proportionnel au temps ici, et c'est assumé : la position d'un jalon dit l'ORDRE, pas la durée. C'est ce qui range ce pattern dans `diagram` et non dans `chart` — un `chart` laisserait une valeur mesurée décider de la géométrie, ce qu'aucune mesure ne fait dans ce fragment.\n\nLe jalon d'action (ajouter une période) est le seul objet sombre du diagramme, et le benchmark le compte : deux jalons sombres rendraient impossible de distinguer une période d'une commande.",
  "id": "diagram-02-branch-timeline"
}

À côté