Notebooks 2.0 is here.

Published
Edited
Apr 1, 2022
1 star
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
d3 = require('d3@6')
Insert cell
Insert cell
import {Scrubber} from "@mbostock/scrubber"
Insert cell
Insert cell
Insert cell
running = d3.csvParse(await FileAttachment("running@2.csv").text(), d => {
return {
date: new Date(d.date),
distance: +d.distance,
pace: d.pace,
vitesse: speed(d.distance, duration(timeParse(d.minutes))) + " km/h" // variable ajoutée, calculée grâce aux fonctions détaillées ci-dessous
}
})
Insert cell
Insert cell
Insert cell
// Fonction pour transformer en format date / heure
timeParse = d3.timeParse("%H:%M:%S");
Insert cell
// Fonction permettant de calculer une durée (convertit les minutes et secondes en heures)
function duration(date) {
return date.getHours() + date.getMinutes()/60 + date.getSeconds()/3600;
}
Insert cell
// Fonction permettant de calculer une distance (avec arrondi à un chiffre décimal utile pour l'affichage dans les futures interractions).
function speed(distance, time) {
return Math.round((distance / time) * 10) / 10; // calculer la vitesse d'une course
}
Insert cell
Insert cell
Insert cell
margin = ({top: 10, right: 40, left: 40, bottom: 30}) // on créé un objet contenant les propriétés sur les marges
Insert cell
Insert cell
innerHeight = height - margin.top - margin.bottom
Insert cell
innerWidth = width - margin.left - margin.right
Insert cell
height = 250
Insert cell
Insert cell
delay=1000
Insert cell
Insert cell
Insert cell
xScale = d3.scaleTime() // échelle temporelle
.domain(d3.extent(running, d => d.date)) // étendue de la variable date dans nos données (min et max)
.range([0, innerWidth]); // largeur de l'axe des abcisses
Insert cell
Insert cell
xAxis = g => g
.attr('transform', "translate(0, "+ innerHeight + ")")
// syntaxe initiale donnée par l'auteur (équivalente) :
//.attr('transform', `translate(0,${innerHeight})`)
.call(d3.axisBottom(xScale).tickSizeOuter(0)) // créé l'axe horizontal en bas avec l'échelle donnée dans xScale
.call(g => g.select('.domain').remove()); // permet d'enlever le trait de l'axe des dates
Insert cell
Insert cell
circleScale = d3.scaleLinear()
.domain(d3.extent(running, d => d.distance)) // étendue de la variable dans nos données (distance min et max)
.range([5, 25]) // les plus petites distances seront représentées par un disque de rayon 5 et les plus grandes par un disque de rayon 25
Insert cell
Insert cell
Insert cell
force = d3.forceSimulation(running)
.force('charge', d3.forceManyBody().strength(0)) // les éléments ne s'attirent pas et ne se repoussent pas car la force d'attraction / répulsion vaut 0
.force('x', d3.forceX().x(d => xScale(d.date))) // permet d'attirer éléments vers la bonne date
.force('y', d3.forceY().y(height / 2)) // attirance constante peu importe le point car la position en ordonnée ne dépend pas des données (le graphique ne compte qu'un seul axe)
.force('collision', d3.forceCollide().radius(d => d.distance)) // empêche les cercles de se chevaucher et on reprécise que le rayon correspond à la distance courue
Insert cell
Insert cell
Insert cell
Insert cell
graph = {
const svg = d3.create('svg') //création de l'objet svg et définition de sa taille
.attr('viewBox', [0, 0, width, height]);
const wrapper = svg.append('g')
.attr('transform', `translate(${margin.left}, ${margin.top})`); //ajout des marges dans la définition de l'image
wrapper.append('g')
.call(xAxis); // ajout de l'axe des abscisses
const circles = wrapper.selectAll('circle') // création de cercles pour chaque course du jeu de données
.data(running) // à partir de la table running importée
.join('circle')
.attr('r', d => d.distance) // définition des attributs des cercles (donne comme valeur du rayon du cercle, la distance parcourue par course)
.attr('fill', d => d.distance > 16 ? '#2779BD' : '#FFED4A') // vous pouvez fixer la distance à partir de laquelle le couleur est bleue
// détermine la couleur de remplissage des cercles; #2779BD pour le bleu et #FFED4A pour le jaune
.attr('stroke', '#684F1D'); // définition de la couleur du contour des cercles; ici #684F1D est la couleur marron
d3.timeout(() => {
for (var i = 0, n = Math.ceil(Math.log(force.alphaMin()) /
Math.log(1 - force.alphaDecay())); i < n; ++i) { // boucle qui permet l'application des forces sur les cercles
force.tick(); //force appliquée sur le noeud
circles
.attr('cx', d => d.x) //position en abscisse du noeud
.attr('cy', d => d.y); //position en ordonnée du noeud
}
})
return Object.assign(svg.node(), {force}); //permet l'affichage de l'image
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
viewof dist = Scrubber(Array.from(d3.group(running, d => d.distance), ([key]) => key).sort(d3.ascending), {delay, loop: false})
Insert cell
Insert cell
tutograph = {
const svg = d3.create('svg')
.attr('viewBox', [0, 0, width, height]);
const wrapper = svg.append('g')
.attr('transform', `translate(${margin.left}, ${margin.top})`);
wrapper.append('g')
.call(xAxis);
d3.timeout(() => {
const circles = wrapper.selectAll('circle')
.data(running)
.join('circle')
.attr('r', d => d.distance)
// NOUVEAU :
.attr('fill', d => d.distance > dist ? '#2779BD' : '#FFED4A') //assigner une couleur en fonction de la distance
//
.attr('stroke', '#684F1D');

for (var i = 0, n = Math.ceil(Math.log(force.alphaMin()) /
Math.log(1 - force.alphaDecay())); i < n; ++i) {
force.tick();
circles
.attr('cx', d => d.x)
.attr('cy', d => d.y);
}
})
return Object.assign(svg.node(), {force});
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
function affiche_vitesse(){
let g = d3.select(this) //

let circle = g.select("circle") // sélectionner un cercle
let text = g.select("text") // ajouter du texte à g

let is_unselected = circle.attr("stroke-width") == 0.4 // on fixe l'épaisseur du contour des cercles à 0.4

circle.attr("stroke-width", is_unselected ? 4 : 0.4) // modifier l'épaisseur du contour du cercle si il est sélectionné
text.attr("display", is_unselected ? "inherit" : "none") // afficher un texte (ici la valeur de la vitesse) si le cercle est sélectionné
};
Insert cell
tutograph2 = {
const svg = d3.create('svg')
.attr('viewBox', [0, 0, width, height]);
const wrapper = svg.append('g')
.attr('transform', `translate(${margin.left}, ${margin.top})`);

// A partir de cette ligne, on a choisie d'adapter le code de l'auteur en lui donnant une syntaxe plus familière avec une boucle:)
for(let r of running){
let g = wrapper.append("g")
g
.append("circle")
.datum(r)
.attr('cx', d => d.x)
.attr('cy', d => d.y)
.attr('r', d => d.distance)
.attr('fill', d => d.distance > size ? '#2779BD' : '#FFED4A') //assigner une couleur en fonction de la distance
.attr('stroke', 'black')
.attr("stroke-width", 0.4)

g
.append("text")
.datum(r)
.attr("x", d => d.x)
.attr("y", d => d.y -40)
.attr("text-anchor", "middle")
.attr("font-size",13)
.attr("display", "none")
.text(d => d.vitesse)
g // code ajouter pour permettre l'interaction
.style("cursor", "pointer") // pointer le curseur sur un cercle
.on("click", affiche_vitesse) // lancer la fonction vitesse et obtenir l'affichage de la vitesse liée à chaque course
}
const circles = wrapper.selectAll("circle");
const texts = wrapper.selectAll("text");

wrapper.append('g')
.call(xAxis);

for (var i = 0, n = Math.ceil(Math.log(force.alphaMin()) /
Math.log(1 - force.alphaDecay())); i < n; ++i) {
force.tick();
};
return Object.assign(svg.node(), {force});
}
Insert cell
Insert cell
tutograph3 = {
const svg = d3.create('svg')
.attr('viewBox', [0, 0, width, height]);
const wrapper = svg.append('g')
.attr('transform', `translate(${margin.left}, ${margin.top})`);
wrapper.append('g')
.call(xAxis);
const text = svg.append("text")
.text("Vitesse:")
.attr("x", 80 ) // positionnement du text sur l'image
.attr("y", 50)
.attr("font-size", 15)
function do_something(d){ //fonction permettant de sélectionner un cercle et récupérer son attribut de vitesse
let circle = d3.select(this)
let is_unselected = circle.attr("stroke-width") == 0.4
circle.attr("stroke-width", is_unselected ? 4 : 0.4)
let vitesse = circle.attr('v') // récupère la vitesse liée à chaque course
const message = "Vitesse : " + vitesse // affiche la vitesse correspondante
text.text(message)
};

d3.timeout(() => {
// boucle qui excécute les forces pour chaque point
const circles = wrapper.selectAll('circle')
.data(running)
.join('circle')
.attr('v', d => d.vitesse) // ajout de l'attribut vitesse pour chaque cercle, on aurait pas eu besoin de le faire si on voulait afficher les distances car elles sont déja définies comme rayon des cercles
.attr('r', d => d.distance)
.attr('fill', '#FFED4A') // attribut de couleur de remplissage des cercles; on aurait pu laisser la condition d => d.distance > size ? '#2779BD' : '#FFED4A' en plus
.attr('stroke', 'black') //la couleur du contour des cercles
.attr('stroke-width', '0.4')
.style("cursor", "pointer"); // pointer vers un cercle donné
for (var i = 0, n = Math.ceil(Math.log(force.alphaMin()) /
Math.log(1 - force.alphaDecay())); i < n; ++i) {
force.tick(); // permet d'exécuter les param définis dans forcesimulation
circles
.attr('cx', d => d.x)
.attr('cy', d => d.y)
.on('click', do_something)
}});
return Object.assign(svg.node(), {force});
}
Insert cell
Insert cell
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more