D3.js Data-Driven Documents

JavaScript biblioteka za vizualizaciju podataka pomoću web standarda. D3 oživljava podatke pomoću SVG-a, Canvas-a i HTML-a. D3 kombinira moćne tehnike vizualizacije i interakcije s pristupom koji se temelji na podacima i manipulaciji DOM-om, pružajući pune mogućnosti modernih preglednika i slobodu dizajniranja pravog vizualnog sučelja za podatke. Prikladan za kreiranje grafova, mapa, ili bilo koje druge reprezentacije podataka.

Za bolje praćenje ovog tutorijala, poželjno je bazično znanje HTML-a, CSS-a, JavaScript-a

Za početak potrebno je uključiti D3.js biblioteku u HTML stranicu

<script src="https://d3js.org/d3.v5.min.js"></script>

Nakon uključivanja bibiloteke dobiva se pristup globalnom d3 objeku u JavaScript kodu.

Najčešće korišten koncept su Selektori. Slično kao i kod JQuery i AngularJs bibiloteka, bazirani su na W3C selectors API.

D3 ima dvije metode selekcije: select za selekciju jednog DOM elementa i selectAll za višestruku selekciju.

Selektori se mogu ulančavati. Na primjer, može se selektirati svu djecu div elementa čiji je id container

// selektiraj dom element sa id-jem container
// te zatim selektiraj svu div djecu tog elementa
d3.select("#container").selectAll("div")

Neki od jednostavnih selektora su sljedeći

d3.select("tag") // gdje je tag ime tag-a DOM elementa, kao što su bodi ili div
d3.select("#id") // gdje id reprezentira točno odreženi DOM element
d3.select(".class") // gdje je class CSS klasa DOM elementa

Nakon selekcije elemenata, na njima se obavljaju razne operacije kao što su mijenjanje svojstava (CSS klasa i stilova, textova, itd.).

Primjer korištenja selektora select

Uzmimo da imamo sljedeći DOM element u HTML stranici

<div id="select-div">
</div>

naredni kod tada selektira div prema id-u, te mijenja njegovu pozadinu u plavu boju

d3.select("#select-div").style("background-color", "#039BE5");

Primjer korištenja selektora selectAll

Uzmimo da imamo sljedeće DOM elemente u HTML stranici

<div class="select-all-div"> Div #1 </div>
<div class="select-all-div"> Div #2 </div>
<div class="select-all-div"> Div #3 </div>

naredni kod tada mijenja pozadinu elementi u nasumičnu boju, na način da dohvati sve elemente sa CSS klasom select-all-div te za svaku od njih poziva funkciju koja dinamički nasumično računa pozadinsku boju

Parametri za funkciju su sljedeći:

d - podaci (eng. data) - koji se mogu predati selekciji - više u nastavku

i - index trenutnog elementa, počevši od nule

d3.selectAll(".select-all-div")
.style("background-color", function (d, i) {
return "hsl(" + Math.random() * 360 + "," + 10 * (i + 1) + "%,50%)";
});

Koncept Ulaz/Ažuriranje/Izlaz

D3.js omogućuje povezivanje podataka s selekcijom pomoću funkcije podataka i dinamičkih vrijednosti.

U sljedećem primjeru ažuriraju se svi elementi sa klasom .item DIV-ovi sadržani u roditelju sa klasom .selection.

Primjer html-a

<div class="selection">
<div class="item">1</div>
<div class="item">2</div>
<div class="item">3</div>
</div>

Prilikom selekcije elemenata postavljaju se predefinirane vrijednosti pomoću funkcije data. Prilikom postavljanja pozadinske boje za trenutni element u funkciji se dobiva jedan od elemenata predanih data funkciji, ovisno o poziciji trenutnog elementa.

d3.select(".selection")
.selectAll(".item")
.data(["#039BE5", "#00897B", "#00ACC1"])
.style("background-color", function (d) {
return d;
});

Ukoliko je predano više podataka nego što ima elemenata, ti su podaci jednostavno zanemareni. Isto vrijedi i u suprotnom. Ako ima manje podataka nego elemenata u selekciji, višak selekcije je zanemaren.

Ako imamo slučaj nejednakog broja podataka i selektiranih elemenata onda u tom slučaju možemo dodavati/brisati elemente.

Dodavanje elemenata:

Za kreiranje novog DOM elementa koristi se enter() funkcija koja manipulira sa podacima koji ulaze u selekciju.

// selekcija spremljena u div variablu
var div = d3.select(".selection")
.selectAll(".item")
.data(["#039BE5", "#00897B", "#00ACC1","#C7254E"]);
// za elemente koji ulaze
div.enter()
// dodaj div element u selekciju
.append("div")
// postavi njegovu CSS klasu na item
.classed("item", true)
// postavi text novog elementa na index + 1
.text(function (d, i) {
return i + 1;
})
// animarija tranzicije
.transition()
// koja traje 0.5 sekunde
.duration(500)
// ažuriraj svaki novi element sa novom pozadinskom bojom
.style("background-color", function (d) {
return d;
});
// također potrebno je ažurirati i sve postojeće elemente
div.transition()
.duration(500)
.style("background-color", function (d) {
return d;
});

Brisanje elemenata:

Za brisanje viška DOM elementa koristi se exit() funkcija koja manipulira sa podacima koji izlaze iz selekcije.

// selekcija spremljena u div variablu
var div = d3.select(".selection")
.selectAll(".item")
.data(["#039BE5", "#00897B"]);
// izbrisi sve elemente koji izlaze iz selekcije
div.exit().remove();
// i naravno potrebno je ažurirati postojeće elemente
div.transition()
.duration(500)
.style("background-color", function (d) {
return d;
});

Primjer

U sljedećem primjeru prikazano dodavanje novog svg elementa u body element, te generiranje i dodavanje krugova u taj svg

// dimenzije prikaza
var width = 960,
height = 400;
// selekcijom dohvati element body
var svg = d3.select("body")
// na njega dodaj novi element svg
.append("svg")
// te postavi dimenzije tog svg elementa
.attr("width", width)
.attr("height", height);
// generiraj raspon od 200 brojeva
var nodes = d3.range(200)
// mapiraj svaki broj kao element sa poljem radius koji se računa nasumično
.map(function() {
return {radius: Math.random() * 12 + 4};
}),
// prvvi čvor je korijen čija će pozicija odgovarati poziciji kurzora na ekranu
root = nodes[0],
// skala boja
color = d3.scaleOrdinal(d3.schemeCategory10);
// korijen nema radius te na njega ne djeluju nikakve sile
root.radius = 0;
root.fixed = true;
// napravi selekciju na sve elemente circle
// naravno elemenata još nema
svg.selectAll("circle")
// podaci su svi čvorovi osim prvog(korijena)
.data(nodes.slice(1))
// za sve elemente koji ulaze
.enter()
// dodaj novi element cricle
.append("circle")
// postavi atribut r na radius čvora iz kolekcije predane data funkciji
.attr("r", function(d) { return d.radius; })
// ispuni krug sa jednom od prvih 3 boje iz prije definirane palete boja
.style("fill", function(d, i) { return color(i % 3); });
// kreiranje nove simulacije generiranih čvorova
var force = d3.forceSimulation(nodes)
// vrijednost između 0 i 1 koja reprezentira preostalo vrijeme izvođenja simulacije
.alpha(0.3)
// za svaki čvor postavi naboj - svi čvorovi imaj naboj -15 osim nultog čvora, odnosno korijena koji ima naboj -2000
// negativan naboj znači da će se čvorovi odbijati, što je veća vrijednost veće je odbijanje
// cilj je da se svi čvorovi jako odbijaju od korijena
.force("charge", d3.forceManyBody().strength(function(d, i){return i ? -15 : -2000}))
// forsiraj sve čvorove prema sredini svg elementa
.force('center', d3.forceCenter(width / 2, height / 2))
// nemoj primjenjivati sile ni po x ni po y osim
.force("x", d3.forceX(function(){return 0}))
.force("y", d3.forceY(function(){return 0}))
// svi čvorovi se mogu sudarati jedni s drugima te je radius u kojem se čvorovi sudaraju zaparavo radius čvora
.force('collision', d3.forceCollide().radius(function(d) {
return d.radius
}));
// registriraj slušač na svaku iteraciju tajmera simulacije
force.on("tick", function(e) {
// zato što je pri definiranju simulacije definirano
// kako se čvorovi grupiraju u sredini te
// kako se odbijaju jedan od drugog za -15 te svi od korijena za -2000
// i kako se čvorovi sudaraju međusobno (nemogu pregaziti jedan drugog)
// dovoljno je samo ažurirati pozicije svih krugova u svg elementu
// zato što se sve operacije računanja koordinata izvšavaju u samoj simulaciji prema zadanim parametrima
svg.selectAll("circle")
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
});
// regist slušač na pokrete kurzora unutar svg elementa
svg.on("mousemove", function() {
// dohvati trenutnu poziciju kurzora
var p1 = d3.mouse(this);
// postavi korijen na trenutnu poziciju kurzora
// zato što korijen ima jako velik negativan naboj i nalazi se na istoj pozziciji kao i kurzor
// stvara se osjećaj kako se svi čvorovi odbijaju od kurzora
root.x = p1[0];
root.y = p1[1];
// simulacija po default-u završava za 300 iteracija
// zato je potrebno "resetirati" preostali broj iteracija simulacije
force.alpha(0.3);
});

Grafovi

Stupčasti graf

Pretpostavimo da imamo sljedeće podatke zapisane u csv datoteci

year value
2011 45
2012 47
2013 52
2014 70
2016 78
2015 75

Kao i uvijek potrebno je dodati novi svg element ili selektirati postojeći

// dimenzije prikaza
var width = 960,
height = 400,
//margina oko grafa
margin = 200;
// selekcijom dohvati element body
var svg = d3.select("body")
// na njega dodaj novi element svg
.append("svg")
// te postavi dimenzije tog svg elementa
.attr("width", width)
.attr("height", height);
// smanji visinu i širinu za iznos margine
width -= margin;
height -= margin;

Nakon toga potrebno je kreirati skale za x i y os. Sljedeći kod prikazuje način kreiranja raspona vrijednosti za svaku od osi.

// kreira se diskretna skala sa rasponom od 0 do duljine grafa
// sa funkcijom padding dodaje se razmak između stupaca
var xScale = d3.scaleBand().range ([0, width]).padding(0.4),
// kreira se linearna skala koja će prikazivati cijene
// u rasponu visine grafa
yScale = d3.scaleLinear().range ([height, 0]);

Na svg element dodaje se element g u koji će se dodati x i y osi i stupci grafa

// na svg element dodaj novi element g (group)
var g = svg.append("g")
//CSS attribut kako bi se graf pozicionirao sa marginom
.attr("transform", "translate(" + 100 + "," + 100 + ")");

Sljedeći korak je učitavanje podataka sa servera (u ovom primjeru iz csv datoteke)

// čitaj csv datoteku "data.csv"
d3.csv("data.csv")
// ako je pronađena i uspješno pročitana
.then(function(data) {
// radi sa podacima
})
// uhvati i obradi grešku
.catch(function(error){ throw error;});

Dohvaćeni podaci koriste se za izgradnju grafa. Variabla data predstavlja referencu na kolekciju podataka pročitanih iz csv datoteke.

Nakon što su podaci učitani, na x i y osi postavljamo vrijednosti domena.

// na skalu x osi postavi sve godine
xScale.domain(data.map(function(d) { return d.year; }));
// na skalu y osi postavi raspon od 0 do maximalne vrijednosti u podacima
yScale.domain([0, d3.max(data, function(d) { return d.value; })]);

Nakon postavljanja vrijednosti skala, dodaj ih u graf. Najprije se u prethodno kreirani element g dodaj novi element g koji će sadržavati x i y osi.

// dodaj novi element g
g.append("g")
// koristi CSS svojstvo kako bi pozicionirao x os prema dnu svg-a
.attr("transform", "translate(0," + height + ")")
// dodaj skalu u novi g element
.call(d3.axisBottom(xScale));
// dodaj novi element g
g.append("g")
// dodaj skalu u novi g element
.call(d3.axisLeft(y)
// zato što y os prikazuje vrijednost onda definiraj vlastiti format labela prikazanih na y osi
.tickFormat(function(d){ return "$" + d; })
// ograniči broj prikazanih vrijednosti labela y osi
.ticks(10))

Sljedeći korak je dodavanje podataka u graf. Sljedeći isječak prikazuje prikaz podataka dohvaćenih prije prikazanom metodom čitanja csv datoteke.

// napravi selekciju na sve elemente sa style klasom bar
g.selectAll(".bar")
// kao podatke prenesi podatke pročitane iz csv datoteke
.data(data)
// za sve podakte koji ne postoje (za sve nove elemente)
.enter()
// dodaj novi element rect(pravokutnik)
.append("rect")
// postavi style klasu novog rect elementa na bar
.attr("class", "bar")
// svaki bar postavlja na pripadajuću x i y poziciju
.attr("x", function(d) { return xScale(d.year); })
.attr("y", function(d) { return yScale(d.value); })
// širina stupaca određena je scaleBand() funkciom.
// Stoga, x skala vraća izračunatu širinu iz raspona(range) i paddinga predanih x skali
.attr("width", xScale.bandwidth())
// visina stupaca je izračunata kao visina yScale(d.value).
// To jest visina svg-a minus trenutna y vrijednost stupca sa y skale.
// Napomena: y vrijednost je vrh stupca
.attr("height", function(d) { return height - yScale(d.value); });

Nakon što su podaci dodani u graf potrebno je te podatke označiti labelama. Labele se u graf dodaje dodavanjem text elemenata u svg.

Sljedeći isječak prikazuje dodavanje naslova, te labela za x i y os

// Naslov
// dodaj novi text element u svg
svg.append("text")
// postavi css svojstvo transform
.attr("transform", "translate(100,0)")
// postavi ga na poziciju 50, 50
.attr("x", 50)
.attr("y", 50)
// povećaj veličinu fonta
.attr("font-size", "24px")
// postavi stvarni tekst koji će element prikazivati
.text("CSV Foods Stock Price")
// x-os
// nadovezujemo se na prije objašnjen kod za dodavanje x skale
g.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(xScale))
// na postojeću x skalu dodaj novi text element
.append("text")
// postavi x i y poziciju
.attr("y", height - 250)
.attr("x", width - 100)
// postavi css svojstva
.attr("text-anchor", "end")
.attr("stroke", "black")
// postavi tekst koji će se prikazivati
.text("Year");
// y-os
// nadovezujemo se na prije objašnjen kod za dodavanje y skale
g.append("g")
.call(d3.axisLeft(yScale)
.tickFormat(function(d){
return "$" + d;
}).ticks(10))
//na postojeću y skalu dodaj novi text element
.append("text")
// rotiraj text za 90 stupnjeva obrnuto smjera kazaljke
.attr("transform", "rotate(-90)")
// postavi y poziciju
.attr("y", 6)
// postavi css svojstva
.attr("dy", "-5.1em")
.attr("text-anchor", "end")
.attr("stroke", "black")
// postavi tekst koji će se prikazivati
.text("Stock Price");

Statički graf u današnje vrijeme je dosta dosadan. Potrebno je napraviti različite animacije kako bi korisniku bio zanimljviji.

Sljedeći isječak prikazuje dodavanje animacija na prethodno kreiran graf. Konkretno, registriraju se slušači za pokrete miša preko stupaca grafa.

// kod za kreiranje grafa prije objašnjen
g.selectAll(".bar")
.data(data)
.enter()
.append("rect")
.attr("class", "bar")
.attr("x", function(d) { return x(d.year); })
.attr("y", function(d) { return y(d.value); })
.attr("width", x.bandwidth())
.attr("height", function(d) { return height - y(d.value); })
// kada korisnik mišem uđe unutar granica novog elementa rect pozovi funkciju onMouseOver
.on("mouseover", onMouseOver)
// kada korisnik mišem iziđe izvan granica novog elementa rect pozovi funkciju onMouseOut
.on("mouseout", onMouseOut)
// animiraj dodavanje novih elemenata tranzicijom
.transition()
// linearno pojavljivanje novih elemenata
.ease(d3.easeLinear)
// u trajanju od 400 milisekundi
.duration(400)
// odgodi svako sljedeće dodavanje novog elementa za 50 milisekundi
.delay(function (d, i) {
return i * 50;
});

Slušač onMouseOver povećava širinu i visinu stupca te mijenja boju stupca u narančastu. Također se prikazuje i y vrijednost stupca kao tekst.

// funkcija prima parametre
// @d = podaci vezani za element
// @i = index elementa
function onMouseOver(d, i) {
// odaberi trenutni element za koji je pozvan ovaj slušač
d3.select(this)
// postavi mu style klasu highlight
.attr('class', 'highlight')
// animacija tranzicije
.transition()
// u trajanju od 400 milisekundi
.duration(400)
// povećaj širinu za 5 pixela
.attr('width', xScale.bandwidth() + 5)
// povećaj visinu za 10 pixela
.attr("height", function(d) { return height - yScale(d.value) + 10; })
// pomakni y poziciju grafa 10 prema dolje
.attr("y", function(d) { return yScale(d.value) - 10; });
// na g element dodaj novi text element
g.append("text")
// dodaj style klasu val
.attr('class', 'val')
// x pozicija je ista kao i kod stupca
.attr('x', function() {
return xScale(d.year);
})
// y pozicija elementa je za 15 pixela manja od vrha
.attr('y', function() {
return yScale(d.value) - 15;
})
// u tekst postavi vrijednost
.text(function() {
return [ '$' +d.value];
});
}

Slušač onMouseOut poništava sve napravljene promjene.

// funkcija prima parametre
// @d = podaci vezani za element
// @i = index elementa
function onMouseOut(d, i) {
// odaberi trenutni element za koji je pozvan ovaj slušač
d3.select(this)
// vrati natrag style klasu bar
.attr('class', 'bar')
// animacija tranzicije
.transition()
// u trajanju od 400 milisekundi
.duration(400)
// postavi širinu na početnu širinu
.attr('width', xScale.bandwidth())
// postavi visinu na početnu vrijednost
.attr("height", function(d) { return height - yScale(d.value); })
// vrati y poziciju na početnu vrijednost
.attr("y", function(d) { return yScale(d.value); });
// odaberi text element koji je prikazivao vrijednost stupaca
d3.selectAll('.val')
// izbrisi sve odabrane elemente
.remove()
}

Konačni kod za primjer stupčastog grafa je sljedeći:

var width = 960,
height = 400,
margin = 200;
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height);
width -= margin;
height -= margin;
var xScale = d3.scaleBand().range ([0, width]).padding(0.4),
yScale = d3.scaleLinear().range ([height, 0]);
var g = svg.append("g")
.attr("transform", "translate(" + 100 + "," + 100 + ")");
d3.csv("data.csv")
.then(function(data) {
xScale.domain(data.map(function(d) { return d.year; }));
yScale.domain([0, d3.max(data, function(d) { return d.value; })]);
g.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(xScale))
.append("text")
.attr("y", height - 250)
.attr("x", width - 100)
.attr("text-anchor", "end")
.attr("stroke", "black")
.text("Year");
g.append("g")
.call(d3.axisLeft(yScale)
.tickFormat(function(d){ return "$" + d; })
.ticks(10))
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", "-5.1em")
.attr("text-anchor", "end")
.attr("stroke", "black")
.text("Stock Price");
g.selectAll(".bar")
.data(data)
.enter()
.append("rect")
.attr("class", "bar")
.attr("x", function(d) { return xScale(d.year); })
.attr("y", function(d) { return yScale(d.value); })
.attr("width", xScale.bandwidth())
.attr("height", function(d) { return height - yScale(d.value); })
.on("mouseover", onMouseOver)
.on("mouseout", onMouseOut)
.transition()
.ease(d3.easeLinear)
.duration(400)
.delay(function (d, i) {
return i * 50;
});
svg.append("text")
.attr("transform", "translate(100,0)")
.attr("x", 50)
.attr("y", 50)
.attr("font-size", "24px")
.text("CSV Foods Stock Price")
})
.catch(function(error){ throw error;});
function onMouseOver(d, i) {
d3.select(this)
.attr('class', 'highlight')
.transition()
.duration(400)
.attr('width', xScale.bandwidth() + 5)
.attr("height", function(d) { return height - yScale(d.value) + 10; })
.attr("y", function(d) { return yScale(d.value) - 10; });
g.append("text")
.attr('class', 'val')
.attr('x', function() {
return xScale(d.year);
})
.attr('y', function() {
return yScale(d.value) - 15;
})
.text(function() {
return [ '$' +d.value];
});
}
function onMouseOut(d, i) {
d3.select(this)
.attr('class', 'bar')
.transition()
.duration(400)
.attr('width', xScale.bandwidth())
.attr("height", function(d) { return height - yScale(d.value); })
.attr("y", function(d) { return yScale(d.value); });
d3.selectAll('.val')
.remove()
}

Pita graf

Pretpostavimo da imamo sljedeće podatke zapisane u csv datoteci

browser percent
Chrome 73.70
IE/Edge 4.90
Firefox 15.40
Safari ,3.60
Opera 1.00

Pita graf gradimo na sličan način kao i prethodni graf.

var width = 500,
height = 400,
radius = Math.min(width, height) / 2;
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height);
var g = svg.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
// kreiranje skale boja za vrijednosti
var color = d3.scaleOrdinal(['#4daf4a','#377eb8','#ff7f00','#984ea3','#e41a1c']);
// funkcija koja će vraćat vrijednost (percent) podatka d
var pie = d3.pie().value(function(d) {
return d.percent;
});
// kreiraj novi luk
var path = d3.arc()
// sa vanjskim radiusom za 10 manjim od stvarnog
.outerRadius(radius - 10)
// i unutarnjim radiusom od 0
.innerRadius(0);
// kreiraj novi luk
var label = d3.arc()
// sa vanjskim radiusom radius
.outerRadius(radius)
// i unutarnjim radiusom za 80 manjim od stvarnog
.innerRadius(radius - 80);
//ovime se postiglo pozicioniraje labela blizu ruba grafa
d3.csv("browseruse.csv")
.then(function(data) {
// dodaj sve podatke
var arc = g.selectAll(".arc")
.data(pie(data))
.enter()
.append("g")
.attr("class", "arc");
// na svaki element dodaj element path
arc.append("path")
// postavi postavke radiusa kao attribut d
.attr("d", path)
// popuni sa bojom iz skale
.attr("fill", function(d) { return color(d.data.browser); })
// kada korisnik prijeđe mišom dodaj style klasu highlight
.on("mouseover", function(d,i){
d3.select(this).attr("class","highlight");
})
// kada korisnik pomakne miš iz elementa postavi style klasu na null, čime se automatski čita fill svojstvo
.on("mouseout", function(d,i){
d3.select(this).attr("class",null);
});
// dodaj labele u svaki luk
arc.append("text")
// translatiraj labelu na odgovarajuću poziciju
.attr("transform", function(d) {
return "translate(" + label.centroid(d) + ")";
})
// postavi tekst labele
.text(function(d) { return d.data.browser; });
})
.catch(function(error){console.log(error)});
Što dalje?

Provjeriti primjere na d3js.org. Pokušati prebaciti primjere na najnoviju verziju v5.

AutorRobert Šajina