Grafice cu d3.js - part 2
Data trecută am făcut un barchart simplu, care să poată fi sortat. Acum, să îl complicăm un pic: vrem să prezentăm un set de date, care este împărțit pe mai multe grupe, datele având mai multe dimensiuni. Vrem să vedem cum se compară grupele diferite la diferite dimensiuni. Vom reprezenta datele din fiecare dimensiune în câte un barchart, separat pentru fiecare grupă sau mai multe împreună, schimbarea făcându-se alegând grupele care să apară. Când schimbăm în cadrul aceleiași grupe, indivizii vor avea persistență, adică caracteristica din a doua dimensiune va fi reprezentată în locul unde a fost reprezentată caracteristica corespunzătoare primei dimensiuni.
În HTML vom pune butoanele pentru sortare și filtrare. Nothing fancy.
<button id="sort">
Sort
</button>
</p>
<select id="grupa" multiple> <option value="Grupa 1" selected>Grupa
1</option> <option value="Grupa 2" selected>Grupa 2</option>
<option value="Grupa 3" selected>Grupa 3</option> </select>
<select id="dim"> <option value="dim1">Dimensiunea 1</option>
<option value="dim2">Dimensiunea 2</option> </select>
Setul de date pe care îl vom reprezenta în acest tutorial va conține 3 grupe și 2 dimensiuni, pentru simplitate, dar poate fi ușor extins. (3 grupe din anul meu cu notele la 2 materii)
var data = {"Grupa 1":
[{"dim1":6,"dim2":6},{"dim1":10,"dim2":10},{"dim1":3,"dim2":4},{"dim1":9,"dim2":8},{"dim1":0,
"dim2":6},{"dim1":4,"dim2":5},{"dim1":5,"dim2":5},{"dim1":6,"dim2":8},{"dim1":8,"dim2":7},{"dim1":0,"dim2":4},
{"dim1":5,"dim2":7},{"dim1":9,"dim2":9},{"dim1":0,"dim2":4},{"dim1":6,"dim2":5},{"dim1":3,"dim2":4},{"dim1":10,
"dim2":6},{"dim1":7,"dim2":4},{"dim1":7,"dim2":9},{"dim1":9,"dim2":10},{"dim1":0,"dim2":4},{"dim1":9,
"dim2":5},{"dim1":8,"dim2":7},{"dim1":7,"dim2":8},{"dim1":5,"dim2":7},{"dim1":0,"dim2":4},{"dim1":7,
"dim2":7},{"dim1":4,"dim2":0}],
"Grupa 2":
[{"dim1":4,"dim2":6},{"dim1":5,"dim2":4},{"dim1":5,"dim2":0},{"dim1":7,"dim2":6},{"dim1":6,"dim2":6},
{"dim1":9,"dim2":8},{"dim1":7,"dim2":9},{"dim1":9,"dim2":7},{"dim1":10,"dim2":10},{"dim1":7,"dim2":8},
{"dim1":8,"dim2":6},{"dim1":5,"dim2":7},{"dim1":5,"dim2":6},{"dim1":6,"dim2":10},{"dim1":3,"dim2":0},
{"dim1":7,"dim2":7},{"dim1":7,"dim2":6},{"dim1":6,"dim2":6},{"dim1":0,"dim2":6},{"dim1":4,"dim2":0},
{"dim1":0,"dim2":4},{"dim1":4,"dim2":4},{"dim1":0,"dim2":4}],
"Grupa 3":
[{"dim1":6,"dim2":7},{"dim1":4,"dim2":6},{"dim1":6,"dim2":4},{"dim1":0,"dim2":4},{"dim1":6,"dim2":6},
{"dim1":6,"dim2":5},{"dim1":7,"dim2":7},{"dim1":4,"dim2":4},{"dim1":0,"dim2":6},{"dim1":7,"dim2":8},
{"dim1":8,"dim2":7},{"dim1":4,"dim2":5},{"dim1":4,"dim2":7},{"dim1":5,"dim2":4},{"dim1":7,"dim2":8},
{"dim1":4,"dim2":6},{"dim1":5,"dim2":5},{"dim1":9,"dim2":4},{"dim1":0,"dim2":4},{"dim1":6,"dim2":4},
{"dim1":4,"dim2":6},{"dim1":6,"dim2":4},{"dim1":0,"dim2":4}]}
Cum datele noastre nu mai sunt într-un simplu array, ci sunt grupate, iar d3.js lucrează în principal cu array-uri, va trebui să punem într-un array datele care ne interesează.
var note = data["Grupa 1"].concat(data["Grupa 2"]).
concat(data["Grupa 3"])
var accesor = function(d) { return d.dim1 }
l = note.length
for (var i = 0; i< l; i++) {
note[i].index = i;
}
La început, lucrăm cu toate datele. Cum datele noastre sunt sub forma unui obiect, trebuie să îi zicem la d3.js care din atributele acestor obiecte să le folosească pentru valori. Așa că definim o funcție de accesare care pentru parametrul de intrare returnează prima dimensiune. De asemenea, cum avem multe date care nu sunt tot timpul afișate, ca d3.js să poată face corespondența între ce este deja afișat și ce trebuie adăugat sau scos, adăugăm un index fiecărui obiect.
Urmează apoi creare scalelor, a axelor și a containerului SVG. Nu îl mai postez aici pentru că e la fel cu celălalt. Și acum începe iar distracția. Pentru că avem multe date, s-ar putea să nu încapă pe ecran și va trebui să facem scrolling. Pentru că SVG îi... nașpa, trebuie să introducem un element în plus care să prindă evenimentele de mouse (sau touch :-" ).
var z = d3.behavior.zoom().scaleExtent([1,1])
.on("zoom", zoom)
svg.append("rect")
.attr("class","panning")
.attr("fill","white")
.attr("width",w)
.attr("height",h)
.attr("pointer-events","all")
.call(z)
d3.js are integrată capabilitatea de a prelucra evenimentele de mouse, atât ca și mișcare ca și scrolling. Acestea sunt obținute prin „behavior”-ul zoom, care cheamă un callback în care avem acces la datele despre eveniment, cum ar fi cât s-a deplasat mouseul și cât s-a scrolluit. Cum noi nu avem nevoie de redimensionare, fixăm scara la 1 și apoi aplicăm această funcție dreptunghiului creat special pentru a prinde evenimentele de translatare.
function zoom() {
var dx = d3.event.translate[0];
var max = d3.max(x.range()) - w + 40
if (max \< 0) {
return
}
if (dx \> 0) {
dx = 0
}
if (dx \< -max) {
dx = -max
}
scrollTo(dx)
}
function scrollTo(dx) {
z.translate([ dx, 0])
rect.attr("transform", "translate(" + dx + ",0)")
svg.select(".x.axis").attr("transform", "translate(" + dx + ")")
}
Partea de translatare este împărțită în două părți: una de calcul al valorii la panning și una de mișcare (care este refolosită altundeva). Deplasarea (absolută) pe X o obținem din d3.event.translate[0], iar apoi verificăm să nu fi ajuns în afara limitelor (înainte sau după grafic). În scrollTo facem mișcarea graficului și a axelor și setăm și valoarea pe care o ține minte d3.js legată de translatare (pentru cazul în care am ajuns la limită, să nu țină el minte că a trecut mai încolo).
var rect = svg.append("svg")
.attr("class","container")
.attr("viewbox","0 0 "+ w + " " + h)
.selectAll("rect")
.data(note,function(d) {return d.index})
Când creăm containerul pentru bare, față de data trecută trebuie să specificăm limitele între care se poate vedea prin el, pentru ca atunci când facem scroll să nu fie elemente puse aiurea, de exemplu înainte de axa verticală.
Față de data trecută, acum nu mai facem nimica direct cu datele, ci punem totul într-o funcție, pentru că mai încolo vom refolosi aceasta când schimbăm care date le afișăm.
function addData() {
var newElems = rect.enter().append("rect")
.attr("pointer-events","none")
.attr("x",function(d,i) { return x(i) })
.attr("width", x.rangeBand())
.attr("y",h)
.attr("height",0)
modify(newElems)
}
function modify(elems) {
elems.transition()
.duration(1000)
.attr("height",function(d) { return h - y(accesor(d))} )
.attr("y",function(d) { return y(accesor(d))})
.attr("x",function(d,i) { return x(i) })
.style("fill",function(d) {
if (accesor(d) == Math.ceil(d3.median(note,accesor)))
return "\#81642A"
else if ( accesor(d) \< 5)
return "\#d62728";
else
return "\#2ca02c"
})
}
Funcția addData se ocupă strict de adăugarea elementelor noi la grafic (cam la fel ca data trecută, doar că mai adăugăm o proprietate legată de mouse clicks), pe când modify aplică tranziția de poziționare și dimensionare la elementele date ca parametru - dacă e apelată din addData, atunci o aplică elementelor noi, iar mai încolo o vom folosi la schimbarea între dimensiuni. În rest e la fel cu ce am făcut data trecută.
function removeData() {
rect.exit()
.transition()
.attr("y",h)
.attr("height",0)
}
Aici trebuie să dau câteva explicații. Ați observat până acuma funcția enter imediat după data, iar acum apare și exit. Ce sunt acestea? Well, în d3.js există conceptul de join (nu ca cele din SQL :)) ) care specifică cum se îmbină două seturi de date.
Stări posibile la reuniunea de date. Thinking with joins
În momentul în care legăm date de o selecție d3.js, ele sunt împărțite în 3 părți: enter, pentru datele noi, modify pentru datele care au același index (sau funcție de indexare), și exit pentru datele care nu se mai regăsesc între cele noi. Când adăugăm pentru prima dată date, toate intră în enter. Dar când le schimbăm, pot apărea toate cazurile. În cazul nostru noi ștergem elementele cu o animație scurtă.
d3.selectAll("#dim").on('change',function(){
var materie = this.value
accesor = function(d) { return d[materie]}
rect = rect.data(note,function(d) {return d.index})
modify(rect)
})
d3.select("#grupa").on('change',function() {
note = []
var options = this.options;
var opt;
for (var i=0, iLen=options.length; i<ilen; i++) {<br></ilen;> opt =
options[i];
if (opt.selected) {
note = note.concat(data[opt.value]);
}
}
index = d3.range(note.length)
setAxis()
rect = rect.data(note,function(d) {return d.index})
scrollTo(0)
modify(rect)
removeData()
addData()
})
Și acum să tratăm schimbările de date. Dacă se schimbă dimensiunea pe care vrem să o afișăm (schimbăm materia) atunci trebuie să schimbăm funcția accesor, ca această să returneze materia nouă. Legăm datele din nou de selecția și apoi aplicăm funcția de modificare, descrisă mai sus.
Când schimbăm grupele, parcurgem selectul ca să vedem care sunt selectate și le concatenăm notele. Resetăm axele (probabil avem alt număr de date) și indexul, legăm datele noi, mergem la poziția 0 (s-ar putea să fi fost la sfârșitul unei chartului și dacă nu am mai avea suficiente bare și după schimbare să ne trezim cu un ecran alb) și apoi aplicăm funcțiile de modificare, ștergere și adăugare.
Și în mare... aceasta ar fi. Sortarea e la fel ca data trecută. Și cu asta avem un grafic destul de drăguț (în opinia mea) cu care putem să explorăm datele noastre multidimensionale și multicategoricale.
Totul poate fi văzut la un loc aici.