@micheeeeel/yinyangapp
v2.3.0
Published
A program to visualize human activity balance to reach Sustainable Development Objectives
Readme
@micheeeeel/yinyangapp
Visualisation « Sphères » (diagramme yin/yang + satellites) de l'équilibre des activités humaines vis-à-vis des Objectifs de Développement Durable.
Deux usages :
- Page standalone (
npm start, déployée sur GitHub Pages) : l'application complète (panneaux, pile d'actions, recherche…) bootstrapée parsrc/main.ts. - Librairie npm : une app hôte (ex. React) importe les fonctions de
src/api.tspour obtenir un diagramme.
Installation dans une app hôte
npm install @micheeeeel/yinyangapp d3d3 (v7) est un peer dependency : il n'est pas embarqué dans le bundle, c'est
l'app hôte qui le fournit — soit via son propre bundler (import "d3", résolu
automatiquement), soit en global window.d3 si le bundle est chargé en <script>
(voir webpack.config.js → externals).
Importer le CSS une fois :
import "@micheeeeel/yinyangapp/dist/bundle.css";API
import {
createYinYangSvg, // rendu hors écran → SVGSVGElement détaché (ou null)
createYinYangSvgs, // idem, en lot
mountYinYang, // diagramme vivant dans un conteneur de l'hôte → { controller, destroy }
mapCauseToInternalModel,
createRandomCause, // Cause aléatoire (2–7 composantes, ODD distincts) — graine optionnelle
getYasuniCause, // le Yasuní complet (7 composantes, 20 ODD) au format Cause
type Cause, type YinYangSvgConfig, type YinYangMountConfig,
} from "@micheeeeel/yinyangapp";data accepte une Cause (format métier : composantes → ODD → facettes avec
score/weight) ou un YinYangInternalModel déjà adapté.
Toutes ces fonctions touchent le DOM : à appeler côté client uniquement
(useEffect en React, pas de SSR).
Exemple React
import { useEffect, useRef } from "react";
import { createYinYangSvg, type Cause } from "@micheeeeel/yinyangapp";
export function YinYang({ data, size = 400 }: { data: Cause; size?: number }) {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const host = ref.current;
if (!host) return;
const svg = createYinYangSvg({ data, viewData: { minEdge: size } });
if (svg) host.appendChild(svg);
return () => { host.innerHTML = ""; };
}, [data, size]);
return <div ref={ref} />;
}Pour un diagramme interactif, préférer mountYinYang({ data, container: ref.current })
et appeler destroy() dans le cleanup du useEffect.
Film léger pour markers de carte
createYinYangFilmSvg « cuit » un tour complet de l'étincelle périmètre (sans
satellites, sans glow) en un SVG autonome sans JavaScript : rotation CSS de
l'étincelle + flip-book de calques pré-rendus pour les bascules de composante.
Coût runtime par instance ≈ un GIF ; prévu pour des dizaines de markers Mapbox.
import { createYinYangFilmSvg } from "@micheeeeel/yinyangapp";
const film = createYinYangFilmSvg({
data, // Cause ou YinYangInternalModel
size: 120, // px
rotationDurationMs: 6000,
transitionFrames: 4, // images par bascule (0 = switch immédiat)
});
new mapboxgl.Marker({ element: film }).setLngLat([lng, lat]).addTo(map);highlightActiveSegment: false: bordure neutre unique (pas de mise en avant du segment de la composante courante), film un peu plus léger.stripSpikes(défauttrue) : les pointes (une<line>+ un dégradé par découpe d'ODD, dans chaque calque) sont retirées — invisibles à la taille d'un marker et lourdes (Yasuní : 492 → 254 Ko, dégradés 484 → 25). Le film ne garde que l'essentiel : lobes yin/yang avec leurs arcs d'ODD, yeux, anneau des facettes.stripSpikes: falsepour les conserver.yinYangRotationMs(défaut : pas de rotation) : rotation des lobes yin/yang cuite en CSS pur — même cinématique que la rotation de la lib (orbite des deux lobes autour du centre, rotation propre de chacun, demi-disque entraîné), continue et uniforme, sans rampe ni retour à la position de base. Durée d'un tour en ms, surchargeable sans recuire par la variable CSS--yy-yinyang-rotationsur le<svg>(ex.film.style.setProperty("--yy-yinyang-rotation", "12s")), comme--yy-rotationpour l'étincelle. Coût : un groupe orbital animé + un groupe de rotation propre par lobe et par calque (film Yasuní ≈ +3 %). Pour rejouer la vitesse du diagramme lui-même, lire la constante exportéeYINYANG_ROTATION_DURATION_MSplutôt que d'écrire un nombre en dur : elle suitanimationConfig.rotationDurationMs.- Cuire une fois par lieu (≈ 50 ms à
transitionFrames: 0, ≈ 150 ms à 4) et cloner :film.cloneNode(true). - Sur une page qui anime en continu (carte), préférer
createYinYangFilmSvgAsync(même sortie, même config) : la cuisson rend la main au navigateur entre chaque calque capturé (pas ≤ 12 ms), donc pas de micro-gel. Options :{ yieldFn, signal }(AbortSignal pour annuler proprement).
const film = await createYinYangFilmSvgAsync({ data, size: 100, transitionFrames: 0 });- Vitesse par instance sans recuire :
clone.style.setProperty("--yy-rotation", "5200ms"). - Ordres de grandeur (4 composantes, 120 px) :
transitionFrames: 4→ ~170 Ko brut / 16 Ko gzip / 1 700 nœuds ;transitionFrames: 0→ ~40 Ko / 350 nœuds. - Survol / sélection : remplacer le film par
mountYinYang(diagramme vivant), puis revenir au film.
Données : Yasuní complet ou Cause aléatoire
import { createRandomCause, getYasuniCause } from "@micheeeeel/yinyangapp";
const yasuni = getYasuniCause(); // identique à la page standalone
const pin = createRandomCause({ seed: mission.id, id: mission.id }); // stable par grainecreateRandomCause tire 2 à 7 composantes (orientations alternées), 1 à 3 ODD
par composante — distincts dans toute la Cause, donc couleurs officielles
jamais répétées — et 1 à 4 facettes par ODD (score ∈ [0.15, 0.85], poids 1–3).
Toutes les plages sont surchargeables (components, oddsPerComponent,
cuttingsPerOdd, score, weight). Même seed ⇒ même Cause ; sans graine,
Math.random. Ne touche pas au DOM (utilisable en SSR).
Guide d'intégration détaillé (React + Mapbox, cache, survol) : docs/integration-yvy-frontend.md.
Développement
npm start # dev server
npm test # vitest
npm run build # dist/bundle.js + dist/bundle.css + dist/types/
npm publish --access public