Des bibliothèques graphiques originales au service de la dataviz sur le Web

Nantes DevFest, octobre 2017

Alexis Jacomy

(twitter|github|slides).com/jacomyal

de la dataviz

Tout le monde veut

dataviz,

c'est :

et une

Un design

Un algorithme

Un moteur de rendu

d3.js

SVG

rendu graphique

on a plein de solutions de

Et pourtant

Les démos sont sur github

La heatmap

Exemple 1

Petite confession

Adobe FLASH

J'ai appris À développer sous

Peu d'outils

Une API de tracé riche et variée

AUCUNE bonne pratique

Plein de méthodes innovantes et variées !

Les meatballs

Comment on fait

metaballs

Metaballs :

Metaballs :

Approche théorique

approche théorique :

(x - x_0)^2 + (y - y_0)^2 \le r_0^2
(xx0)2+(yy0)2r02(x - x_0)^2 + (y - y_0)^2 \le r_0^2
\dfrac{r_0^2}{(x - x_0)^2 + (y - y_0)^2} \ge 1
r02(xx0)2+(yy0)21\dfrac{r_0^2}{(x - x_0)^2 + (y - y_0)^2} \ge 1

équation pour un disque :

approche théorique :

\displaystyle\max_{i=0}^n\dfrac{r_i^2}{(x - x_i)^2 + (y - y_i)^2} \ge 1
maxi=0nri2(xxi)2+(yyi)21\displaystyle\max_{i=0}^n\dfrac{r_i^2}{(x - x_i)^2 + (y - y_i)^2} \ge 1
\displaystyle\sum_{i=0}^n\dfrac{r_i^2}{(x - x_i)^2 + (y - y_i)^2} \ge 1
i=0nri2(xxi)2+(yyi)21\displaystyle\sum_{i=0}^n\dfrac{r_i^2}{(x - x_i)^2 + (y - y_i)^2} \ge 1

équation pour Plusieurs disques :

équation pour les metaballs :

approche théorique :

APPROCHE THÉORIQUE :

Trop peu performant

O (pixels x balls)

Pas d'antialiasing

"marching cubes"

(excellent tuto ici)

approche théorique :

Optimisation avec les

APPROCHE THÉORIQUE :

Trop peu performant

O (blocks x balls)

Excellent résultat

MAIS toujours

Le

"Gooey effect"

Approche pratique :

APPROCHE PRATIQUE :

Flou

Contraste

APPROCHE PRATIQUE :

FILTER: blur()

filter: contrast()

APPROCHE PRATIQUE :

Les heatmaps, donc

Retour à la dataviz :

Étape 1 : nuage de points

DATA.forEach(([ x, y ]) => {
  const point = document.createElement('DIV');

  // Règles de 3 :
  point.style.left = (x / xMax * 100) + '%';
  point.style.bottom = (y / yMax * 100) + '%';

  points.appendChild(point);
});

Étape 2 : Filtres

<svg>
  <filter id="posterize">
    <feComponentTransfer>
      <feFuncR type="discrete" tableValues="0 .5 1" />
      <feFuncG type="discrete" tableValues="0 .5 1" />
      <feFuncB type="discrete" tableValues="0 .5 1" />
    </feComponentTransfer>
  </filter>
</svg>

Étape 2 : Filtres

<style>
  #points {
    filter: url('#posterize');
  }
</style>

Étape 3 : Le reste

Résultat :

Le CAMEMBERT

Exemple 2

3D !

C'est compliqué

Le WebGL,

Problème :

déjà

low-levels structures

low-levels functions

TypedArray

glsl

et aussi

Les vertex shaders

Enfin

Les Fragment shaders

par exemple

Bref...

Le WebGL n'a rien à voir avec tout ce qu'on connait dans le dev web

THREE.js

à la rescousse !

Une doc

plutôt bonne (ici)

Des tonnes d'exemples de code ()

Mais surtout

Google

Et comment on teste une nouvelle techno ?

Driven

Development !

Étape 1 : Le camembert

Étape 1 : Le camembert

// On génère la forme en 2D :
const slide2d = new THREE.Shape();
slide2d.moveTo(0, 0);
slide2d.arc(0, 0, size, aFrom, aTo, false);
slide2d.lineTo(0, 0);

Étape 1 : Le camembert

// On extrude pour en faire un volume 3D :
const slice3d = new THREE.ExtrudeGeometry(
  slide2d,
  { amount: value * size / 2 }
);

Étape 1 : Le camembert

// On ajoute dans la scène :
const mat = new THREE.MeshPhongMaterial(
  { color }
);
scene.add(
  new THREE.Mesh(slice3d, mat)
);

Étape 1 : Le camembert

Étape 2 : Les labels

Étape 2 : Les labels

// On trace le texte dans un canvas :
const canvas =
  document.createElement('CANVAS');
const context = canvas.getContext('2d');

context.fillStyle = '#000';
context.font = '30px sans-serif';
context.fillText(message, 0, 30);

Étape 2 : Les labels

// On génère une texture, puis un Sprite :
const map = new THREE.Texture(canvas);
map.needsUpdate = true;
const material =
  new THREE.SpriteMaterial({ map });

const sprite = new THREE.Sprite(material);
sprite.scale.set(100, 50, 1);

Étape 2 : Les labels

Étape 3 : LE swag

uniform sampler2D mapLeft;
uniform sampler2D mapRight;
varying vec2 vUv;

void main() {
  vec4 colorL, colorR;
  vec2 uv = vUv;

  colorL = texture2D(mapLeft, uv);
  colorR = texture2D(mapRight, uv);

  gl_FragColor = vec4(
    colorL.g * 0.7 + colorL.b * 0.3,
    colorR.g,
    colorR.b,
    colorL.a + colorR.a
  );
}

Étape 3 : Le SWAG

AR.js à la rescousse !

/AR.js

  /three.js

    /examples

      /basic.html

const group= new THREE.Group();
scene.add(group);
group.rotation.x = -Math.PI / 2;

Petit soucis de rotation

Les pictographes

Exemple 3

On peut construire des viz sur ce principe

Sexperience

disparu du Web :(

Les données :

Dataviz survey

@elijah_meeks, 2017

On veut observer les répartitions des réponses aux questions...

...en observant toujours TOUS les individus

Problème :

Animer fluidement ~1000 éléments graphiques

Moteur de rendu 2D

basée sur Webgl

avec fallback canvas

vraiment

très

performant

Une bonne doc (ici)

Pleins d'exemples ()

Retour à notre graphique...

Étape 1 : Le state

const _STATE = {};
const _CALLBACKS = [];

export function setState(key, value) {
  if (value === _STATE[key]) return;

  _STATE[key] = value;
  _CALLBACKS.forEach(({ fn, keys }) => {
    if (!keys || keys.includes(key)) {
      fn(_STATE);
    }
  });
}

export function onStateChange(keys, fn) {
  _CALLBACKS.push({ fn, keys });
}

Étape 2 : boucle de rendu

lines.forEach((line, i) => {
  // On génère le sprite :
  const sprite = drawSprite(line);

  sprite.targetX = sprite.x = 0;
  sprite.targetY = sprite.y = 0;

  // On ajoute le sprite dans la scène :
  scene.stage.addChild(sprite);

  // On ajoute un listener, et on fait en sorte que
  // le sprite se dirige constamment vers sa cible :
  scene.ticker.add(() => {
    sprite.x = (sprite.x + sprite.targetX) / 2;
    sprite.y = (sprite.y + sprite.targetY) / 2;
  });
});

Étape 3 : Le layout

Étape 4 : Les sprites

Problème

Étape 4 : Les sprites

\displaystyle rows = \dfrac{H}{h'}
rows=Hh\displaystyle rows = \dfrac{H}{h'}
\displaystyle cols = \dfrac{W}{w'}
cols=Ww\displaystyle cols = \dfrac{W}{w'}
\displaystyle \dfrac{h'}{w'} = \dfrac{h}{w}
hw=hw\displaystyle \dfrac{h'}{w'} = \dfrac{h}{w}
\displaystyle N \times h' \times w' = W \times H
N×h×w=W×H\displaystyle N \times h' \times w' = W \times H
\displaystyle \times N
×N\displaystyle \times N
\displaystyle W
W\displaystyle W
\displaystyle H
H\displaystyle H
\displaystyle w
w\displaystyle w
\displaystyle h
h\displaystyle h
\displaystyle w'
w\displaystyle w'
\displaystyle h'
h\displaystyle h'

Étape 4 : Les sprites

\displaystyle rows = \lceil \sqrt{N \times \dfrac{H \times w}{W \times h}} \rceil
rows=N×H×wW×h\displaystyle rows = \lceil \sqrt{N \times \dfrac{H \times w}{W \times h}} \rceil
\displaystyle cols = \lceil \sqrt{N \times \dfrac{W \times h}{H \times w}} \rceil
cols=N×W×hH×w\displaystyle cols = \lceil \sqrt{N \times \dfrac{W \times h}{H \times w}} \rceil
const rows = Math.ceil(Math.sqrt(count * (h * W) / (w * H)));
const cols = Math.ceil(Math.sqrt(count * (h * W) / (w * H)));

const bar = {
  rows, cols,
  colWidth: W / cols,
  rowHeight: H / rows,
};

Étape 5 : Bonus

// On génère pour chaque sprite deux "vitesses"
// aléatoires entre 1 et 21 :
const xSpeed = Math.random() * 20 + 1;
const ySpeed = Math.random() * 20 + 1;

scene.ticker.add(() => {
  // Les Sprites se déplaceront sur des courbes toutes
  // différentes :
  sprite.x =
    (sprite.x * xSpeed + sprite.targetX) / (xSpeed + 1);
  sprite.y =
    (sprite.y * ySpeed + sprite.targetY) / (ySpeed + 1);
});

Étape 5 : Bonus

scene.ticker.add(() => {
  // ...
  // On ajoute une petite vibration constante :
  sprite.x += (Math.random() * 0.6) - 0.3;
  sprite.y += (Math.random() * 0.6) - 0.3;
  sprite.rotation = Math.random() / 30 - 1 / 60;
});

Étape 6 : Bonus 2

const SKINS = [
  '#ffdcb1', '#e5c298', '#e4b98e',
  '#e2b98f', '#e3a173', '#d99164',
  '#cc8443', '#c77a58', '#a53900',
  '#880400', '#710200', '#440000',
];

ctx.fillStyle =
  SKINS[Math.floor(Math.random() * SKINS.length)];

[
  // Tête :
  [1, 0, 4, 1], [0, 1, 6, 3],
  // Corps et bras :
  [1, 4, 4, 1], [0, 5, 6, 5],
  // Jambes :
  [1, 10, 1, 2], [4, 10, 1, 2], [5, 11, 1, 1],
].forEach(
  rect => ctx.fillReact(...rect)
);
const TSHIRTS = ['#eaeaea', '#242424', '#bababa'];
const SHIRTS = ['#6fd1ec', '#de96e3', '#bee89a'];
const VESTS = ['#5a4728', '#2f3045', '#562c2c'];

// Les "jeunes" en T-Shirts
if (person.age < 28) {
  // T-Shirt :
  ctx.fillStyle =
    TSHIRTS[Math.floor(Math.random() * TSHIRTS.length)];
  ctx.fillRect(0, 5, 6, 2);
  ctx.fillRect(1, 7, 4, 2);

// Les "vieux" en veste + chemise
} else {
  // Shirt :
  ctx.fillStyle =
    SHIRTS[Math.floor(Math.random() * SHIRTS.length)];
  ctx.fillRect(0, 5, 6, 4);

  // Vest :
  ctx.fillStyle =
    VESTS[Math.floor(Math.random() * VESTS.length)];
  ctx.fillRect(0, 5, 1, 3);
  ctx.fillRect(1, 5, 1, 4);
  ctx.fillRect(4, 5, 1, 4);
  ctx.fillRect(5, 5, 1, 3);
}
// ...

Résultat final...

Y'en a plein !

Et des 

exemples du genre 

"Ascii experiment" 

à la main 

CONCLUSION

MAIS

d3.js + SVG, c'est la valeur sûre

Il y pleins de solutions plus fun

PROFITONS-EN !

Merci beaucoup !