chart-01-stadium-bars
chart-04-unit-textured-bar
Barre à unité déclarée
Donner à un histogramme une UNITÉ visible sans lui faire porter le comptage : la hauteur de la barre est exactement proportionnelle à la valeur, la texture qui la remplit est un peigne dont la période vaut une unité déclarée, et le montant exact est écrit au-dessus — la texture donne l'échelle, elle ne compte jamais à la place du chiffre.
chartprintslidesocialweb704×620px9 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
Sur une comparaison de deux à quatre montants dont l'écart EST le message (coût cumulé, budget, dette), quand le lecteur doit sentir l'ordre de grandeur avant de lire les chiffres. Excellent sur une charte à deux couleurs, où la texture remplace l'aplat plein sans rien coûter en palette.
Éviter quand
Au-delà de quatre barres — le peigne devient du bruit moiré ; sur une série temporelle, où l'unité de l'axe suffit ; ou quand la plus petite valeur descend sous deux périodes, la texture disparaissant alors dans un trait plein qui MENT sur l'échelle (un benchmark le refuse).
Notes
POURQUOI CE PATTERN EXISTE : un histogramme nu donne un rapport mais aucune échelle — le lecteur voit « trois fois plus » sans savoir trois fois plus DE QUOI. Un isotype donne l'échelle mais impose de compter, et se déforme dès que les valeurs dépassent une trentaine d'unités. Ici les trois rôles sont séparés : la HAUTEUR porte le rapport, la TEXTURE porte l'unité, le TEXTE porte la valeur exacte. Aucun des trois ne fait le travail d'un autre, et c'est ce qui rend le graphique inattaquable. LE PIÈGE MESURÉ : laisser la texture décider de la hauteur. Si on arrondit la barre à un nombre entier de crans, la hauteur cesse d'être proportionnelle et le graphique devient faux d'un demi-cran — sur une petite barre, jusqu'à 20 %. La hauteur est donc calculée depuis la valeur, la texture répétée par-dessus, et le dernier cran du haut est TRONQUÉ. C'est correct : le cran partiel dit exactement ce qu'il vaut. LE PEIGNE SE TIRE DU BAS (`repeating-linear-gradient(to top, …)`). Tiré du haut, la phase change avec la hauteur de chaque barre et les crans ne s'alignent plus d'une barre à l'autre : le lecteur y lit une erreur de rendu avant d'y lire une unité. SEUIL DE DEUX PÉRIODES : sous deux crans, la barre lit comme un aplat plein et l'unité qu'elle prétend porter n'est plus visible. Un benchmark refuse ce cas — la réponse est de changer l'unité déclarée, jamais d'étirer la barre.
Mots-clés
Benchmarks — la géométrie, en assertions vérifiables
Rejouables par node bin/check.mjs chart-04-unit-textured-bar : le pattern se prouve, il ne se croit pas.
| assertion | mesure | attendu |
|---|---|---|
| LE CONTRAT — le rapport des hauteurs EST le rapport des valeurs | (box('.vl-utb__col:nth-child(2) .vl-utb__bar').h / box('.vl-utb__col:nth-child(1) .vl-utb__bar').h) / (Number($('.vl-utb__col:nth-child(2) .vl-utb__bar').dataset.value) / Number($('.vl-utb__col:nth-child(1) .vl-utb__bar').dataset.value)) | 1 ± 0.01 |
| le contrat tient aussi sur la plus petite barre | (box('.vl-utb__col:nth-child(3) .vl-utb__bar').h / box('.vl-utb__col:nth-child(1) .vl-utb__bar').h) / (Number($('.vl-utb__col:nth-child(3) .vl-utb__bar').dataset.value) / Number($('.vl-utb__col:nth-child(1) .vl-utb__bar').dataset.value)) | 1 ± 0.01 |
| la période du peigne vaut EXACTEMENT l'unité déclarée | parseFloat(getComputedStyle($('.vl-utb')).getPropertyValue('--vl-utb-period')) / (Number($('.vl-utb').dataset.unit) * Number($('.vl-utb').dataset.scaleH) / Number($('.vl-utb').dataset.scaleV)) | 1 ± 0.02 |
| la plus petite barre garde au moins DEUX périodes — sinon la texture ment | box('.vl-utb__col:nth-child(3) .vl-utb__bar').h / parseFloat(getComputedStyle($('.vl-utb')).getPropertyValue('--vl-utb-period')) | ≥ 2 |
| chaque barre porte son montant écrit | [1,2,3].every((i) => text('.vl-utb__col:nth-child(' + i + ') .vl-utb__value').length > 0) ? 1 : 0 | 1 ± 0 |
| le montant est AU-DESSUS de la barre, jamais dedans | box('.vl-utb__col:nth-child(1) .vl-utb__value').bottom <= box('.vl-utb__col:nth-child(1) .vl-utb__bar').top ? 1 : 0 | 1 ± 0 |
| les barres reposent sur UNE ligne de base commune | Math.max(box('.vl-utb__col:nth-child(1) .vl-utb__bar').bottom, box('.vl-utb__col:nth-child(2) .vl-utb__bar').bottom, box('.vl-utb__col:nth-child(3) .vl-utb__bar').bottom) - Math.min(box('.vl-utb__col:nth-child(1) .vl-utb__bar').bottom, box('.vl-utb__col:nth-child(2) .vl-utb__bar').bottom, box('.vl-utb__col:nth-child(3) .vl-utb__bar').bottom) | 0 ± 0.5 |
| la phrase d'unité est présente — c'est elle qui rend le peigne lisible | text('.vl-utb__unit').length > 12 ? 1 : 0 | 1 ± 0 |
| le montant écrit domine son libellé d'un cran franc | num('.vl-utb__value','fontSize') / num('.vl-utb__label','fontSize') | 2.14 ± 0.25 |
Le code
patterns/chart-04-unit-textured-bar.html
Télécharger<!-- chart-04-unit-textured-bar — fragment autonome. Couleurs via --vl-* uniquement, aucune valeur en dur. -->
<div class="vl-utb" data-unit="3000" data-scale-h="400" data-scale-v="84000">
<div class="vl-utb__plot">
<div class="vl-utb__col">
<div class="vl-utb__value">84 000 €</div>
<div class="vl-utb__bar" data-value="84000" style="height:400px"></div>
</div>
<div class="vl-utb__col">
<div class="vl-utb__value">45 000 €</div>
<div class="vl-utb__bar" data-value="45000" style="height:214.3px"></div>
</div>
<div class="vl-utb__col">
<div class="vl-utb__value">12 000 €</div>
<div class="vl-utb__bar is-muted" data-value="12000" style="height:57.1px"></div>
</div>
</div>
<div class="vl-utb__foot">
<div class="vl-utb__label">option A</div>
<div class="vl-utb__label">option B</div>
<div class="vl-utb__label">northbeam</div>
</div>
<div class="vl-utb__unit">une case = 3 000 € · hauteur exactement proportionnelle</div>
</div>
<style>
/* LA HAUTEUR PORTE LA VALEUR, LA TEXTURE PORTE L'UNITÉ — jamais l'inverse.
La barre est mise à l'échelle par sa hauteur ; la texture, elle, est un
PEIGNE de période constante qui dit ce que vaut un cran. Compter les crans
doit redonner la valeur, mais personne n'a à le faire : le montant est écrit
au-dessus. C'est ce qui sépare ce pattern d'un isotype (où compter EST la
lecture) et d'un histogramme nu (où l'unité reste implicite). */
.vl-utb{
--vl-utb-period:14.286px; /* 3 000 € ramenés à l'échelle 400 px / 84 000 € */
width:704px;
font-family:"Hanken Grotesk",Inter,system-ui,sans-serif;
color:var(--vl-near);
}
.vl-utb__plot{
display:grid;grid-template-columns:repeat(3,1fr);gap:56px;
align-items:end;
border-bottom:var(--vl-rule) solid var(--vl-ink);
}
.vl-utb__col{display:flex;flex-direction:column;justify-content:flex-end}
.vl-utb__value{
font-family:Newsreader,Georgia,serif;
font-size:47px;line-height:1.12;text-align:center;
margin-bottom:14px;
color:var(--vl-near);
}
/* Le peigne est tiré DU BAS : c'est le pied de la barre qui est calé sur la
ligne de base, donc c'est de là que les crans doivent partir. Un peigne tiré
du haut décale la phase d'une barre à l'autre et les crans cessent de
s'aligner horizontalement — le lecteur y voit une erreur avant d'y voir une
unité. */
.vl-utb__bar{
width:100%;
box-shadow:inset 0 0 0 var(--vl-rule) var(--vl-ink);
background:repeating-linear-gradient(
to top,
var(--vl-ink) 0 calc(var(--vl-utb-period) - 1.8px),
var(--vl-paper) calc(var(--vl-utb-period) - 1.8px) var(--vl-utb-period));
}
.vl-utb__bar.is-muted{
background:repeating-linear-gradient(
to top,
var(--vl-ink-faint) 0 calc(var(--vl-utb-period) - 1.8px),
var(--vl-paper) calc(var(--vl-utb-period) - 1.8px) var(--vl-utb-period));
}
.vl-utb__foot{
display:grid;grid-template-columns:repeat(3,1fr);gap:56px;
margin-top:20px;
}
.vl-utb__label{
font-family:"DM Mono",ui-monospace,monospace;
font-size:22px;line-height:1.42;color:var(--vl-ink);
text-align:center;white-space:nowrap;
}
.vl-utb__unit{
margin-top:26px;padding-top:18px;
border-top:1px solid var(--vl-ink-faint);
font-family:"DM Mono",ui-monospace,monospace;
font-size:22px;line-height:1.42;letter-spacing:.04em;
color:var(--vl-ink);text-transform:uppercase;
}
</style>
systems/ref-16-cobalt-graph-paper.json
Télécharger:root {
--vl-paper: #F0EBDE;
--vl-paper-2: #E6E0CE;
--vl-ink: #1F2BE0;
--vl-ink-faint: rgba(31,43,224,0.20);
--vl-grid: rgba(31,43,224,0.10);
--vl-near: #14163A;
--vl-grid-step: 35.2px;
--vl-cell-pitch: 70.4px;
--vl-cell-gap: 6px;
--vl-rule: 1.25px;
--vl-margin: 92px;
}
les métadonnées — patterns/chart-04-unit-textured-bar.json (télécharger)
{
"name": "Barre à unité déclarée",
"family": "chart",
"ref": "ref-16-cobalt-graph-paper",
"intent": "Donner à un histogramme une UNITÉ visible sans lui faire porter le comptage : la hauteur de la barre est exactement proportionnelle à la valeur, la texture qui la remplit est un peigne dont la période vaut une unité déclarée, et le montant exact est écrit au-dessus — la texture donne l'échelle, elle ne compte jamais à la place du chiffre.",
"when_to_use": "Sur une comparaison de deux à quatre montants dont l'écart EST le message (coût cumulé, budget, dette), quand le lecteur doit sentir l'ordre de grandeur avant de lire les chiffres. Excellent sur une charte à deux couleurs, où la texture remplace l'aplat plein sans rien coûter en palette.",
"avoid_when": "Au-delà de quatre barres — le peigne devient du bruit moiré ; sur une série temporelle, où l'unité de l'axe suffit ; ou quand la plus petite valeur descend sous deux périodes, la texture disparaissant alors dans un trait plein qui MENT sur l'échelle (un benchmark le refuse).",
"media": [
"print",
"slide",
"social",
"web"
],
"tags": [
"histogramme",
"barres",
"unité",
"échelle",
"texture",
"peigne",
"coût",
"comparaison",
"cumulé",
"isotype",
"deux couleurs",
"monochrome"
],
"vars": [
{
"name": "--vl-utb-period",
"role": "période du peigne = UNE unité déclarée, à l'échelle du tracé",
"default": "14.286px"
},
{
"name": "--vl-ink",
"role": "encre des barres et du chrome",
"default": "#1F2BE0"
},
{
"name": "--vl-ink-faint",
"role": "barre atténuée — la valeur qu'on ne veut PAS voir dominer",
"default": "rgba(31,43,224,0.20)"
},
{
"name": "--vl-paper",
"role": "papier, et creux du peigne",
"default": "#F0EBDE"
},
{
"name": "--vl-near",
"role": "encre du montant écrit",
"default": "#14163A"
},
{
"name": "--vl-rule",
"role": "épaisseur de la ligne de base et du liseré",
"default": "1.25px"
}
],
"slots": [
{
"name": "value",
"role": "le montant EXACT, écrit au-dessus de chaque barre — jamais facultatif"
},
{
"name": "label",
"role": "l'identité de la barre, sur UNE ligne"
},
{
"name": "unit",
"role": "la phrase qui déclare l'unité et l'exactitude de l'échelle — c'est le contrat du pattern"
}
],
"geometry": {
"root": ".vl-utb",
"frame": [
704,
620
],
"ratios": {
"plot_height_over_width": 0.568,
"gap_over_width": 0.08,
"period_over_plot_height": 0.0357,
"value_over_label": 2.14
},
"type_px": {
"value": 47,
"label": 22,
"unit": 22
},
"pad_ratio": {
"top": 0,
"x": 0,
"bottom": 0
}
},
"benchmarks": [
{
"name": "LE CONTRAT — le rapport des hauteurs EST le rapport des valeurs",
"measure": "(box('.vl-utb__col:nth-child(2) .vl-utb__bar').h / box('.vl-utb__col:nth-child(1) .vl-utb__bar').h) / (Number($('.vl-utb__col:nth-child(2) .vl-utb__bar').dataset.value) / Number($('.vl-utb__col:nth-child(1) .vl-utb__bar').dataset.value))",
"expect": 1,
"tol": 0.01
},
{
"name": "le contrat tient aussi sur la plus petite barre",
"measure": "(box('.vl-utb__col:nth-child(3) .vl-utb__bar').h / box('.vl-utb__col:nth-child(1) .vl-utb__bar').h) / (Number($('.vl-utb__col:nth-child(3) .vl-utb__bar').dataset.value) / Number($('.vl-utb__col:nth-child(1) .vl-utb__bar').dataset.value))",
"expect": 1,
"tol": 0.01
},
{
"name": "la période du peigne vaut EXACTEMENT l'unité déclarée",
"measure": "parseFloat(getComputedStyle($('.vl-utb')).getPropertyValue('--vl-utb-period')) / (Number($('.vl-utb').dataset.unit) * Number($('.vl-utb').dataset.scaleH) / Number($('.vl-utb').dataset.scaleV))",
"expect": 1,
"tol": 0.02
},
{
"name": "la plus petite barre garde au moins DEUX périodes — sinon la texture ment",
"measure": "box('.vl-utb__col:nth-child(3) .vl-utb__bar').h / parseFloat(getComputedStyle($('.vl-utb')).getPropertyValue('--vl-utb-period'))",
"min": 2
},
{
"name": "chaque barre porte son montant écrit",
"measure": "[1,2,3].every((i) => text('.vl-utb__col:nth-child(' + i + ') .vl-utb__value').length > 0) ? 1 : 0",
"expect": 1,
"tol": 0
},
{
"name": "le montant est AU-DESSUS de la barre, jamais dedans",
"measure": "box('.vl-utb__col:nth-child(1) .vl-utb__value').bottom <= box('.vl-utb__col:nth-child(1) .vl-utb__bar').top ? 1 : 0",
"expect": 1,
"tol": 0
},
{
"name": "les barres reposent sur UNE ligne de base commune",
"measure": "Math.max(box('.vl-utb__col:nth-child(1) .vl-utb__bar').bottom, box('.vl-utb__col:nth-child(2) .vl-utb__bar').bottom, box('.vl-utb__col:nth-child(3) .vl-utb__bar').bottom) - Math.min(box('.vl-utb__col:nth-child(1) .vl-utb__bar').bottom, box('.vl-utb__col:nth-child(2) .vl-utb__bar').bottom, box('.vl-utb__col:nth-child(3) .vl-utb__bar').bottom)",
"expect": 0,
"tol": 0.5
},
{
"name": "la phrase d'unité est présente — c'est elle qui rend le peigne lisible",
"measure": "text('.vl-utb__unit').length > 12 ? 1 : 0",
"expect": 1,
"tol": 0
},
{
"name": "le montant écrit domine son libellé d'un cran franc",
"measure": "num('.vl-utb__value','fontSize') / num('.vl-utb__label','fontSize')",
"expect": 2.14,
"tol": 0.25
}
],
"pptx": {
"primitive": "textured_bar",
"note": "En .pptx le peigne se fait en remplissage à motif (pattern fill) à pas fixe, pas en N rectangles empilés : au-delà de ~30 crans le fichier devient illisible à l'édition, et c'est le fichier que le client ouvre."
},
"notes": "POURQUOI CE PATTERN EXISTE : un histogramme nu donne un rapport mais aucune échelle — le lecteur voit « trois fois plus » sans savoir trois fois plus DE QUOI. Un isotype donne l'échelle mais impose de compter, et se déforme dès que les valeurs dépassent une trentaine d'unités. Ici les trois rôles sont séparés : la HAUTEUR porte le rapport, la TEXTURE porte l'unité, le TEXTE porte la valeur exacte. Aucun des trois ne fait le travail d'un autre, et c'est ce qui rend le graphique inattaquable.\n\nLE PIÈGE MESURÉ : laisser la texture décider de la hauteur. Si on arrondit la barre à un nombre entier de crans, la hauteur cesse d'être proportionnelle et le graphique devient faux d'un demi-cran — sur une petite barre, jusqu'à 20 %. La hauteur est donc calculée depuis la valeur, la texture répétée par-dessus, et le dernier cran du haut est TRONQUÉ. C'est correct : le cran partiel dit exactement ce qu'il vaut.\n\nLE PEIGNE SE TIRE DU BAS (`repeating-linear-gradient(to top, …)`). Tiré du haut, la phase change avec la hauteur de chaque barre et les crans ne s'alignent plus d'une barre à l'autre : le lecteur y lit une erreur de rendu avant d'y lire une unité.\n\nSEUIL DE DEUX PÉRIODES : sous deux crans, la barre lit comme un aplat plein et l'unité qu'elle prétend porter n'est plus visible. Un benchmark refuse ce cas — la réponse est de changer l'unité déclarée, jamais d'étirer la barre.",
"id": "chart-04-unit-textured-bar"
}
À côté
chart-02-isotype
Isotype à taux de remplissage
chart-03-accent-column-callout
Histogramme à colonne accentuée et info-bulle
chart-05-tile-heatmap
Heatmap en tuiles arrondies, rampe achromatique + marque de catégorie
layout-04-graph-paper-cells