storytelling-viz : Graphiques prêts à l'emploi
L'objectif est un graphique directement utilisable dans une présentation, un document ou un journal, pas un simple histogramme bricolé. Il doit satisfaire trois points :
- Le bon type de graphique. Le type correspond à ce qu'on compare, le focus est clair, les annotations sont directes, pas de légende.
- Ne pas planter sur des données réelles. Les longs libellés, les valeurs négatives, les données manquantes, les catégories nombreuses et les valeurs aberrantes doivent s'afficher correctement.
- Conforme aux spécifications de sortie. Dimensions, tailles de police, épaisseurs de ligne, format, polices intégrées sont conformes.
Cette compétence ne s'occupe que du graphique. Le choix du sujet, l'analyse et la structure du rapport ne font pas partie de son périmètre.
Workflow
- Déterminer le contexte. Le contexte détermine les conventions par défaut pour les couleurs, les notes de bas de page, les nombres, etc. (voir "Conventions des deux contextes") :
- Revue d'activité : graphiques d'indicateurs opérationnels destinés aux équipes métier ou à la direction, quel que soit le rythme ou le support (revue hebdomadaire, mensuelle, tableau de bord quotidien, analyse des écarts, page de données opérationnelles pour le management / le conseil d'administration / la stratégie). Le critère est "le contenu est-il un indicateur opérationnel ? Le lecteur doit-il prendre une décision métier basée sur ce graphique ?", pas "est-ce une revue hebdomadaire ?".
- Général / Publication : documentation d'analyse ou de recherche, résultats d'expériences, pages web, figures d'articles, etc.
- En cas de doute, traiter comme une revue d'activité (les conventions sont plus strictes, cela ajoute seulement une ligne de note de bas de page). Le contexte "revue d'activité" nécessite d'abord la lecture de
references/business.md, même si le code d'exemple semble suffisant.
- Choisir la fonction selon "ce qu'on compare". Voir le tableau de sélection des fonctions ci-dessous ; le même tableau pour les deux contextes.
- Choisir les spécifications de sortie. Utiliser
spec=(diapositive, document, journal, etc.). - Générer le graphique et vérifier les résultats automatiques.
figure()/save()afficheQA … okou liste chaque avertissement après le rendu ; l'attribut.qade la valeur de retour contient la même liste de problèmes (liste vide = succès). S'il y a des avertissements, corrigez, ne livrez pas. - Ouvrir le PNG et vérifier avec la liste de contrôle. Ouvrir le PNG généré avec Read et vérifier chaque point de la liste de contrôle à la fin.
Démarrage rapide
# /// script
# dependencies = ["plotnine>=0.15,<0.16", "pandas", "pillow", "pypdf"]
# ///
import sys; sys.path.insert(0, "<skill>/scripts")
from viz_common import figure, titled, set_lang, save
import charts as C # Fonctions graphiques générales
import biz as B # Fonctions graphiques pour revues d'activité (peut être mélangé avec C)
set_lang("en") # ou "zh" (par défaut)
c = C.bar_rank(df, "channel", "gmv", highlight="POS", top_n=8, fmt="num")
figure([[(titled(c, title="POS is the only channel that shrank"), 1)]],
spec="doc", name="fig1", outdir="out")
save(c, "fig2", spec="journal-1col", outdir="out") # Journal, une seule figure : pas de titre
# Revue d'activité : même figure(), mais la note de bas de page est obligatoire
k = B.kpi_cards([dict(label="GMV (MTD)", value=16.5e6, prev=15.9e6, target=57e6, progress=0.29)])
figure([[(k, 1)]], spec="doc", size=(6.5, 2), name="kpi", outdir="out",
caption="Basis: paid-order GMV (USD); MTD to Oct 9 vs Sep 1–9.")
Tableau de sélection des fonctions
Trouvez la ligne selon ce que vous comparez. C. = charts.py, B. = biz.py ; ★ indique les fonctions conçues spécifiquement pour les revues d'activité, mais elles peuvent être utilisées dans n'importe quel contexte.
| Ce qu'on compare | Fonction | Points clés |
|---|---|---|
| Classement, composition à un instant, valeurs positives/négatives | C.bar_rank(df, cat, value, highlight=, top_n=, fmt=) | Tri automatique ; au-delà de top_n, fusionner en "Autres/Other" ; valeurs à l'intérieur ou à l'extérieur de la barre |
| Qui a apporté le changement (volume + croissance + contribution par dimension) | ★ B.contrib_table(df, dim, prev, cur, top_n=, growth_alert=) | Barres de données pour le volume, ▲▼ pour la croissance, variation, contribution à la variation totale ; ligne total en haut |
| Tendance : 1 à N courbes ensemble | C.line_trend(df, x, y, group=, highlight=, band=, ref=) | Annotation directe en fin de courbe ; étiquettes de mois à deux niveaux automatiques pour les données mensuelles ; lignes brisées pour les données manquantes ; band= pour l'intervalle de confiance |
| Tendance : plusieurs groupes séparément | C.small_multiples(df, x, y, facet, highlight=) | Même échelle y pour toutes les facettes ; lignes d'arrière-plan grises pour les autres groupes |
| Tendance : un indicateur opérationnel + comparaison avec la période précédente | ★ B.trend(df, date, value, compare="month"/"year", events=, as_of=, period_end=) | Lignes de comparaison alignées automatiquement ; annotation des événements ; ligne pointillée + estimation pour la période en cours |
| Progression : cumul mensuel vs objectif | ★ B.cumulative_vs_target(df, date, value, target, start, end, as_of=, prior=) | Cumul réel, ligne de progression de l'objectif, cumul de la période précédente, estimation basée sur la moyenne quotidienne |
| Réel vs objectif (plusieurs lignes) | ★ B.bullet(df, cat, actual, target, progress=) | Barre réelle + ligne verticale objectif + ligne pointillée "ce qui devrait être fait à ce jour" ; les lignes en retard deviennent rouges |
| Réel vs intervalle (intervalle de prévision, plage normale) | C.range_dot(df, cat, low, high, value, flag="below", bench=) | Les lignes hors limites utilisent la couleur de focus ; bench= pour spécifier une ligne de référence |
| Deux valeurs par groupe (avant/après, prévu/réalisé) | C.dumbbell(df, cat, start, end, labels=, highlight=) | Point vide = début, point plein = fin ; trié par point final ; chaque ligne avec le taux de variation |
| Deux périodes extrêmes, tailles de groupe très différentes | C.slope(df, cat, start, end, mode="change", groups=) | En mode mode="change", toutes les lignes partent de 0% ; les lignes avec un point de départ à 0 sont supprimées avec un WARNING |
| Pont d'augmentation/diminution : période précédente → contributions → période actuelle | C.waterfall(df, cat, delta, start=, end_label=) | Couleurs neutres par défaut + signes + et - ; si la notion de bien/mal a un sens métier, passer up=PAL.good, down=PAL.bad |
| Décomposition d'indicateur (GMV = utilisateurs × fréquence × panier moyen) | ★ B.driver_tree(nodes) | Chaque nœud affiche la valeur actuelle, la variation ▲▼, la contribution (part logarithmique pour la multiplication, variation pour l'addition) |
| Composition : pourcentage de chaque partie dans plusieurs groupes | C.stacked_100(df, cat, part, value, order=, focus_cat=) | Parties dans l'ordre naturel, dégradé monochrome ; légende dans chart.legend |
| Tableau à deux dimensions (groupe × temps, groupe × groupe) | C.heatmap(df, x, y, value, fmt=) | Blanc à monochrome, palette basée sur les quantiles 2%–98% ; texte blanc sur cellules foncées |
| Volume + taux (nombre de demandes et taux d'approbation) | ★ B.bar_line_dual(df, x, bar, line, line_range=) | La seule utilisation autorisée d'un double axe : barres de volume (gauche) + ligne de taux (droite), axe du taux commençant à 0 ou sur une plage fixe |
| Distribution | C.distribution(df, value, group=, kind="box"/"strip"/"hist") | Tri par médiane ; moustaches 1,5×IQR ; échantillonnage si trop de points |
| Relation entre deux variables continues | C.scatter(df, x, y, label=, fit="linear", groups=, x_title=, y_title=) | OLS + intervalle de confiance, affiche R² ; n'annote que les points en focus ; légende dans chart.legend avec groups= |
| Estimations et intervalles de confiance | C.point_range(df, cat, est, low, high, ref=0) | Graphique forestier ; écriture "estimation [intervalle inférieur, intervalle supérieur]" à droite |
| Résumé de quelques chiffres clés (haut de page de rapport) | ★ B.kpi_cards([dict(label, value, prev, target, progress, spark, …), …]) | Grands chiffres, variation ▲▼, taux d'atteinte et progression temporelle, mini-courbe de tendance ; rouge uniquement pour les anomalies |
★ Les détails des paramètres (champs de kpi_cards, structure de nœuds de driver_tree, events= / as_of= de trend, prior= de cumulative_vs_target, règles d'anomalie, alignement des périodes de comparaison) se trouvent dans references/business.md.
Comment choisir quand deux options semblent adaptées :
B.contrib_tablevsC.waterfall: Pour voir simultanément le volume, la croissance et la part, ou si plus d'environ 8 dimensions → tableau ; pour raconter "comment la période précédente s'est transformée en période actuelle" avec moins d'environ 8 éléments → waterfall. Les pages d'analyse causale mettent souvent les deux côte à côte.C.line_trendvsC.small_multiples: Une ligne à mettre en avant, ou pas plus de 4 lignes →line_trend(au-delà de 4, ajouterhighlight=) ; pour voir la forme de chaque groupe individuellement, avec des groupes de même importance →small_multiples. Une colonne de journal (3,5 po) ne peut pas contenir plus de 5 facettes, utiliserline_trend+highlight=.- Deux valeurs par groupe, d'abord regarder l'échelle des groupes : si les tailles sont proches (max / min < environ 10 fois) →
C.dumbbell. Si l'échelle diffère d'un ordre de grandeur ou plus, les petits groupes deviennent illisibles sur une échelle commune, tracer directement le taux de variation : moins d'environ 8 groupes →C.slope(mode="change"); plus de groupes → d'abord calculer la colonne de taux de variation, puis utiliserC.bar_rank(df, cat, "chg", fmt="pct"), le volume absolu étant écrit dans l'étiquette ou un tableau adjacent.
Paramètres et valeurs de retour communs à toutes les fonctions :
highlight=: nom de catégorie, liste ou fonction. Les éléments sélectionnés utilisent la couleur de focus, les autres deviennent gris, le nom de la catégorie est également mis en gras et coloré.fmt=:"num","pct","pp","int","raw", chaîne de format ou fonction ;nd=contrôle le nombre de décimales.- Retourne un objet panel, à insérer directement dans
figure()/save();chart + couche_plotninepeut être enchaîné ;chart.legendest une légende de mots colorés sur une ligne,titled()l'utilise automatiquement.
Quand aucune fonction n'est adaptée, dessiner manuellement :
- Utiliser
C.Frame(w, h, xr, yr, left=…, …)pour réserver l'espace blanc autour en fonction de la largeur réelle du texte (text_width) ; pour l'axe, utiliserC.axis_range(lo, hi). - Sur
C.Canvas(), dessiner avecrect,seg,pt,text,line,band, puis générer le graphique aveccv.plot(fr). - Encapsuler dans
C.Chart(build), oùbuild(w, h)calcule la disposition après avoir reçu les dimensions finales.
Ne pas coder en dur les coordonnées ou les plages d'axes dans le code.
Conventions des deux contextes
Les fonctions elles-mêmes dessinent de la même manière dans les deux contextes ; les différences concernent ces conventions par défaut. Il n'y a pas de paramètre "contexte" dans le code : les fonctions ★ intègrent automatiquement les conventions des revues d'activité (▲▼, rouge seulement pour les anomalies) et font que figure() émet un WARNING en l'absence de note de bas de page. Les graphiques C. sur la même page sont paramétrés par vous selon les conventions du tableau ci-dessous (par exemple caption=, highlight=, couleurs up= / down=).
| Élément | Général / Publication | Revue d'activité |
|---|---|---|
| Hausse / baisse | Utiliser good / bad seulement si la notion de bien/mal a un sens, et toujours accompagné d'un signe + ou – ou d'un texte | Toujours ▲▼ + couleur neutre, jamais de rouge/vert pour les hausses/baisses |
| Quand mettre du rouge | Seulement pour indiquer "mauvais" | Seulement pour ce qui est jugé anomalie (retard sur objectif / dépassement de seuil / au-delà de ±2σ historique) ; les anomalies positives sont seulement mises en gras |
| Quand dire "regardez ici" (exemple : canal qui pénalise la croissance) | highlight= (couleur de focus orange) | Idem ; l'orange signifie seulement "regardez ici", pas du bien ou du mal. Le caractère anormal ou non est indiqué par ▲▼ et le rouge |
| Note de bas de page sur les définitions | Suggérée | Obligatoire (caption=) : numérateur/dénominateur, devise, fenêtre temporelle, base de comparaison ; son absence fait que figure() émet un WARNING |
| Base de comparaison | Selon les besoins de l'analyse | Par défaut : comparaison mensuelle, cumul du mois actuel vs cumul du mois précédent (même nombre de jours), atteinte d'objectif ; pour les métriques de trafic journalier, alignement sur la semaine |
| Période en cours non terminée | Ton clair ou pointillés + "jusqu'au X" | Idem, et tracer l'estimation pour la période complète basée sur la progression |
| Nombres | Selon les habitudes du domaine | Montants en K/M/B ; variation de taux en pp, croissance en % ; unité écrite dans le titre ou la note |
| Double axe | Ne pas utiliser | Autorisé uniquement pour "barres de volume + ligne de taux" (B.bar_line_dual) |
| Camembert / anneau | Éviter si possible | Interdit |
| Spécifications courantes | doc, web, journal-* | PPT utiliser slide ; documents Feishu / Lark / Notion ou chat utiliser doc (illustration PNG) ; si l'utilisateur n'a pas précisé le support, utiliser doc |
Raisons et contre-exemples dans references/rules.md (la partie revue d'activité est B1–B7).
Spécifications de sortie (spec=)
| spec | Dimensions (pouces) | Taille de police du corps | Format par défaut | Usage |
|---|---|---|---|---|
slide / slide-4x3 | 13,33×7,5 / 10×7,5 | 12 / 11,5 | SVG avec contours, SVG modifiable, PNG (150 dpi) | Diapositives de présentation |
web | 8×4,5 | 10,5 | SVG, PNG (200 dpi) | Pages web, emails |
doc | 6,5×4 | 9 | PDF, SVG, PNG (300 dpi) | Un graphique dans un document |
a4 / letter | Page entière | 10 | PDF, SVG, PNG | Rapport pleine page |
journal-1col / -1.5col / -2col | 3,5 / 5,35 / 7,2 de large | 7–7,5 | PDF, SVG, PNG (600 dpi) | Journal (89 / 136 / 183 mm) |
size=(w, h)peut remplacer les dimensions par défaut, par exemple pour un graphique de journal plus haut, ou une ligne de cartes KPI plus basse.- Les tailles de police, épaisseurs de ligne et tailles de points sont mises à l'échelle avec la spécification ; le PDF intègre les polices en Type 42, conformément aux exigences des journaux.
- Les graphiques de journal n'ont pas de titre, la légende est dans le texte : pour un graphique unique, utiliser
save(chart, name, spec="journal-1col"); pour un graphique multi-panneaux, utiliserfigure(rows, spec="journal-2col"), sans passertitle=, le numéro de panneau est fourni partitled(chart, title="a"). figure()produit une page entière (éventuellement avec titre, sous-titre, note de bas de page, plusieurs panneaux) ;save(chart, "fig2", spec="journal-1col")exporte seulement un graphique, sans titre de page ni note de bas de page. Pour les revues d'activité, la note de bas de page étant obligatoire, utiliserfigure().- Deux types de SVG :
<nom>.svgconvertit le texte en contours, identique partout ;<nom>_editable.svgconserve le texte, modifiable dans Figma / PPT, mais nécessite que la police soit installée sur l'ordinateur. Pour modifier du texte dans PPT, utiliser ce dernier, sinon utiliser le premier ou le PNG. slide/webne génèrent pas de PDF par défaut ; pour en avoir, passerformats=("pdf", "png").
Combinaison de panneaux multiples
figure(rows, title=, subtitle=, caption=, spec=):rows=[[(panel, span), …], …],spanest la largeur relative au sein d'une ligne.- La hauteur des lignes est répartie automatiquement par défaut : les lignes contenant uniquement du texte ou un tableau prennent la hauteur de leur contenu, le reste de l'espace est réparti également entre les autres lignes.
titled(chart, title=, legend=, note=): ajoute un titre de panneau, une ligne de légende et une note au-dessus du graphique ; silegend=n'est pas fourni,chart.legendest utilisé automatiquement.- Les titres supportent le balisage enrichi :
**gras**;[phrase](focus)pour mettre en gras et colorer. Les couleurs peuvent êtrefocus,brand,good,bad,muted,inkou#hex. - Autres panneaux :
text_panel(para(...))pour un paragraphe,table_panel(rows, header=)pour un petit tableau (colonnes numériques alignées à droite automatiquement),column(a, b)pour empiler deux panneaux verticalement. - Chaque panneau est rendu séparément avec ses dimensions finales, donc la taille de police est en points réels ; lors de l'assemblage SVG, les identifiants des éléments de chaque panneau sont automatiquement préfixés pour éviter les conflits.
Règles visuelles (déjà exécutées par les fonctions, à suivre si vous dessinez manuellement)
- Couleurs hiérarchisées :
focus(orange) pour marquer l'unique élément à regarder, n'indique pas en soi le bien ou le mal ;brand(bleu foncé) pour les séries standard ; gris pour l'arrière-plan et les références ; l'utilisation degood/baddépend du contexte (voir tableau ci-dessus). - Même couleur = même sens. Les mots-clés et les données correspondantes utilisent la même couleur ; les éléments de sens différent ne doivent pas partager une couleur.
- Le texte doit être lisible. Le contraste du texte est d'au moins 4,5:1. Pour la couleur de focus utilisée comme texte, utiliser
readable(color)pour foncer automatiquement ; pour le texte sur fond foncé, utiliserink_on(fill). Les fonctions gèrent automatiquement,Canvas.textvérifie et avertit en cas de dessin manuel. - Utiliser des annotations directes au lieu de légendes. Le titre de l'axe ne doit pas être le nom du champ. Si toutes les barres sont étiquetées avec des valeurs, supprimer l'axe y et la grille.
- Le tri doit avoir un sens. S'il y a un ordre naturel (étapes, niveaux), le suivre ; sinon, trier par valeur numérique ; placer l'élément en focus à l'endroit le plus visible ; ne pas trier alphabétiquement.
- Axes. Les graphiques à barres doivent commencer à 0 ; les graphiques en lignes dont les données sont proches de 0 doivent commencer à 0 (
zero="auto") ; les panneaux liés utilisent la même échelle ; le double axe est limité au cas "barres de volume + ligne de taux". - Précision. Suffisante pour distinguer. Les nombres sont toujours calculés à partir des données, formatés avec
pct/pp/num/fmt_value, ne pas les écrire manuellement. - Choisir le graphique selon ce qu'on compare : Ne pas utiliser de barres empilées en valeur absolue pour un changement de proportion ; ne pas faire plusieurs camemberts pour des compositions de groupes ; au-delà de 4 lignes, utiliser
highlight=pour souligner ou passer aux petits multiples ; si la comparaison de deux éléments dans un arbre ou un radar prend plus d'environ 2 secondes, remplacer par des barres horizontales. - Les problèmes de données doivent être signalés sur le graphique. Les périodes en cours non terminées utilisent des tons clairs ou des pointillés avec "jusqu'au X" ; les petits échantillons indiquent n ; pour les données manquantes, briser la ligne, ne pas relier.
Les détails et les raisons sont dans references/rules.md ; les exemples de corrections de graphiques SWD sont dans references/swd_practices.md.
Langue et typographie
set_lang("zh" | "en")change simultanément la police par défaut, les étiquettes de mois (chinois "1月\n2026", anglais "Jan\n2026") et "Autres/Other". Les unités monétaires sont par défaut K/M/B dans les deux langues ; pour utiliser 万/亿, utiliserset_number_style("cn"). Les étiquettes contenant du chinois utilisent automatiquement la police CJK.T("Autres", "Other")récupère la chaîne dans la langue courante.- Les chaînes de caractères chinoises passent automatiquement par
zh(): la ponctuation devient pleine chasse, les espaces superflus autour de la ponctuation pleine chasse sont supprimés, les URL et abréviations restent inchangées. - Règles de césure : les nombres et les mots anglais ne sont pas coupés ; la ponctuation n'apparaît pas en début de ligne ; la parenthèse ouvrante ne reste pas en fin de ligne.
- Les polices utilisées sont affichées au démarrage. Si un
WARNING: no CJK fontapparaît, installer Noto Sans SC.
Vérifications avant livraison
Vérifications automatiques (voir la sortie de figure() ou .qa) :
-
QA … ok: pas de chevauchement de texte, rien ne sort du panneau, rien n'est coupé par la zone de dessin. - Pas d'avertissement de contraste, ni d'avertissement
rows do not fit,labels take most of the panel,cells too small,basis caption.
Vérifications manuelles (ouvrir le PNG) :
- En cachant le titre, en regardant seulement le graphique, peut-on dire ce qu'il exprime ? N'y a-t-il qu'un seul focus ?
- Les nombres dans le titre et les annotations proviennent-ils de la même variable que les valeurs sur le graphique ?
- Le type de graphique correspond-il à ce qu'on compare (voir le tableau de sélection) ? Le tri a-t-il un sens ? Les échelles des panneaux liés sont-elles identiques ?
- L'utilisation des couleurs respecte-t-elle les conventions du contexte ? Si on enlève les couleurs, le bien/mal / la hausse/baisse sont-ils encore lisibles via les symboles ou le texte ?
- Absence de légende par défaut, de nom de champ, d'effet 3D ou de dégradé ?
- Les dimensions et la taille de police sont-elles conformes à la spécification cible (environ 7 pt pour le corps d'un article de journal, 12 pt ou plus pour une diapositive) ? Le PDF a-t-il été exporté si nécessaire ?
Supplément pour les revues d'activité :
- La note de bas de page sur les définitions est-elle complète ? Les chiffres sont-ils cohérents avec les autres tableaux de bord (dans le même rapport, le même nombre est-il cohérent entre les pages) ?
- L'unité, le nombre de décimales et le format sont-ils uniformes dans tout le graphique ?
Environnement et tests
- Dépendances :
plotnine>=0.15,<0.16(testé avec 0.15.8 + matplotlib 3.11),pandas,pillow, etpypdfpour l'export PDF. Le code est dansscripts/viz_common.py(outils de base, spécifications, export, QA),scripts/charts.py(C.) etscripts/biz.py(B.). - Emplacement de sortie : Définir
outdirsur un répertoire visible par l'utilisateur ; ne pas écrire dans le répertoire de la compétence elle-même. - Galerie d'exemples :
uv run references/gallery.py [outdir](chaque fonctionC., diapositive chinoise + journal anglais) ;uv run references/business_gallery.py [outdir](chaque fonctionB., diapositive chinoise + document anglais). La sortie par défaut est dans le répertoireout/courant. - Tests de régression :
uv run references/tests/edge_cases.pyutilise 38 jeux de données limites et vérifie, dans deux spécifications (soit 76 tests), le comportement des fonctions et la conformité de la QA ; après avoir modifié une fonction de graphique, ce test doit être relancé et doit passer entièrement avant toute publication. - Si plotnine ne peut pas être installé dans le bac à sable : Utiliser d'abord le simulateur matplotlib dans
PYTHONPATH=<skill>/scripts/plotnine_previewpour prévisualiser la mise en page et exécuter la QA (les règles d'épaisseur de ligne et de coupe sont cohérentes avec plotnine). LeQA okdu simulateur n'est pas un critère de livraison : passer le script à l'utilisateur pour qu'il l'exécute avecuv runsur sa machine, et lui indiquer clairement que ce graphique n'a pas encore été rendu et vérifié avec le vrai plotnine.