HTML5 - Bubble Chart


Vali maad (+Ctrl mitme valimiseks):
Selles loengus esitatakse:

Ü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

Küsimused, probleemid : e-mail