Skip to content

Instantly share code, notes, and snippets.

@Fil
Last active September 7, 2016 13:28
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Fil/e681ce83b87a26e6e78edb82b42e9444 to your computer and use it in GitHub Desktop.
Save Fil/e681ce83b87a26e6e78edb82b42e9444 to your computer and use it in GitHub Desktop.
General Update Pattern, tutoriel interactif
license: mit
height: 640

Une exploration interactive du general update pattern de D3 (version 4). Utilisez les flèches du clavier pour navigeur dans les lignes de code.

Le but est de montrer une boucle de mise à jour de données, en avançant pas à pas dans les lignes du code. Des détails, comme l'endroit où l'on positionne les éléments, sont omis, de manière à pouvoir se concentrer sur 1) ce qui est sélectionné et 2) ce qui existe dans le DOM.

Version originale réalisée par Chris Given sur une idée d'Alex Engler. Traduit par Philippe Rivière. Suggestions et remarques bienvenues.

<!DOCTYPE html>
<meta charset="utf-8">
<style>
body > div { float: left; width: 472px; }
#code { font-size: 16px; }
#description {
box-sizing: border-box;
width: 100%;
height: 235px;
background: #cef;
padding: 16px;
position: relative;
}
.step-group {
background: #eee;
border-top: 1px solid #fff;
padding: 6px 0;
}
.step-group.active { background: #cef; }
pre {
margin: 0;
padding: 2px 8px;
}
pre.step.active {
background: #0ad;
color: #fff;
}
text {
font-family: monospace;
font-size: 16px;
}
.node text {
text-transform: uppercase;
font-size: 30px;
font-weight: 700;
text-anchor: middle;
}
circle.selected {
fill: none;
stroke: #0ad;
stroke-width: 3;
}
.step-description { display: none; }
.step-description.active { display: block; }
h2 {
font-family: monospace;
float: left;
margin: 0;
}
p {
float: left;
margin: .25em 0 0 1em;
max-width: 42em;
line-height: 1.35;
font-family: sans-serif;
}
button {
position: absolute;
bottom: 16px;
right: 16px;
margin: 0 auto;
padding: 1em 1em;
background: #0ad;
color: #fff;
border: 0;
font-size: 16px;
font-weight: 700;
cursor: pointer;
}
</style>
<body>
<div id="code">
<div class="step-group">
<pre>function update(data) {</pre>
</div>
<div class="step-group">
<pre id="step-1" class="step update-selected exit-selected">
var update = svg.selectAll('circle')
</pre>
<pre id="step-2" class="step data-bound update-selected">
.data(data, function (d) { return d })
</pre>
</div>
<div class="step-group">
<pre id="step-3" class="step data-bound enter-selected">
var enter = update.enter()
</pre>
<pre id="step-4" class="step data-bound enter-selected enter-circle">
.append('circle')
</pre>
</div>
<div class="step-group">
<pre id="step-5" class="step data-bound enter-circle exit-selected">
var exit = update.exit()
</pre>
</div>
<div class="step-group">
<pre id="step-6" class="step data-bound update-selected update-fill enter-circle">
update.style('fill', 'black')
</pre>
</div>
<div class="step-group">
<pre id="step-7" class="step data-bound update-fill enter-selected enter-circle enter-fill">
enter.style('fill', 'green')
</pre>
</div>
<div class="step-group">
<pre id="step-8" class="step data-bound update-fill enter-circle enter-fill exit-selected exit-fill">
exit.style('fill', 'red')
</pre>
<pre id="step-9" class="step data-bound update-fill enter-circle enter-fill exit-selected exit-fill exit-remove">
.remove()
</pre>
</div>
<div class="step-group">
<pre id="step-10" class="step data-bound update-selected update-fill enter-selected enter-circle enter-fill exit-remove">
update.merge(enter)
</pre>
<pre id="step-11" class="step data-bound update-selected update-fill enter-selected enter-circle enter-fill exit-remove pulse">
.call(pulse)
</pre>
</div>
<div class="step-group">
<pre>}</pre>
</div>
</div>
<div id="results"></div>
<div id="description">
<div id="step-description-1" class="step-description">
<h2>.selectAll()</h2>
<p>Commençons par sélectionner tous les cercles qui existent dans la page. On le fait même si on sait qu'il n'en existe pas encore, car l'opération permet de créer une sélection vide, dans laquelle on pourra créer les cercles dont on aura besoin.</p>
<button onclick="step(2)">Étape suivante</button>
</div>
<div id="step-description-2" class="step-description">
<h2>.data()</h2>
<p>On indique à D3 quels sont les cercles qu'on veut faire exister, en lui indiquant nos données, sous la forme d'une liste de lettres tirées au hasard entre A et H. Deux remarques : 1. Le second argument est une <em>fonction de clé</em>, qui indique à D3 comment reconnaître un élément d'un appel de la fonction update() à l'autre; dans ce cas, il s'agit simplement de l'identité (la lettre elle-même), mais si les variables d'un élement évoluent au cours du temps, on fournira à la place un identifiant unique. 2. La sélection est modifiée ; les cercles qui existaient déjà et correspondent à des données présentes dans data sont sélectionnés. Ces cercles constituent la variable "update", qui contient la sélection des éléments à <em>mettre à jour</em>.</p>
<button onclick="step(3)">Étape suivante</button>
</div>
<div id="step-description-3" class="step-description">
<h2>.enter()</h2>
<p>On affiche dans le coin en haut à gauche les lettres qui figurent dans data, mais qui n'ont pas encore de représentation sur la page. En appelant .enter() sur la variable update, on sélectionne ces cercles non-existants, et puis&hellip;</p>
<button onclick="step(4)">Étape suivante</button>
</div>
<div id="step-description-4" class="step-description">
<h2>.append()</h2>
<p>&hellip;on crée une représentation pour chacun d'entre eux. Le résultat forme notre <em>sélection des éléments à créer</em>, qui contient chaque élément nouveau n'existait pas avant l'appel à .data().</p>
<button onclick="step(5)">Étape suivante</button>
</div>
<div id="step-description-5" class="step-description">
<h2>.exit()</h2>
<p>De la même manière, on va créer une <em>sélection des éléments qui disparaissent</em>, en appelant .exit() sur la variable update. Cette sélection contient tous les cercles qui existent mais ne figurent plus dans les données, et dont il va falloir se débarrasser. En utilisant ces sélections, ce qui s'affiche dans le navigateur reste cohérent avec les données. Ces différentes sélections constituent également un moyen pratique de créer des transitions pour afficher les éléments qui entrent et qui sortent de la page au fur et à mesure des variations dans les données.</p>
<button onclick="step(6)">Étape suivante</button>
</div>
<div id="step-description-6" class="step-description">
<h2>.style()</h2>
<p>Chacune de ces sélections peut être manipulée, soit seule soit en combinaison avec les autres. Montrons par exemple les cercles qui existaient déjà en les coloriant en noir.</p>
<button onclick="step(7)">Étape suivante</button>
</div>
<div id="step-description-7" class="step-description">
<h2>.style()</h2>
<p>Montrons les nouveaux cercles en vert.</p>
<button onclick="step(8)">Étape suivante</button>
</div>
<div id="step-description-8" class="step-description">
<h2>.style()</h2>
<p>Les cercles qui sont sur le point de sortir vont être affichés en rouge.</p>
<button onclick="step(9)">Étape suivante</button>
</div>
<div id="step-description-9" class="step-description">
<h2>.remove()</h2>
<p>Ces cercles rouges n'ont pas le droit de rester dans la page, on appelle .remove() pour les supprimer.</p>
<button onclick="step(10)">Étape suivante</button>
</div>
<div id="step-description-10" class="step-description">
<h2>.merge()</h2>
<p>Certaines opérations concernent plusieurs sélections, on ne va pas les effectuer indépendamment. En utilisant .merge(), on peut combiner deux sélections. Cela nous permet d'appliquer une opération sur l'ensemble des cercles restants, ceux qui étaient déjà présents et ceux qu'on vient d'ajouter.</p>
<button onclick="step(11)">Étape suivante</button>
</div>
<div id="step-description-11" class="step-description">
<h2>.call(pulse)</h2>
<p>Dans cet exemple, on appelle une fonction personnalisée qui provoque une petite pulsation sur l'ensemble des cercles des sélections enter et update. Puis, quand de nouvelles données arrivent, on peut reboucler sur l'ensemble de ce processus.</p>
<button onclick="loop()">Relancer la fonction Update()</button>
</div>
</div>
<script src="//d3js.org/d3.v4.min.js"></script>
<script>
var width = 480
var height = 405
var radius = 26
var alphabet = 'abcdefgh'.split('')
var activeStep
var stepCount = 11
var svg = d3.select('#results').append('svg')
.attr('width', width)
.attr('height', height)
var dataLabel = svg.append('text')
.attr('x', 12)
.attr('y', 23)
var nodes = []
var enterNodes = []
var updateNodes = []
var update = svg.selectAll('.node').data(nodes)
var enter = update.enter()
var exit = update.exit()
var simulation = d3.forceSimulation()
.on('tick', ticked)
.force('centerX', d3.forceX(width / 2).strength(0.05))
.force('centerY', d3.forceY(height / 2).strength(0.05))
.force('collide', d3.forceCollide(function (d) { return d.active ? radius + 6 : 0 }))
.nodes(nodes)
loop()
function loop() {
step(10)
enter.each(function (d) { d.oldFill = 'green' })
update.each(function (d) { d.oldFill = 'black' })
exit.remove()
nodes = updateNodes
updateNodes = []
enterNodes = []
var letters = d3.shuffle(alphabet)
.slice(0, Math.floor(Math.random() * alphabet.length) + 1)
.sort()
letters.forEach(function (letter) {
var node = nodes.find(function (d) { return d.letter === letter })
if (!node) {
node = { letter: letter }
enterNodes.push(node)
}
updateNodes.push(node)
})
nodes = nodes.concat(enterNodes)
update = svg.selectAll('.node').data(updateNodes, function (d) { return d.letter })
enter = update.enter().append('g').attr('class', 'node')
.each(function (d) { d.oldFill = '#aaa' })
exit = update.exit()
enter.append('circle')
.attr('class', 'bg')
.attr('r', 0)
enter.append('text')
.attr('dy', '.25em')
.attr('opacity', 0)
.text(function (d) { return d.letter })
enter.append('circle')
.attr('class', 'selected')
.attr('r', radius + 4)
.attr('stroke-dasharray', '6,5')
.attr('opacity', 0)
dataLabel.text('data = ["' + letters.join('", "') + '"]')
simulation
.nodes(nodes)
.alpha(1)
.restart()
step(1)
}
function step(_) {
var el, number
if (typeof _ === 'number') {
el = d3.select('#step-' + _)
number = _
} else {
el = _
number = parseInt(_.attr('id').split('-')[1], 10)
}
activeStep = number
d3.selectAll('.step-group').classed('active', false)
d3.selectAll('.step').classed('active', false)
d3.selectAll('.step-description').classed('active', false)
el.classed('active', true)
d3.select(el.node().parentNode).classed('active', true)
d3.select('#step-description-' + number).classed('active', true)
var state = d3.set(el.attr('class').split(' '))
var i = 0
enter.each(function (d) {
d.fx = state.has('enter-circle') ? null : (++i) * radius + 4
d.fy = state.has('enter-circle') ? null : radius * 2 + 8
d.active = state.has('enter-circle')
})
exit.each(function (d) { d.active = !state.has('exit-remove') })
if (!state.has('pulse')) {
enter.merge(update).selectAll('.bg').interrupt()
}
enter.selectAll('.bg')
.transition()
.attr('r', function () { return state.has('enter-circle') ? radius : 0 })
.style('fill', function (d) { return state.has('enter-fill') ? 'green' : d.oldFill })
update.selectAll('.bg')
.transition()
.style('fill', function (d) { return state.has('update-fill') ? '#000' : d.oldFill })
exit.selectAll('.bg')
.transition()
.attr('r', function () { return !state.has('exit-remove') ? radius : 0 })
.style('fill', function (d) { return state.has('exit-fill') ? 'red' : d.oldFill })
enter.selectAll('text')
.attr('opacity', function () { return state.has('data-bound') ? 1 : 0 })
.style('fill', function () { return state.has('enter-circle') ? '#fff' : '#000' })
exit.selectAll('text')
.attr('opacity', function () { return state.has('exit-remove') ? 0 : 1 })
enter.selectAll('.selected')
.attr('opacity', function () { return state.has('enter-selected') ? 1 : 0 })
update.selectAll('.selected')
.attr('opacity', function () { return state.has('update-selected') ? 1 : 0 })
exit.selectAll('.selected')
.attr('opacity', function () { return state.has('exit-selected') && !state.has('exit-remove') ? 1 : 0 })
if (state.has('pulse')) {
enter.merge(update)
.selectAll('.bg')
.attr('r', radius)
.transition()
.on('start', function repeat() {
d3.active(this).attr('r', radius - 2)
.transition().duration(600).attr('r', radius + 2)
.transition().on('start', repeat)
})
}
simulation.nodes(nodes)
if (simulation.alpha() < 0.3) { simulation.alpha(0.3).restart() }
}
function ticked() {
svg.selectAll('.node')
.attr('transform', function (d) { return 'translate(' + d.x + ',' + d.y + ')' })
}
window.focus()
d3.select(window).on('keydown', function () {
var number = activeStep
switch (d3.event.keyCode) {
case 37: number -= 1; break // left
case 38: number -= 1; break // up
case 39: number += 1; break // right
case 40: number += 1; break // down
}
if (number !== activeStep) {
d3.event.preventDefault()
if (number > stepCount) { loop() } else if (number > 0) { step(number) }
}
})
d3.selectAll('.step').on('mouseover', function () { step(d3.select(this)) })
</script>
</body>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment