Üks parimaid näiteid html5 ja Javaskripti v'õimalustest on Hans Rosling-i animeeritud mullidiagrammi video.
Tavaline (joon) graafik esitab kahe attribuudi vahelist seost : X (näiteks aeg/aasta) ja Y (näiteks merepinna kõrgus).
Värvi lisamine kasutamine võimaldab lisada veel ühe attribuudi - näiteks erinevad riigid, seega saab näidata iga elaniku poolt toodetud majandusprodukti (GDP per capita) muutumist aastate jooksul erinevates riikides.
Kui joone asemel kasutada ringi/mulli, saab mulli suurusega illustreerida veel üht muutujat ja kui graafik animeerida (näidata aega animatsioonina), võib X-teljel näidata mingit teist attribuuti aja asemel.
Mullidiagramm (tasapinnaline, s.t. 2D) v'õimaldab peale x,y koordinaatide demonstreerida veel kaht parameetrit - neid iseloomustavad mulli värv ja suurus; kui diagramm on animeeritud (muutub ajas), siis kokku viis parameetrit.
Täiendame eelnevas vaadeldud diagrammi skripti.
Andmed loetakse csv (comma separated values) failist - csv on tavaline ascii tekstifail (kõik selle elemendid on stringid!), kus kahemõõtmelise tabeli reas olevad andmed on komadega eraldatud (sageli kasutatakse eraldajana ka tabuleerimissümbolit). Sellise faili võib saada näiteks World DataBank lehelt. Siit andmefaili saamiseks tuleb valida algul andmebaas, siis uuritavad muutujad (variables) ja siis määrata faili formateering.
Valime esimese andmebaasi: World Development Indicators & Global Development Finance
nüüd valime maad - (näiteks) Croatia,Estonia, Finland, Greece,Germany; maid võib olla ka rohkem, kuid kogu tabel tuleb liiga suur ja selle käsitlemine liiga aeglane. Teeme graafiku üldisema: kasutaja saab ise valida, milliseid maid vaadata.
Järgmiseks - indikaatorid (Series), indikaatoreid tuleb valida kolm - X,Y telgede ja mulli jaoks; (näiteks) :
"Life expectancy at birth, total (years)" - X-teljele
"Birth rate, crude (per 1,000 people)" - Y-teljele
"GDP per capita (constant 2000 US$)" - mull
Kuna graafikul näidatakse erinevaid maid ja mitme aasta näitajaid (animeeritud), näidatakse kokku viit parameetrit.
GDP per capita (sisemajanduse kogutoodang inimese kohta) ütleb, kui palju iga selle maa elanik toodab aastas uusi väärtusi - see määrab ka elanike elatustaseme, sündide indeks näitab, kas maa elanike arv kasvab (> 1) või kahaneb (< 1); kui kahaneb, siis keskmine eluiga näitab, kui kiiresti.
Muutujate valikul tuleb mõelda, millist neist näidatakse X-teljel, millist Y-teljel ja milline määrab mulli suuruse. Mulli värvi määrab maa.
Järgmiseks - aastad, näiteks 1995.. 2011 (need on äärmised väärtused, kus Eesti kohta on andmed)
Kuna andmetes võib olla auke, tuleb enne mahalaadimist tabelit vaadata ja kontrollida, kas kõigi valitud muutujate andmed kõigi aastate jaoks on olemas (sellepärast ongi valitud just need aastad). Andmete olemasolu saab kontrollida, minnes "Format Report" ja valides "View Data" - see avab eraldi akna, kus iga maa andmeid saab vaadata; valikut/andmeid saab parempoolsest menüüst editeerida.
Kui andmed on aukudeta, klõpsame 'Download', valime formaadiks 'Tabbed TXT' (csv-formaat, kuid failinime laiendiks on .txt), 'Names only', 'Text field delimeter: None' (oluline!) ja salvestame mahalaaditud txt-faili oma html-dokumendi kataloogi. Kuna faili nimi on pikk ja keeruline, on parem see lihtsustada, näit data.txt.
Tekstina näeksid andmed välja nii (need on minimaalsed andmed - mitte need, mida kasutatakse kõrvaloleval demol):
Country Name,Country Code,Indicator Name,Indicator Code,1999,2004,2008 Estonia,EST,GDP per capita (constant 2000 US$),NY.GDP.PCAP.KD,3765.939084,5682.608021,7023.88193 Estonia,EST,Health expenditure per capita (current US$),SH.XPD.PCAP,243.7692108,445.6779954,1057.761804 Estonia,EST,"Industry, value added (% of GDP)",NV.IND.TOTL.ZS,26.90852658,27.90049435,29.1419 Estonia,EST,"Expenditure per student, tertiary (% of GDP per capita)",SE.XPD.TERT.PC.ZS,31.81502,17.58431,22.1165 Finland,FIN,GDP per capita (constant 2000 US$),NY.GDP.PCAP.KD,22386.62957,25774.06477,28789.54385 Finland,FIN,Health expenditure per capita (current US$),SH.XPD.PCAP,1864.050642,2954.010467,4253.864436 Finland,FIN,"Industry, value added (% of GDP)",NV.IND.TOTL.ZS,34.04850306,32.50371331,32.05045378 Finland,FIN,"Expenditure per student, tertiary (% of GDP per capita)",SE.XPD.TERT.PC.ZS,40.48563,36.04171,32.45631
ja tabelina nii (tabelina saab vaadata näit Excelis):
Country Name | Country Code | Indicator Name | Indicator Code | 1999 | 2004 | 2008 |
Estonia | EST | GDP per capita (constant 2000 US$) | NY.GDP.PCAP.KD | 3765.939084 | 5682.608021 | 7023.88193 |
Estonia | EST | Health expenditure per capita (current US$) | SH.XPD.PCAP | 243.7692108 | 445.6779954 | 1057.761804 |
Estonia | EST | Industry, value added (% of GDP) | NV.IND.TOTL.ZS | 26.90852658 | 27.90049435 | 29.1419 |
Estonia | EST | Expenditure per student, tertiary (% of GDP per capita) | SE.XPD.TERT.PC.ZS | 31.81502 | 17.58431 | 22.1165 |
Finland | FIN | GDP per capita (constant 2000 US$) | NY.GDP.PCAP.KD | 22386.62957 | 25774.06477 | 28789.54385 |
Finland | FIN | Health expenditure per capita (current US$) | SH.XPD.PCAP | 1864.050642 | 2954.010467 | 4253.864436 |
Finland | FIN | Industry, value added (% of GDP) | NV.IND.TOTL.ZS | 34.04850306 | 32.50371331 | 32.05045378 |
Finland | FIN | Expenditure per student, tertiary (% of GDP per capita) | SE.XPD.TERT.PC.ZS | 40.48563 | 36.04171 | 32.45631 |
Seega tabeli rida on ka tekstis rida (reavahetustega eraldatud), rea ruutud eraldatud kas tabuleemissümbolite või komadega (siin: tabuleerimissümbolitega).
Animatsiooni jaoks on tarvis siit "välja võtta" tulbad alates viiendast - need esitavad korraga näidatavaid (sama aasta) andmeid.
Javaskriptis kasutamiseks peab seda formaati muutma: kogu tabel on massiiv, read - massivi elemendid - on taas massivid, seega iga tabeli ruut muutub Javascriptis kahe indeksi abil adresseeritavaks: esimene on rida, teine - veerg.
Editeerime faili nagu eelnevas: asendame reavahetused stringiga *RV* (Notepad++-is : "View all symbols", "Search - Replace - Extended") :
Find what: \r\n Replace with: *RV* \\ asendame reavahetuse märkidega *RV*
Muudame kogu selle andmemassiivi Javascripti stringimuutujaks, võttes kõik jutumärkidesse ja lisades ette var data = ; tulemus peaks välja nägema nii:
var data = "Country Name Indicator Name 1995 ... 37321.8158164172";
Salvestame faili laiendiga .js (nüüd see deklareerib Javascripti muutuja!) ja lisame oma html-dokumendi päisesse selle faili laadimise:
<script src="js/data.js"></script>
Lisame html-dokumendi kehase kanvaa definitsiooni ja selle alla valiku maade valimiseks:
<div class="pilt_paremal" id="altContent">
<div id="container" style='text-align:center;color:#000;'>
<canvas id="canvas" width="400" height="300" style='background-color:#fafaff;'></canvas>
<br>
Vali maad
(+Ctrl mitme valimiseks): <select id = 'country' name="maa" multiple onchange="showInfoC(this.value)">
</select>
<button id="selected" onclick="selected()">Näita !</button>
</div>
Selle koodi kasutamisel copy-paste abil peab taas sümbolite < koodid asendama vastavate ascii-märkidega!
Graafiku joonestamine koosneb kahest etapist: telgede joonistamine ja (kui kasutaja on maad valinud ja klõpsanud 'Näita') valitud maade mullide animatsioon.
Esimest osa sooritav funktsioon draw() käivitatakse taas html-dokumendi keha laadimisel:
<body onload="draw()">
Kuna teisel sammul (animatsioon) käivitatav funksioon vajab mitmeid esimesel sammul funktsioonis draw() loodud muutujaid, deklareerime need enne funktsiooni draw(9 algust - siis on need globaalsed; muidu ei saa neid ka brauserite Javascripti silumise aknas (F12) kätte - nad on funktsiooni draw() sisemised, s.t. lokaalsed muutujad).
Funktsioon draw teisendab andmete stringi kahekordseks massiiviks (rida.veerg):;
var ctx,countries,indicators,countrySelect,indicatorSelect,countryOptions,w,h,n,dataInd,allColors; var xPadding = 40; //vabad ääred X-telje suunas var yPadding = 40; //vabad ääred Y-telje suunas var selectedCountries = []; var countryIndexes = []; var countries = []; var indicators = []; function draw(){ var canvas = document.getElementById("canvas"); if (canvas.getContext) { ctx = canvas.getContext("2d"); } else alert("This browser does not recognize canvas - update your browser!"); countrySelect = document.getElementById("country"); w = canvas.width; h = canvas.height; //data.replace(',',''); // kui arvutis on reaalarvu eraldajaks määratud '.' data = data.split('*RV*'); // data - ridade massiiv console.log(data.length); for(var i = 0; i < data.length; i++) data [i] = data[i].split('\t'); //iga rida - massiiv console.log(data[0].length); //joonistame teljed ctx.strokeStyle = '#333'; //r=g=b - tume (3) hall ctx.beginPath(); ctx.moveTo(xPadding, yPadding); ctx.lineTo(xPadding, h - yPadding); // vahe ääreni ctx.lineTo(canvas.width-xPadding, h - yPadding); ctx.stroke(); //teeb jooned nähtavaks
Moodustame maade ja indikaatorite loetelud ja valikud kasutajale nende valimiseks (lisame funktsioonile draw() ) :
//maad on esimene sõna teisest kuni eelviimase reani for (var i = 1; i < data.length - 1; i++){ if (countries.indexOf(data[i][0])==-1){ countries.push(data[i][0]); var option = document.createElement("option"); option.text = data[i][0]; countrySelect.add(option);} } console.log(countries); //indikaatorid on kõigil maadel samad //need võib võtta esimese maa järelt for (var i = 1; i <= (data.length-1)/countries.length; i++){ indicators.push(data[i][1]); } console.log(indicators);
Nüüd peaks testimisel kanvaa all juba ilmuma valik maa valimiseks. Indikaatorite massiivis indicators on esimene element X-teljele kantav indikaator, teine - Y - teljele kantav ja kolmas - mullina (mulli raadiusena) esitatav. Mulli värvi määrab maa, kuid see selgub alles pärast seda, kui kasutaja on maad valinud.
Leiame esimesest reast aastate veeruindksid - need on veerud, kus väärtus (string) esitab arvu :
dataInd = []; //indeksid/veerud kus esimeses reas on arv, s.t. aasta for (var i = 0; i < data[0].length; i++){ //viimane veerg võib olla puudulik ! if (!(isNaN(data[0][i]))) dataInd.push(i); } console.log(dataInd); n = dataInd.length; //kui palju aastaid on
Kui see skript on olemas, võib andmefaili interpreteerimise tulemusi kontrollida Javascripti silumisaknas (F12) konsoolilt - klõpsata "Console" ja kirjutada parempoolsesse aknasse näiteks data[0][dataInd[0]] - nüüd "Run" klõpsamisel peab Firebug väljastama esimesest reast esimese aastaarvu:
"1995"Telgede joonistamisel andmete ühtlaseks paigutamiseks peab leidma andmete minimaalsed ja maksimaalsed väärtused kõigi kolme indikaatori jaoks. Need leitakse järgneva funktsiooniga (ka see nagu kõik eelnevad koodijupid tuleb veel lisada funktsioonile draw() ); funktsioon getMaxMin () käivitatakse kohe pärast selle definitsiooni, seega eraldi käivitamist pole tarvis; telgedel 'ümmarguste' väärtuste saamiseks ümmardatakse minmaalseid väärtusi alla. maksimaalseid - ülespoole:
// get max min values var minYear = minX = minY = minR = 100000, maxYear = maxX = maxY = maxR = 0; // kindlasti kõigist suurem/väiksem ! (function getMaxMin(){ for(var i = dataInd[0]; i < dataInd[0]+dataInd.length-1; i++){ if (parseFloat(data[0][i]) < minYear) minYear = parseFloat(data[0][i]); if (parseFloat(data[0][i]) > maxYear) maxYear = parseFloat(data[0][i]); for(var j = 1; j < data.length ; j+=indicators.length ) { if (parseFloat(data[j][i]) < minX) minX = parseFloat(data[j][i]); if (parseFloat(data[j][i]) > maxX) maxX = parseFloat(data[j][i]); if (parseFloat(data[1+j][i]) < minY) minY = parseFloat(data[1+j][i]); if (parseFloat(data[1+j][i]) > maxY) maxY = parseFloat(data[1+j][i]); if (parseFloat(data[2+j][i]) < minR) minR = parseFloat(data[2+j][i]); if (parseFloat(data[2+j][i]) > maxR) maxR = parseFloat(data[2+j][i]); } }})(); console.log(minYear,maxYear,minX,maxX,minY,maxY,minR,maxR); // //ümmardame X,Y väärtused : minX = Math.floor(minX); // round down maxX = Math.ceil(maxX); // round up minY = Math.floor(minY); maxY = Math.ceil(maxY); console.log(minX,maxX,minY,maxY);
Pärast nende funktsioonide lisamist võib taas testida: küsida Javascripti silumisaknas:
minX
Vastuseks peaks tulema 67 (see sõltub ka valitud maadest)
Andmete esitamiseks on tarvis abifunktsioone (need on juba iseseisvad, s.t. ei kuulu funktsiooni draw() ) : väärtuste x-y koordinaatide ja mulli raadiuse arvutamiseks; mulli raadius skaleeritakse nii, et vertikaalsuunas (kanvaa kitsam mõõt) mahuks 10 mulli:
function getXPixel(val) { return (((w - 2*xPadding) / (maxX - minX)) * (val - minX) + xPadding); } function getYPixel(val) { return (((h - 2*yPadding) / (maxY - minY)) * (maxY - val) + yPadding); } function getR(val){ return (((h - 2*yPadding)/(10*maxR)) * val ); }
Nüüd võib (funktsiooni draw() viimase toimetamisena) kanda telgedele ka mõõtkava:
//kanname X,Y-telgedele mõõtkava: ctx.font ="11px _sans"; ctx.textBaseline = "top"; for(var i = minX; i <= maxX; i++) { ctx.fillText(i, getXPixel(i), h - yPadding ); } //lisame indikaatori ctx.textAlign = "center"; ctx.fillText(indicators[0], (xPadding+canvas.width)/2, h - yPadding/2 ); //ctx.fillText(data[0][dataInd[dataInd.length-1]], getXPixel(data[0][dataInd[dataInd.length-1]]), h - yPadding/2 ); // kanname y-teljele väärtused ctx.textAlign = "right"; ctx.textBaseline = "middle"; for(var i = minY; i <= maxY; i ++) ctx.fillText(i, xPadding-2, getYPixel(i) ); ctx.textAlign = "center" //vertikaalse teksti saamiseks tuleb kanvaad pöörata! ctx.save(); ctx.rotate(-Math.PI/2); ctx.fillText(indicators[1],-canvas.height/2,yPadding/2); ctx.rotate(Math.PI/2); ctx.restore; ctx.textAlign = "left" ctx.fillText(indicators[2],1.5*xPadding,40);
Kui kasutaja on maad valinud (vähemalt ühe) ja klõpsanud "Näita", käivitub programmi teine osa: funktsioon select(); see salvestab kasutaja poolt valitud maade nimed massiivis selectedCountries ja nende asukoha (indeksi) kõigi maade loetelus countryIndexes (muidu poleks hiljem võimalik leida selle maa indikaatoreid):
//get user selections var selected = function(){ countryOptions = countrySelect.selectedOptions; if (countryOptions.length < 1) { alert("You should select some countries!"); return; } for(var i=0; i < countryOptions.length; i++) { selectedCountries.push(countryOptions[i].value); countryIndexes.push(countries.indexOf(countryOptions[i].value)); } console.log(selectedCountries,countryIndexes); allColors = ["#FF0000","#00FF00","#0000FF","#990033","#339900","#FFFF00","#00FFFF","#FF00FF","#003399"]; //kirjutame värvide tähenduse ctx.textAlign = "left"; ctx.textBaseline = "top"; for (var i=0; i < selectedCountries.length; i++){ ctx.fillStyle = allColors[i]; ctx.fillRect (1.5*xPadding+i*65, 60, 10, 10); ctx.fillText(selectedCountries[i],1.5*xPadding+i*65+20,60); ctx.stroke(); } play(); }Animatsiooni esitab funktsioon play(), mis igal sammul käivitab antud aasta andmete joonistamiseks funktsiooni drawYear():
function drawYear(){ ctx.clearRect(1.5*xPadding,80,80,34); //kustutame eelmise aasta ctx.fillStyle = "rgba(160, 160, 220,0.8)"; ctx.font = '32px _sans'; var year = data[0][dataInd[time]]; ctx.fillText(year, 1.5*xPadding,80); //vähendame igal sammul mullide läbipaistvust var min_alph = 0.2; var delta = (1-min_alph)/data.length; var alph = min_alph + time * delta; for (var c=0; c < selectedCountries.length; c++){ var dx = data[1+countryIndexes[c]*indicators.length][dataInd[time]]; var dy = data[2+countryIndexes[c]*indicators.length][dataInd[time]]; var dr = data[3+countryIndexes[c]*indicators.length][dataInd[time]]; console.log(countryIndexes[c]*indicators.length,dx,dy,dr); ctx.save(); ctx.fillStyle = allColors[c]; ctx.globalAlpha = alph; ctx.beginPath(); ctx.arc(getXPixel(dx),getYPixel(dy),getR(dr),0,Math.PI*2); console.log(getXPixel(dx),getYPixel(dy),getR(dr)); ctx.fill(); ctx.restore(); }; } function play(){ time = 0; var animdraw = function() { drawYear(); time++; if(time < dataInd.length) { setTimeout(animdraw,500); } } animdraw(); }
Ja ongi kõik...
Ülesandeid.
1. Muuda animatsiooni näitamise tempot - praegu on see liiga kiire.
2. Programmis on viga - telgede skaleerimine ei
arvesta mulli suurust ja kui mull on suur ja telje lähedal, katab see
telje ja kanvaad ümbritseva ala (pildil).
2. Kasutajale võiks anda võimaluse ka indikaatorite valikuks (analoogiliselt maade valikuga).
© Jaak Henno