Alexis Jacomy

# Approche théorique

## approche théorique :

(x - x_0)^2 + (y - y_0)^2 \le r_0^2
$(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
$\dfrac{r_0^2}{(x - x_0)^2 + (y - y_0)^2} \ge 1$

## approche théorique :

\displaystyle\max_{i=0}^n\dfrac{r_i^2}{(x - x_i)^2 + (y - y_i)^2} \ge 1
$\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
$\displaystyle\sum_{i=0}^n\dfrac{r_i^2}{(x - x_i)^2 + (y - y_i)^2} \ge 1$

# Les heatmaps, donc

## Étape 1 : nuage de points

import DATA from '../assets/data.json';

const MAX_X = Math.max(...DATA.map(([vx, vy]) => vx));
const MAX_Y = Math.max(...DATA.map(([vx, vy]) => vy));
const POINTS = document.getElementById('points');

// Créé un point pour chaque individu :
DATA.forEach(([ vx, vy ]) => {
const point = document.createElement('div');
point.style.left = (vx / MAX_X * 100) + '%';
point.style.bottom = (vy / MAX_Y * 100) + '%';

POINTS.appendChild(point);
});


## Étape 2 : Filtres

<!DOCTYPE html>
<html>
<style>
#points {
filter: url('#posterize');
}
</style>
<body>
<div style="visibility:hidden;">
<svg>
<filter id="posterize">
<feComponentTransfer>
<feFuncR type="discrete" tableValues="0 0.25 0.5 0.75 1" />
<feFuncG type="discrete" tableValues="0 0.25 0.5 0.75 1" />
<feFuncB type="discrete" tableValues="0 0.25 0.5 0.75 1" />
</feComponentTransfer>
</filter>
</svg>
</div>
</body>
</html>


# Development !

## Étape 1 : Le camembert

const total = DATA.map(a => a.value).reduce((a, b) => a + b);

DATA.forEach(({ value, color, label }) => {
const angle = 2 * Math.PI * value / total;

const material = new THREE.MeshPhongMaterial({ color });

// Forme en 2D :
const geometry = new THREE.Shape();
geometry.moveTo(0, 0);
geometry.arc(0, 0, SIZE, acc, acc + angle, false);
geometry.lineTo(0, 0);

// Forme en 3D :
const extruded = new THREE.ExtrudeGeometry(
geometry,
{ amount: value * SIZE / 2 }
);

const slice = new THREE.Mesh(extruded, material);

acc += angle;
});

## Étape 2 : Les labels

function makeTextSprite(message) {
// On créé la texture :
//   1. On créé un canvas
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');

context.fillStyle = '#000';
context.font = '30px sans-serif';

//   2. On dessine le texte
context.fillText(message, 0, 30);

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

//   4. On génère une Sprite
const sprite = new THREE.Sprite(material);
sprite.scale.set(100, 50, 1);

return sprite;
}

## Étape 3 : LE swag

// On créé d'abord 2 caméras, puis on merge les images
// grace au fragment shader suivant :

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

// On veut que la caméra alterne entre deux positions très proches,
// pour donner l'effet stéréoscopique :

const angle = (
// Angle de base :
-Math.PI / 3
// On alterne toutes les 50ms :
+ Math.floor(Date.now() / 50) % 2 ?
0 :
Math.PI / 200
);

// FOCAL : Distance entre le centre de l'objet et la caméra :
const direction = new Vector3(
FOCAL * Math.cos(angle),
-FOCAL * Math.sin(angle),
FOCAL
);

camera.position.set(...position.toArray());
camera.lookAt(CENTER);
renderer.render(scene, camera);

# 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 :

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

## Étape 3 : Le layout

const SPARE_HEIGHT = HEIGHT - 2 * BORDER_MARGIN;
const SPARE_WIDTH = (
WIDTH - 2 * BORDER_MARGIN
- (values.length - 1) * BARS_MARGIN
);
const BAR_WIDTH = SPARE_WIDTH / values.length;

// values de la forme { value, count } :
const max = _.maxBy(values, 'count');
const bars = values.map(({ value, count }, i) => {
// La fameuse règle de 3 :
const barHeight = SPARE_HEIGHT * count / max;

return {
top: SPARE_HEIGHT - barHeight,
left: (BAR_WIDTH + BARS_MARGIN) * i,
width: BAR_WIDTH,
height: barHeight,
};
});

# Problème

## Étape 4 : Les sprites

W

H

h

w

w'

h'

rows = H / h'

cols = W / w'

h' / w' = h / w

N x h' x w' = W x H

x N

## Étape 4 : Les sprites

// On factorize les constantes :
const factor = SPRITE_HEIGHT * BAR_WIDTH / SPRITE_WIDTH;

const bars = values.map(({ value, count }, i) => {
// ...
const rows = Math.ceil(Math.sqrt(count * barHeight / factor));
const cols = Math.ceil(Math.sqrt(count * factor / barHeight));

return {
// ...
rows,
cols,
colWidth: BAR_WIDTH / cols,
rowHeight: barHeight / rows,
};
});

## Étape 4 : Les sprites

const spentValues = {};

lines.forEach((line, i) => {
const value = line[field];
const bar = barsDict[value];

// On détermine la case où on doit placer le sprite :
const index = spentValues[value];
const total = dict[value].count;
const col = index % bar.cols;
const row = Math.floor(index / bar.cols);

// On détermine les positions X et Y cible du sprite :
const sprite = sprites[i];
sprite.targetX = BORDER_MARGIN + bar.left + col * bar.colWidth;
sprite.targetY = sceneHeight - BORDER_MARGIN - row * bar.rowHeight;

// On incrémente le nombre de cases de la barre déjà remplie :
spentValues[value] = (spentValues[value] || 0) + 1;
});

## Étape 5 : Bonus

lines.forEach((line, i) => {
// ...
// On génère deux nombres aléatoire entre 1 et 21 :
const xRatio = Math.random() * 20 + 1;
const yRatio = Math.random() * 20 + 1;

// 1. On interpole vers la cible :
sprite.x = (sprite.x * xRatio + sprite.targetX) / (xRatio + 1);
sprite.y = (sprite.y * yRatio + sprite.targetY) / (yRatio + 1);

// 2. On ajoute une petite vibration :
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',
];

function _drawBody(ctx, person) {
ctx.fillStyle =
SKINS[Math.floor(Math.random() * SKINS.length)];

// Tête :
ctx.fillRect(1, 0, 4, 1);
ctx.fillRect(0, 1, 6, 3);

// Corps et bras :
ctx.fillRect(1, 4, 4, 1);
ctx.fillRect(0, 5, 6, 5);

// Jambes :
ctx.fillRect(1, 10, 1, 2);
ctx.fillRect(4, 10, 1, 2);
ctx.fillRect(5, 11, 1, 1);
}
const TSHIRTS = ['#eaeaea', '#242424', '#bababa'];
const SHIRTS = ['#6fd1ec', '#de96e3', '#bee89a'];
const VESTS = ['#5a4728', '#2f3045', '#562c2c'];

function _drawClothes(ctx, person) {
// Les jeunes sont 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 sont 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);
}

// ...
}

By Alexis Jacomy

# La dataviz pour les hipsters - v2, 50 minutes

Nantes.js Septembre 2017, Nantes

• 2,054