d3.js tutorijal: animirana interaktivna populaciona piramida 

18.11.2019. / 10:14
Share
Slika 1: Primer klasične populacione piramide za Sjedinjene američke države
Slika 1: Primer klasične populacione piramide za Sjedinjene američke države
Slika 1: Primer klasične populacione piramide za Sjedinjene američke države

Populacione piramide jedan su od najčešćih oblika statističkih grafikona. Zovemo ih piramidama jer su tradicionalno izgledale kao piramide. Na y-osi predstavljen je uzrast (dole su tek rođeni, gore su stari); na x-osi je raspodela, broj pripadnika populacije koji pripadaju određenom uzrastu, tj. starosnom opsegu. S jedne strane prikazani su muškarci, s druge žene. S obzirom da su tradicionalna društva imala daleko više tek rođenih nego onih najstarijih, ovaj dijagram raspodele izgledao je kao trougao ili piramida. U savremenim društvima, međutim, prirodni priraštaj po pravilu je nizak i populacione piramide uglavnom nemaju oblik trougla. 

 
Slika 1: Primer klasične populacione piramide za Sjedinjene američke države 

Klasična populaciona piramida razdvaja muškarce i žene na levu i desnu stranu. Ovaj način prikaza nije idealan; osim načelnog problema podrazumevanog kategoričkog rodnog dualizma (da li razlikujemo rod i pol? Šta se dešava sa transrodnim i transeksualnim osobama?), ima i sasvim praktičnih nedostataka: teško je uporediti broj muškaraca sa brojem žana u istom starosnom opsegu. Kako je mogućnost poređenja jedan od osnovnih ciljeva svake vizuelizacije podataka, jasno je da populacionoj piramidi predstoji redizajn.  

Piramida koju ćemo u ovom članku dizajnirati i programirati unapređena je verzija klasične piramide. Kreirana je uz pomoć tehnologije d3.js. Zasnovana je na dizajnerskom modelu Mikea Bostocka, autora d3.js biblioteke. Bostockova piramida vešto preklapa stupce za muškarce i žene, te se jasno vidi višak populacije koji ide na jednu ili drugu stranu. Za niže starosne opsege muškaraca je nešto više od žena, jer ih se više i rodi, ali oko 40. godine starosti žene preuzimaju primat. U poznijim godinama života ima znatno više žena nego muškaraca (veća je šansa da ćete doživeti 90 godina ukoliko ste žena). 

Baza podataka koju ćemo obrađivati preuzeta je od Republičkog zavoda za statistiku Srbije. Ovu bazu pretvorićemo u interaktivnu i animiranu grafiku uz pomoć tehnologije d3.js, JavaScript biblioteke za vizuelizaciju podataka na Webu.  

Evo kako izgleda konačni rezultat: 

 
Slika 2: Interaktivna populaciona piramida Republike Srbije 

 

Tamnozeleni pravougaonici na kraju stupca pokazuju da u datom starosnom opsegu muškarci čine većinu; žuti pravougaonici pokazuju da većinu čine žene. Kao i u slučaju SAD, prelomna tačka dešava se oko 40. godine starosti. 

Interaktivna i animirana verzija okačena je na server autora članka. Pritiskom na kursorske strelice na tastaturi (→ i ←) vizuelizacija prelazi sa jednog popisa na drugi. Tranzicije su animirane. Na ovaj način može se pratiti šta se dešava sa svakom generacijom u Republici Srbiji u poslednjih nešto više od pola stoleća. 

 

Podaci 

Za početak, preuzmite bazu podataka. U pitanju je jednostavan CSV fajl (Comma Separated Values) sa četiri kolone: godina popisa (year), uzrast (age), pol (sex) i broj stanovnika Republike Srbije koji pripada datoj grupi (people).  

year,age,sex,people 

1951,0,1,343261 

1951,0,2,329472 

1951,5,1,247844 

1951,5,2,238630 

... 

2011,80,1,67814 

2011,80,2,108754 

2011,85,1,27667 

2011,85,2,53883 

Kôd 1: Mali deo baze podataka (početak i kraj) 

 

Od kraja Drugog svetskog rata, u Republici Srbiji popisi su organizovani na 10 godina. (Izuzetak je 1951. godina: popis koji je tada trebalo da bude održan prebačen je za 1953. godinu; оvu nedoslednost ispravićemo u interfejsu vizuelizacije.) Za svaki od popisa uzimaju se opsezi uzrasta od 5 godina (0–4 godine, 5–9, 10–14, itd.). Pol je kodiran jedinicama i dvojkama (1 za muško, 2 za žensko). 

Napravite novi folder i u njega stavite fajl koji ste upravo preuzeli, population_serbia.csv. U tom istom folderu napravite još tri fajla: index.html, censusViz.css i censusViz.js. HTML, CSS i JS fajl napravite u svom omiljenom razvojnom okruženju ili editoru za tekst (Sublime TextAtomWebStorm, Notepad, itd). Za sada će ovi fajlovi ostaće prazni. 

Ukoliko radite u Windowsu, evo kako treba da izgleda vaš folder: 

 

 

HTML 

Svaka Web stranica sastoji se iz makar jednog HTML fajla. U strogom smislu, HTML nije programski nego opisni jezik koji sadrži deskripciju strukture i sadržaja sajta. Ova hijerarhijska struktura ugnježdenih tagova naziva se DOM model (Document Object Model). 

 

U index.html ukucajte sledeći kôd: 

<!DOCTYPE html> 

<head> 

  <meta charset="utf-8"> 

  <link rel="stylesheet" type="text/css" href="censusViz.css"> 

</head> 

<body> 

  <script src="http://d3js.org/d3.v3.min.js"></script> 

  <script src="censusViz.js"></script> 

</body> 

Kôd 2: index.html za naš primer 

 

Ovaj HTML fajl sadrži samo osnovnu strukturu, zajedno sa referencom na CSS fajl, censusViz.css, i dve reference na JS fajlove, samu biblioteku d3.js, kao i censusViz.js, JS skriptu koji mi pišemo. 

 

CSS 

Dok HTML fajlovi po pravilu opisuju sadržaj i strukturu dokumenta, CSS fajlovi opisuju njegov stil: boje, fontove, raspored (layout) elemenata, itd. U našem slučaju potrebno je da opišemo stilove za nekoliko klasa kojima ćemo označiti grafičke vektorske elemente u SVG formatu (Scalable Vector Graphics). Ove elemente generisaćemo uz pomoć JavaScripta i oni će postati deo DOM modela stranice. 

censusViz.css treba da sadrži sledeći kôd: 

svg { 

  font: 10px monospace; 

  fill: #009b5b; } 

.x.axis path { 

  display: none; } 

.x.axis line { 

  stroke: #fff; 

  stroke-opacity: .2; 

  shape-rendering: crispEdges; } 

.x.axis .zero line { 

  stroke: #000; 

  stroke-opacity: 1; } 

.title { 

  font: 300 24px monospace; 

  fill: #009b5b; 

  text-anchor: middle; } 

.birthyear, .age { 

  text-anchor: middle; } 

.birthyear { 

  fill: #fff; } 

rect {  

  fill-opacity: .5; 

  fill: #ffc600; } 

rect:first-child { 

  fill-opacity: .8; 

  fill: #009b5b; } 

Kôd 3: censusViz.css 

 

Već po nazivima klasa nije teško pretpostaviti na koje se grafičke elemente odnose: linije osa, brojeve na osama, naslov ili pravouganike koji čine stupce dijagrama. Ovi pravougaonici, definisani uz pomoć dva selektora, sadrže opis boja; ukoliko želite da promenite boju dijagrama, promenite njihove atribute fill i fill-opacity. 

 

JS 

censusViz.js treba da sadrži kôd za crtanje vizuelizacije. Ovaj kôd znatno je složeniji od HTML-a i CSS-a; zato ćemo ići korak po korak. 

Za početak, definišimo varijable kojima ćemo odrediti margine grafike koju crtamo, njenu širinu, visinu, tekst naslova, kao i širinu pojedinačnog stupca: 

var margin = {top: 20, right: 40, bottom: 30, left: 30}, 

width = 500 - margin.left - margin.right, 

height = 600 - margin.top - margin.bottom, 

titleString = "Popis ", 

barWidth = Math.floor(height / 18) - 1; 

Kôd 4: Početak fajla censusViz.js 

 

S obzirom da starosnih opsega ukupno ima 18, širinu pojedinačnog stupca dobijamo tako što ukupnu visinu podelimo sa 18. 

Zatim je potrebno da napravimo dve funkcije za mapiranje, x i y , u skladu sa sistemom biblioteke d3.js: 

var x = d3.scale.linear().range([0, width]); 

var y = d3.scale.linear().range([height - barWidth / 2, barWidth / 2]); 

Kôd 4: Funkcije za mapiranje 

 

Funkcije x i y mapiraju ulazne vrednosi (godine i broj stanovnika) na izlazne vrednosti (koordinate na ekranu). Da bi mapiranje funkcionisalo, x i y treba da imaju definisan domen (domain) i opseg (range). Domen se odnosi na ulazne podatke. Naš domen za x-osu definiše se uz pomoć minimalne i maksimalne vrednosti za broj stanovnika u bilo kojem starosnom opsegu. Za y-osu, domen predstavljaju godine za dati popis. S obzirom da ove vrednosti zavise od konkretne baze podataka, njih nećemo definisati sada, nego nakon što učitamo dataset, tj. CSV fajl. 

Opseg funkcija za mapiranje odnosi se na izlazne podatke, u ovom slučaju: piksele, dimenzije na ekranu. Opseg se određuje funkcijom range(). Za x, opseg ide od 0 do širine grafike (0 stanovnika mapira se na stupac širine 0, a maksimalan broj stanovnika u datasetu mapira se na stupac pune širine). Za y, opseg se definiše u odnosu na ukupnu visinu grafike i širinu pojedinačnog stupca. 

Nakon što smo definisali funkcije za mapiranje, možemo da definišemo x-osu, kojoj određujemo funkciju skaliranja (to će biti prethodno definisana funkcija x), orijentaciju (na dole) i veličinu podeoka: 

var xAxis = d3.svg.axis() 

.scale(x) 

.orient("bottom") 

.tickSize(height); 

Kôd 5: x-osa 

 

Sledeći korak: napraviti osnovni kontejner za SVG grafiku, zajedno sa promenljivama kojima definišemo klasu za stupce koji će predstavljati godine (birthyears), kao i naslov (title). 

var svg = d3.select("body").append("svg") 

.attr("width", width + margin.left + margin.right) 

.attr("height", height + margin.top + margin.bottom) 

.append("g") 

.attr("transform", "translate(" + margin.left + "," + margin.top + ")");  

var birthyears = svg.append("g") 

.attr("class", "birthyears"); 

var title = svg.append("text") 

.attr("class", "title") 

.attr("x", width/2) 

.attr("y", 0) 

.attr("dy", ".71em") 

.text(titleString + "1953"); 

Kôd 6: kreiranje osnovnog SVG kontejnera 

 

Napokon je došlo vreme da učitamo dataset. 

d3.csv("population_serbia.csv", function(error, data) { 

  data.forEach(function(d) { 

    d.people = +d.people; 

    d.year = +d.year; 

    d.age = +d.age; 

  }); 

  /* 

   * Ovde dolazi ostatak kôda 

   */ 

}); 

Kôd 7: Učitavanje CSV fajla 

 

Funkcija csv() je posebna funkcija tehnologije d3.js kojom se učitavaju CSV fajlovi. Nakon što se funkcija pozove, promenljiva data koristi se da bi se pristupilo podacima. Petlja forEach() služi tome da brojeve u CSV tabeli pretvorimo iz tekstualnog u brojčani format. 

Preostale komadiće sofvera do kraja ovog tutorijala ubacite na mesto komentara “Ovde dolazi ostatak kôda”. Bitno je da ostanu u okviru opsega funkcije csv(), jer koriste podatke povučene iz fajla – varijablu data. 

Na redu je određivanje domena funkcija za mapiranje, x i y, na osnovu minimuma i maksimuma za broj stanovnika i godine: 

  var age1 = d3.max(data, function(d) { return d.age; }), 

  year0 = d3.min(data, function(d) { return d.year; }), 

  year1 = d3.max(data, function(d) { return d.year; }), 

  year = year0; 

  x.domain([0, d3.max(data, function(d) { return d.people; })]); 

  y.domain([year0, year0 - age1]); 

Kôd 8: Određivanje domena funkcija x i y (ovaj kôd ostaje u opsegu funkcije csv()) 

 

Zatim ćemo grupisati podatke radi jednostavnije analize i prikaza. To radimo uz pomoć funkcija nest(), key() i rollup()

  data = d3.nest() 

  .key(function(d) { return d.year; }) 

  .key(function(d) { return d.year - d.age; }) 

  .rollup(function(v) { return v.map(function(d) { return d.people; }); }) 

  .map(data); 

Kôd 9: Grupisanje podataka 

 

Vraćamo se na SVG kontejner koji smo kreirali nekoliko redova ranije. SVG grafiku proširujemo (funkcija append()) grupom “g” koja će obuhvatiti grafiku koja obuhvata x-osnu. Takođe je pomeramo 10 piksela u stranu. 

  svg.append("g") 

  .attr("class", "x axis") 

  .attr("transform", "translate(0, 10)") 

  .call(xAxis); 

Kôd 10: Dodavanje x-ose u SVG kontejner 

 

Vreme je da nacrtamo pravouganone stupce, srž naše vizuelizacije. Svaki horizontalni stubac zapravo se sastoji od dva pravouganika (element rect): preklopljenog muškog i ženskog. Onaj koji viri definisaće dodatni prostor tamnozelene ili žute boje, na osnovu kojeg vidimo: (a) koga ima više u datom starosnom opsegu, muškaraca ili žena; i (b) koliko ih ima više. 

S obzirom da se svaki stubac sastoji od dva pravouganika, potrebno je da za svaki stubac najpre formiramo krovnu grafičku grupu “g”, te da u nju stavimo dva pravouganika, dva elementa rect. Moć d3.js-a ogleda u sistemu mapiranja podataka na dimenzije. Za početnike, on se može učiniti komplikovanim, ali ukoliko pravite iole složeniju vizuelizaciju, uverićete se koliko je ovaj sistem fleksibilan. Posebno obratite pažnju sa sistem selekcija, kao i na dodavanje atributa “x”, “y”, “width”, “height” i “transform”; definisani uz pomoć anonimnih funkcija, ovi atributi postaju dinamički i fleksibilni. 

  var birthyear = birthyears.selectAll(".birthyear") 

  .data(d3.range(year0 - age1, year1 + 1, 5)) 

  .enter().append("g") 

  .attr("class", "birthyear") 

  .attr("transform", function(birthyear) { 

    return "translate(0, " + y(birthyear) + ")";  

  }); 

  birthyear.selectAll("rect") 

  .data(function(birthyear) {  

    return data[year][birthyear] || [0, 0];  

  }) 

  .enter().append("rect") 

  .attr("y", -barWidth / 2) 

  .attr("height", barWidth) 

  .attr("x", 0) 

  .attr("width", function(value) { return x(value); }); 

Kôd 11: Dodavanje stubaca 

 

Dodavanje teksta za godine funkcioniše na sličan način: 

  svg.selectAll(".age") 

  .data(d3.range(0, age1 + 1, 5)) 

  .enter().append("text") 

  .attr("class", "age") 

  .attr("y", function(age) { return y(year - age); }) 

  .attr("x", -25) 

  .attr("dx", ".71em") 

  .text(function(age) { return age; }); 

Kôd 12: Dodavanje teksta za godine 

 

Kontrola korisničkih događaja (events) obavlja se uz pomoć funkcije on(). Svaki taster na tastaturi ima svoju šifru, svoj broj; šifra za levu kursorsku stralicu je 37, za desnu 39. Nakon što se odgovaraću taster pritisne, grafika se iznova iscrtava (funkcija update(), koja će biti objašnjena uskoro) a opsezi godina pomeraju se za 10 (na gore ili na dole), jer se popisi dešavaju na 10 godina. 

  d3.select(window).on("keydown", function() { 

    switch (d3.event.keyCode) { 

      case 37: year = Math.max(year0, year - 10); break; 

      case 39: year = Math.min(year1, year + 10); break; 

    } 

    update(); 

  }); 

Kôd 13: Definisanje događaja (event) koji obezbeđuje da se vizuelizacija menja kursorskim strelicama na tastaturi 

 

Na ovaj način prikazano je kako vremenski tok utiče na svaku generaciju, tj. šta se s njom dešava od jednog do drugog popisa. 

Možda se najsloženiji deo ovog primera odnosi na funkciju update(), koja kontroliše animaciju i interakciju: 

  function update() { 

    if (!(year in data)) return; 

    yearText = year; 

    if (year == 2001) { yearText = 2002; }  

    else if (year == 1951) { yearText = 1953; } 

    title.text(titleString + yearText); 

    birthyears.transition() 

    .duration(750) 

    .attr("transform", "translate(0, " + (y(year0) - y(year)) + ")"); 

    birthyear.selectAll("rect") 

    .data(function(birthyear) { return data[year][birthyear] || [0, 0]; }) 

    .transition() 

    .duration(750)     

    .attr("width", function(value) { return x(value); }); 

  } 

Kôd 14: Animirana tranzicija od popisa do popisa 

 

Funkcija update() najpre reguliše ažuriranje podataka koji se tiču godina popisa. Ukoliko se dođe do godine pre prvog popisa ili posle poslednjeg popisa, funkcija prestaje da se izvršava. Takođe se ispravljaju nepravilne godine popisa, tj. one koje ne odgovaraju ustaljenoj desetogodišnjoj šemi. 

Zatim se formira tekst naslova, tako što se reči “Popis” dodaje godina. 

Naposletku se animiraju tranzicije između stubaca, tako što im se ažurira i animira širina (width) i pozicija na y-osi (transform po vertikalnoj osi). 

Ne zaboravite da se i funkcija update() nalazi unutar opsega funkcije csv(). 

 

Server 

Da bi vam primer radio, potrebno je da ga pokrenete sa lokalnog servera. Bez brige: pokrenuti server na lokalnom kompjuteru, tj. vašem kompjuteru, nije teško. Možete koristiti serverski softver po želi. Mi vam preporučujemo XAMPP

Kako bismo vam olakšali pokretanje servera, pripremili smo vam uputstva za podešavanje

 

Tehnologija vs. interpretacija 

Čestitamo: napravili ste svoju animiranu i interaktivnu populacionu piramidu. :)  

Većina digitalnih data-dizajnera svoj posao ovde proglase završenim, međutim, to nije tako. Posao dizajnera nije tek da vizuelizaciju isprogramira, nego da podatke razume i na pravi ih način predstavi – da ispriča priču. Drugim rečima, dizajner se ne bavi samo tehnologijom i izgledom, nego i interpretacijom. 

Šta možemo da zaključimo posmatrajući ovu vizuelizaciju? Pre svega, populaciona piramida Republike Srbije ima oblik piramide, ali izvrnute: više je starih nego mladih. Ukoliko se pogleda istorijski tok, takva situacija postaje upadljiva posle osamdesetih godina. Na starim popisima vidljive su dve pukotine, dva udubljenja: njih čine oni koji su 1961. imali 15 i 40 godina – deca, dakle, rođena tokom I i II svetskog rata. Ukoliko bismo sebi dozvolili površnu publicističku interpretaciju, mogli bismo kandidovati zaključak da su ratovi, nacionalistička divljanja i neoliberalna tranzicija unazadile populaciju Srbije više nego I i II svetski rat zajedno.  

Treba međutim biti oprezan: ozbiljna interpretacija ovog grafikona posao je za sociologe i demografe. U idealnom slučaju, autor vizuelizacije sarađuje sa domenskim stručnjacima, te oni zajedno, timski, pišu scenario za vizuelizaciju zajedno sa modelima interakcije. 

Ukoliko se pojavi mogućnost za tako nešto, našu vizuelizaciju mogli bismo obogatiti dodatnim slojem interpretacije. Takođe bi bilo zanimljivo dodati još podataka. Na primer, šta je sa emigracijom, možemo li razlikovati emigrirale od preminulih? Takođe, kako izgledaju projekcije za budućnost? Šta će se desiti 2021. ili 2031. godine? Kako obrnuta populaciona piramida utiče na ekonomiju i blagostanje? Zašto se javnost, kada je reč o prirodnom priraštaju, češće poziva na brigu o tzv. opstanku nacije, umesto da se govori o ekonomskim pokazateljima? Možemo li govoriti o populacionoj piramidi a da ne upadnemo u zamku kolektivne etničke frustracije? Napokon, koji su to modeli kojima se možemo boriti protiv negativnih populacionih procesa i kakvu ulogu tu mogu da odigraju, primera radi, migracije iz trećeg sveta u naše krajeve? Možda su migranti šansa za Balkan a ne opasnost? 

Sva ova pitanja zaslužuju ozbiljnu analizu i razradu koja prevazilazi mogućnosti dizajnera vizuelizacije podatka, ali sama vizuelizacija – pod uslovom da je kreirana pametno i timski – može biti sjajan medij da prava pitanja otvori i artikuliše. 

U najgorem slučaju, vizuelizacija podataka puka je propaganda, ali u najboljem, ona je slika sveta koja poziva da se on promeni. 

(Napomena: kôd za primer iz ovog teksta možete preuzeti besplatno sa autorovog servera.) 

 

Ocijenite kvalitet članka