
Je voulais D3 pour ma timeline. J'ai fini en SVG à la main
J'ai passé des mois à essayer de construire la timeline de Minoa avec D3, avant de la dessiner en SVG à la main. Plusieurs mois après le lancement de l'app, j'ai reconstruit un POC pour vérifier si D3 avait vraiment tort. Verdict, mesures, et pourquoi D3 est quand même resté dans le projet.
En écrivant cet article, je voulais revenir sur mon hésitation entre D3 et SVG pour la timeline de Minoa. En fait non, je n’hésitais pas : je voulais faire avec D3 !
Sauf que D3 n’a pas convenu. La timeline de Minoa est dessinée en SVG, à la main. Et pourtant, D3 est toujours dans le projet… pour les courbes de croissance.
Dans l’article de lancement, j’avais glissé que j’avais « longuement exploré » cette approche face à D3, et gardé les détails techniques pour plus tard. Plus tard, c’est maintenant.
(Pour les non-devs : D3 est une bibliothèque JavaScript très connue pour dessiner des graphiques et des visualisations de données. Le SVG, c’est le format de dessin vectoriel du navigateur : on peut l’écrire soi-même, trait par trait. C’est ce que j’ai fini par faire.)
J’aimais D3
Je trouve D3 chouette. Vraiment. C’est une lib qui a une philosophie, une manière de penser la donnée et le dessin, bref : quand j’ai un graphique à construire, c’est le premier nom qui me vient.
Alors j’ai commencé par D3, mais mes premiers essais n’étaient pas fous : manque de fluidité, superpositions que je n’arrivais pas à gérer, c’est à cet instant que j’ai envisagé une autre piste : le SVG. Alors pendant plusieurs mois, j’ai exploré les deux pistes. (Pas à temps plein : le soir, par essais successifs : vous savez, ces soirées de dév qui se terminent à 2h du mat’…). Je me suis beaucoup aidé de l’IA pour tester plusieurs compositions, comparer un rendu avec D3 et un avec du SVG écrit à la main, voir ce que ça donnait à l’écran. C’est exactement l’usage « explorer ce qu’on ne connaît pas encore » dont je parlais dans Coder avec l’IA (review comprise, évidemment).
Et à chaque fois, le SVG à la main se révélait plus fluide pour l’UX, moins d’ajustements à faire. Avant de faire ces tests, sur le papier j’aurais clairement pas parié sur le SVG.
Ce qui coinçait, honnêtement
Je dois être honnête : je ne me souviens plus précisément de ce qui bloquait.
De mémoire, je sais que ça ne convenait pas, ça ne fonctionnait pas comme il fallait. Il y avait des soucis avec les trous dans la ligne de temps, des problèmes de tailles, des superpositions et des problèmes de zoom que je n’arrivais pas à gérer avec D3, mais qui fonctionnaient avec le SVG.
Est-ce que j’ai mal utilisé D3 ? Probablement ! Est-ce que ça aurait été plus simple avec D3 ? Pour moi : non. Mais peut-être que je n’avais pas assez fouillé ? Ça m’a trotté récemment en tête, et j’ai décidé d’y retourner pour vérifier.
Ce que fait le SVG à la main aujourd’hui
Avant de juger D3, voilà ce que fait réellement la timeline. Deux composables Vue, useTimelineAxis.js et useTimelineZoom.js, environ 800 lignes à eux deux.
Les cartes ne sont pas dans le SVG. Ce sont des éléments HTML positionnés en absolu. Le SVG ne dessine que l’axe, les marqueurs et les fils.
Les périodes sans souvenir sont compressées. Au-delà de 30 jours sans rien, une période prend entre 32 et 300 px à l’écran, quelle que soit sa durée réelle, et elle est hachurée. Un long creux ne mange pas trois écrans de vide.
Les rafales sont étalées. Un anniversaire, des vacances : parfois plusieurs souvenirs tombent en quelques jours. Une passe de séparation pousse chaque carte sous la précédente pour qu’aucune ne chevauche sa voisine. Et surtout, chaque segment de l’axe s’allonge pour contenir ses cartes :
seg.endY = Math.max(y + h, lastEventBottom + 20)
Cette ligne résume tout le reste de l’article : l’axe suit les souvenirs.
Le zoom fonctionne par paliers : macro, méso, micro, jour. Pendant le geste, au pincement comme à la molette, on n’applique qu’un simple transform: scale(), que le GPU gère tout seul. Au relâchement, un seul recalcul de la mise en page, et le souvenir qu’on visait reste sous les doigts.
Le POC : D3 avait-il vraiment tort ?
Le doute ne partait pas. Alors début novembre 2026, j’ai reconstruit un POC dans un bac à sable.
Précision importante : ce n’est pas la reproduction de mes essais de mai, je ne les ai plus. C’est une reconstruction, avec ce que je sais aujourd’hui de mon propre problème.
Les données : deux comptes de démo de Minoa. Léa, 112 souvenirs sur deux ans et demi, et Ethan, 167 souvenirs sur sept ans. Trois variantes, en D3 v7 + Vue.
Variante 1 : linéaire
Le réflexe D3 : d3.scaleTime, 2 px par jour. Une date entre, une position sort.
Résultat sur Léa, au zoom par défaut (k = 1) : 110 cartes sur 112 chevauchent leur voisine. En zoomant ×4, il en reste encore la moitié : 50 sur 112. C’est la capture en haut de l’article. Un mille-feuille.
Variante 2 : par morceaux
D3 sait faire une échelle à plusieurs segments : on lui donne un domaine et une plage à plusieurs points, et scaleTime interpole morceau par morceau. On peut donc compresser les trous comme dans Minoa :
domain.push(previous, current)
range.push(previousY, previousY + gapHeight(days, zoomK))
Ça marche. Et ça ne change presque rien. Léa n’a qu’un seul trou de plus de 30 jours ; Ethan en a 17, le plus long faisant 63 jours. Autrement dit : le vrai problème n’était pas les trous, c’était les rafales.
Variante 3 : par morceaux + séparation
Alors j’ajoute la même passe que dans Minoa : chaque carte poussée sous la précédente. Zéro chevauchement, enfin.
Sauf que l’axe D3, lui, n’a pas bougé. Il fait 1 858 px quand les cartes en occupent 8 128. Sur Léa, 99 cartes sur 112 se retrouvent à plus d’un écran de leur date sur l’axe, avec un décalage maximum de 6 154 px. Sur Ethan : 154 sur 167.
Et le zoom ne sert plus à rien : entre k = 0,1 et k = 1, rien ne change. Ce sont les cartes qui dictent la hauteur, plus l’échelle.
La fluidité
CPU bridé ×4, sur Ethan, trois mesures. Recalculer la mise en page à chaque image : 57 à 84 fps. La preview GPU façon Minoa (donc SVG) : 83 à 99 fps.
La différence est réelle, mais pas rédhibitoire. Et une nuance qui compte : les cartes du POC ne contiennent que du texte, les vraies portent des photos.
d3-zoom et le scroll mobile - le petit truc à savoir
Un dernier détail : par défaut, d3-zoom appelle preventDefault sur chaque touchmove, même à un seul doigt. Sur mobile, le scroll natif du navigateur et son inertie disparaissent. Pour une timeline verticale qu’on parcourt au pouce, c’est embêtant.
Ça se contourne avec zoom.filter(), en ne laissant passer que les gestes à deux doigts et Ctrl+molette. Mais il faut le savoir.
Le verdict
D3 sait faire chaque morceau : l’échelle par morceaux, le zoom, les axes. Ce n’est pas une question de fonctionnalités manquantes, c’est une question de modèle.
Dans D3, une échelle est une fonction : une date entre, une position sort. Les données suivent l’axe.
Dans Minoa, la position d’un souvenir dépend de ses voisins. Si trois souvenirs tombent la même semaine, le troisième est plus bas parce que les deux premiers existent. Ce n’est plus un problème d’échelle, c’est un problème de mise en page.
Avec D3, les souvenirs suivent l’axe. Dans Minoa, c’est l’axe qui suit les souvenirs.
En refaisant ce POC j’ai recompris ce qui m’avait bloqué durant le développement initial : trop de contraintes et de bidouilles : pour arriver à un résultat pas tant satisfaisant. Je suis retourné sur le principe de base : KISS (Keep it Simple & Stupid).
Pourquoi D3 est resté pour les courbes de croissance
Mais D3 est bien dans Minoa ! Les courbes de croissance (taille et poids, avec les courbes de référence de l’OMS en arrière-plan) passent par useGrowthChart.js : scaleLinear, scaleTime, d3.line avec curveMonotoneX, axisLeft, axisBottom, axisRight.
Là, ce sont de vraies données continues, sur de vrais axes. Une mesure prise à six mois doit être à six mois, pas poussée plus bas parce que la précédente prend de la place. C’est exactement ce pour quoi D3 est fait, et il le fait très bien.
Donc ce n’est pas « D3 contre SVG ». C’est D3 là où le problème est une échelle, et du SVG à la main là où le problème est une mise en page.
Ce que j’en retiens
Je regrette un peu de ne pas avoir utilisé D3 pour la timeline, je l’avoue.
Une bibliothèque puissante donne un cadre. Tant que le problème rentre dedans, c’est un bonheur : tu écris peu, c’est élégant, ça marche. Mais quand le cadre est trop serré pour ce que tu veux faire, ça devient compliqué, malgré toute sa puissance.
Faire soi-même prend du temps, les 800 lignes ne se sont pas écrites en un soir (malgré l’IA, et oui !). Mais ça donne une maîtrise plus fine, et pour un produit aussi précis qu’une timeline où chaque souvenir doit tomber au bon endroit, au bon zoom, sous le bon doigt, j’en avais besoin.
Peut-être que j’y retournerai. Et si vous avez une idée, ou bien un retour d’expérience pour que l’axe suive les souvenirs avec D3, je suis preneur ^^
La timeline en question est visible sur minoabook.app (faudra mettre des souvenirs dans votre timeline, qui est vierge au début).
Bon dév, et bisou :)
Keiro