Alexis Jacomy
I am a data-visualization engineer from Nantes, France. I frequently speak in conferences and meetups about data visualization, web technologies, and networks mapping.
Nantes.js septembre 2017, Nantes
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.classList.add('point');
point.style.left = (vx / MAX_X * 100) + '%';
point.style.bottom = (vy / MAX_Y * 100) + '%';
POINTS.appendChild(point);
});
<!DOCTYPE html>
<html>
<head>
<style>
#points {
filter: url('#posterize');
}
</style>
</head>
<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>
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);
scene.add(slice);
acc += angle;
});
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;
}
// 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
);
}
// 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
);
const position = CENTER.clone().add(direction);
camera.position.set(...position.toArray());
camera.lookAt(CENTER);
renderer.render(scene, camera);
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 });
}
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;
});
});
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,
};
});
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
// 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,
};
});
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;
});
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;
scene.ticker.add(() => {
// 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;
});
});
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
Nantes.js Septembre 2017, Nantes
I am a data-visualization engineer from Nantes, France. I frequently speak in conferences and meetups about data visualization, web technologies, and networks mapping.