momente şi schiţe de informatică şi matematică
anti point—and—click

O aplicaţie cu tabele dinamice (situaţia şcolară)

DOM | sort
2008 apr

Presupunem creată o pagină Web statică, având drept conţinut următoarele trei elemente: un paragraf explicativ, un element <textarea> şi un <button> (se poate vedea Aplicaţii şcolare - "Situaţia şcolară"):

Vrem să realizăm funcţii javaScript care (la click pe "Medii generale") să creeze şi să insereze în document un element <table>, conţinând numele şi media generală pentru fiecare linie de date din <textarea>. În acest scop vom folosi câteva metode din DOM: .createElement(), .appendChild(), şi altele.
Fireşte, vor apărea şi alte cerinţe: crearea posibilităţii de ordonare, asocierea unei diagrame statistice.

Schema procedurală

Adoptăm stilul procedural, prevăzând următoarele variabile şi funcţii:

var CATALOG = []; // tablou global pentru Elevi-medii 
var SORT_ANT;     // care coloană a fost indicată pentru sortare 

function load_catalog(textarea_id) { 
  // încarcă în CATALOG, liniile de date din <textarea>
}

function med_gen(dest) { // apelată de click pe butonul 'Medii Generale'
  /*
    tabel = createElement('table') // creează apoi, 'thead', 'tfoot', 'tbody'
    pentru fiecare linie din CATALOG:
          creează TR şi TD pentru Nume-Prenume,
          calculează media-generală a mediilor de pe linia respectivă
          appendChild() - adaugă TD la TR, apoi TR la TBODY
     tabel.appendChild(TBODY);
     dest.appendChild(tabel); // înscrie tabelul în DOM, la ID = dest
  */
}

function sort_table(scol) { // rangul coloanei după care sortează (Nume/Medie)
  // apelată de click pe antetul coloanei (<TH><A onclick=...>)
  // rows[] = Array local al liniilor din TBODY (pentru tabelul mediilor generale)
  // ordonează rows[] după valorile din coloana indicată 
  // creează un nou TBODY având ca TR-uri rândurile (ordonate) din rows[]
  // înlocuieşte vechiul TBODY cu noul TBODY // table.replaceChild(tbody1, tbody0)
}

Pentru realizarea diagramei statistice privitoare la medii - vom vedea cum putem invoca serviciul-web oferit de Google, anume Google Chart.

Sensul considerării unei variabile globale SORT_ANT se vede imaginând următoarea desfăşurare: se constituie tabloul CATALOG, apelând load_catalog(); apoi, se declanşează butonul "Medii generale", rezultând elementul <table> al Numelor şi mediilor generale; în acest tabel, elevii apar în ordinea în care existau în CATALOG - deci, nu neapărat ordonaţi în vreun fel.

Făcând acum click pe antetul "Nume Prenume", se lansează sort_table(0) care realizează schimbările necesare în tabel încât elevii să fie redaţi în ordine alfabetică. Făcând imediat, un al doilea click tot pe "Nume Prenume", se apelează iarăşi sort_table(0), realizând de această dată ordonarea inversă celei obţinute la primul click - ori de data aceasta (la al doilea click) putem scuti prelucrările de ordonare specifice, dat fiind că putem invoca direct funcţia nativă reverse().

Memorând "primul click" (de fapt, rangul coloanei respective) în SORT_ANT, vom putea şti dacă este suficient să se invoce reverse(), sau trebuie efectiv folosită procedura de ordonare.

load_catalog()

function load_catalog(textarea_id) {
   var catalog = document.getElementById(textarea_id);
   var text = catalog.value.replace(/^\s+/mg,"").replace(/\s+$/mg,"");
   if(text) CATALOG = text.split(/[\n\r]+/g);
}

Primind atributul ID al elementului <textarea> care conţine numele şi mediile, se accesează nodul respectiv (folosind .getElementById()) şi se preia (în variabila "text") şirul de caractere existent, eliminând însă eventualele spaţii iniţiale sau finale. Apoi, split(/[\n\r]+/g) separă şirul "text" în subşiruri care corespund rândurilor din <textarea> - iar aceste subşiruri devin componentele tabloului global CATALOG (de exemplu, CATALOG[0] = "Aaaa Baaaaa 5.25 5.67 5.33")

Nu am implicat deocamdată nicio verificare (trebuie acceptate numai şiruri care să reprezinte Nume Prenume şi respectiv "medii" de forma: dd.dd, unde d este o cifră zecimală). Dar am prevăzut explicit această funcţie (în loc de a îngloba liniile de cod de mai sus în med_gen()) tocmai în scopul de a o putea extinde ulterior (incluzând diverse verificări), fără a "deranja" codul altor funcţii.

Desigur, load_catalog() va trebui apelată în prealabil, fie direct din med_gen(), fie din codul global care determină desfăşurarea aplicaţiei.

med_gen() - constituirea în DOM a tabelului

Funcţia med_gen(dest) construieşte în DOM, ca "child" al nodului "dest", elementul <table> corespunzător tabelului mediilor generale (Nume, Media generală).

function med_gen(dest) {
   if(CATALOG.length == 0) { alert("înscrieţi întâi Catalogul!"); return false; }
   var dest = document.getElementById(dest); 
   dest.innerHTML = ''; // iniţializare care permite "reîncărcarea" datelor

   var tabel = document.createElement('table'); 
   tabel.setAttribute('id', 'situatie');  // pentru accesare din sort_table()

   var TR = document.createElement('tr'); // nodurile TR, TH, TD vor fi apoi "clonate",
   var TH = document.createElement('th'); // evitând recrearea lor
   var TD = document.createElement('td');
   var row, cel; 

   var thead = document.createElement('thead'); // secţiunea <thead> a tabelului 
   var antet = ['Nume Prenume', 'media'];
   row = TR.cloneNode(true);
   for(var i = 0; i < 2; i++) { 
      cel = TH.cloneNode(true); 
      cel.innerHTML = "<a onclick='sort_table(" + i + ");' href='javascript:;'>"
                       + antet[i] + "</a>"; 
      row.appendChild(cel); // click va apela sort_table(scol), cu scol=0 sau scol=1
   }
   thead.appendChild(row); 
   tabel.appendChild(thead); // amânăm crearea <tfoot>, pentru a calcula şi media clasei

   var tbody = document.createElement('tbody'); // secţiunea <tbody>
   var mgcl = 0; // media generală a clasei (va fi înscrisă în <tfoot>)
   var n = CATALOG.length;
   for(var i = 0; i < n; i++) {
       var el = CATALOG[i]; 
       var nume = el.replace(/^(\D+).+/,"$1");
       var medii = el.replace(/^\D+(.+)$/,"$1").split(/\s+/g);   // alert(medii);
       var mg = 0; var no = medii.length;
       for(var m = 0; m < no; m++)
          mg += parseFloat(medii[m]);
       mg /= no; mgcl += mg;
       row = TR.cloneNode(true); 
       cel = TD.cloneNode(true); cel.innerHTML = nume; row.appendChild(cel);
       cel = TD.cloneNode(true); 
       cel.innerHTML = mg.toPrecision(4).substring(0,4); // cu 2 zecimale exacte
       row.appendChild(cel);
       if(i & 1) row.setAttribute('class', 'altern'); // alternez background rânduri
       tbody.appendChild(row);
   }

   mgcl /= n; // media clasei va fi adăugată la subsol
   var tfoot = document.createElement('tfoot'); // secţiunea <tfoot>
   row = TR.cloneNode(true); 
   cel = TD.cloneNode(true); cel.setAttribute('colspan', '2');
   cel.innerHTML = "media: " + mgcl.toPrecision(5).substring(0,5);
   row.appendChild(cel); tfoot.appendChild(row); 
   tabel.appendChild(tfoot);

   tabel.appendChild(tbody);
   dest.appendChild(tabel); // înscrie tabelul în document (în DOM)

   SORT_ANT = -1; // încă nu s-a apelat la sortare după vreo coloană
}

<thead> şi <tfoot> trebuie inserate înaintea lui <tbody> (tabel.appendChild(tfoot) înainte de tabel.appendChild(tbody)), dar această regulă nu ne împiedică să creem întâi <tbody> şi apoi <tfoot> (ceea ce a fost necesar aici, pentru a obţine întâi media generală a clasei şi a o înscrie apoi în <tfoot>).

Cele două elemente <th> din <thead> conţin câte un link precum <a onclick = "sort_table(0);" href = "javascript:;">Nume Prenume</a>. Atributul href trebuie să specifice resursa vizată (dacă este un URL - de exemplu href = "http://www.google.com" - atunci la click pe link-ul respectiv va fi încărcată pagina indicată de URL); aici, nu este vizat protocolul HTTP, ci "protocolul javascript:", deci click pe link-ul respectiv este "inoperant" şi de fapt clik-ul va lansa funcţia indicată de atributul onclick (ordonarea tabelului după coloana de rang 0).

Elementele tabloului CATALOG sunt şiruri precum "Ionescu Ion 7.67 8.67 9 10 10 10 10 10 8.67 7.67 8.67 9 10 10 10 10 10 8.67" (Nume Prenume şi apoi mediile la obiecte); la crearea elementului <tbody> se preia câte un asemenea şir prin var el = CATAL[i];, se extrage numele, apoi se extrage şirul mediilor şi se constituie (cu split) tabloul mediilor la obiecte.

Numele este extras prin var nume = el.replace(/^(\D+).+/,"$1");; aici, /^(\D+).+/ este o expresie regulată, adică un şablon construit după anumite specificaţii standard, care permite selectarea unei anumite secvenţe de caractere dintr-un şir; ^ şi $ specifică "de la primul" şi respectiv, "până la ultimul" caracter din şir; \d şi \D specifică un caracter care este şi respectiv, nu este o cifră zecimală, iar . "ţine loc" de oricare caracter; \D+ specifică o secvenţă de unul sau mai multe caractere succesive care sunt non-cifre (iar .+ corespunde unei secvenţe de unul sau mai multe caractere oarecare). Parantezele asigură memorarea grupului de caractere încadrat, el putând apoi fi extras prin $n unde n este rangul grupului (1 pentru primul grup parantezat).

Astfel, var nume = "Ionescu Ion 7.67 8.67" . replace(/^(\D+).+/,"$1"); va memora în "$1" secvenţa de non-cifre "Ionescu Ion " de la începutul şirului (ignorând cifrele care urmează) şi va transfera rezultatul în variabila "nume".

În final, se iniţializează SORT_ANT cu un rang de coloană inexistent - încât la prima apelare a funcţiei sort_table() să se facă efectiv ordonarea tabelului (şi nu să se invoce reverse()).

sort_table() - reconstrucţia în DOM a tabelului ordonat

Funcţia med_gen() a creat tabelul cu două coloane "Nume Prenume" şi "medii" şi i-a setat atributul ID cu valoarea "situatie" (prin tabel.setAttribute('id', 'situatie');) - încât tabelul poate fi accesat apoi, din alte funcţii (inclusiv, din sort_table()).

Un <table> poate conţine mai multe secţiuni <tbody>, iar .getElementsByTagName('tbody') furnizează un tablou conţinând referinţele corespunzătoare fiecăreia; reţinem în tbody0 referinţa la unicul <tbody> din tabloul nostru şi încărcăm referinţele la rândurile <tr> din acest <tbody>, în tabloul rows.

Constituim apoi tabloul arr_col, în care fiecare componentă este o pereche formată din indicele rândului curent din rows şi valoarea din coloana de rang scol de pe acel rând. Putem formula o asemenea pereche prin { rând_curent: 2, valoare_col: "Popescu Giorgică" } unde acoladele încadrează elementele perechii, virgula le separă, iar : permite să disociem între identificatorii de elemente ale perechii şi valorile propriu-zise.

La prima vedere, identificatorii elementelor din pereche nu sunt necesari (perechea propriu-zisă ar fi [2, "Popescu Giorgică"] - numai că… ar fi vorba atunci de un tablou de valori!); de fapt, identificatorii chiar devin esenţiali în operaţiile fireşti de atribuire şi de selectare: var P = { rând: 2, val_col: "Ion" }; alert(P.val_col); P.val_col = "Geo"; alert(P). Variabila P de aici este ceea ce se cheamă în diverse limbaje, o variabilă de tip hash.

arr_col este deci un tablou de hash-uri, în care: arr_col[i].oldr = i este rangul rândului curent din tabloul rows, iar arr_col[i].valc = rows[i].getElementsByTagName('td')[scol].firstChild.nodeValue este valoarea de pe rândul respectiv din coloana de rang scol.

Ordonăm tabloul arr_col folosind funcţia nativă sort() şi o funcţie de comparare corespunzătoare (sau, în funcţie de valoarea din variabila globală SORT_ANT - folosind arr_col.reverse()). Apoi, creem un nou <tbody>, în care înscriem rândurile existente în rows, dar în ordinea rezultată în arr_col, deci rows[arr_col[i].oldr]; după înlocuirea vechiului cu noul <tbody>, tabelul corespunde ordinii dorite.

function sort_table(scol) { // rangul coloanei după care sortează (aici, 0 sau 1)
   var table = document.getElementById('situatie');
   var tbody0 = table.getElementsByTagName('tbody')[0]; // vechiul TBODY
   var rows = tbody0.getElementsByTagName('tr'); // rândurile din vechiul TBODY

   var arr_col = [];  // valorile din coloana de sortat ("array of hashes")
   for (var i = 0, len = rows.length; i < len; i++) {
      arr_col[i] = {}; // "hash" { rând curent (old) => valoarea din coloană }
      arr_col[i].oldr = i;
      arr_col[i].valc = rows[i].getElementsByTagName('td')[scol].firstChild.nodeValue;
   }

   if (scol == SORT_ANT) { // dacă s-a sortat anterior după coloana 'scol' (ASC), 
      arr_col.reverse();  // atunci e suficientă inversarea rândurilor (DESC)
   }
   else {
      SORT_ANT = scol;  // memorează rangul coloanei de sortat (pentru "reverse") 
      if (scol == 0) arr_col.sort(hash_cmp_lex); // ordonează lexicografic numele, 
      else arr_col.sort(hash_cmp_num);  // ordonează numeric mediile 
   }

   var tbody1 = document.createElement('tbody');  // constituie noul TBODY al tabelului
   for (var i=0, len = arr_col.length; i < len; i++) {
      var myrw = rows[arr_col[i].oldr]; // rândul de pe vechea poziţie
      var cls = i&1 ? 'altern' : ''; // setează/resetează 'altern' pe rândul respectiv
      myrw.setAttribute('class', cls);
      tbody1.appendChild(myrw.cloneNode(true)); // adaugă rândul pe noua poziţie 
   }

   table.replaceChild(tbody1, tbody0); // înlocuieşte în tabel, vechiul cu noul TBODY
}

function hash_cmp_lex(a, b) { // ordonează lexicografic
   var aVal = a.valc, bVal = b.valc;  
   return (aVal == bVal ? 0 : (aVal > bVal ? 1 : -1));
}

function hash_cmp_num(a, b) { // ordonează numeric
   var aVal = parseFloat(a.valc), bVal = parseFloat(b.valc);
   return (aVal - bVal);
}

Privind funcţiile native sort() şi reverse(), iată vreo două exemple semnificative:

["Foo", "Bar", "bar", "Baz"].sort()  lexicografic: ["Bar", "Baz", "Foo", "bar"] 
["Foo", "Bar", "bar", "Baz"].reverse()  "inversează": ["Baz", "bar", "Bar", "Foo"] 
[30, 7, 300, 31].sort()  ordonează lexicografic [30, 300, 31, 7] 
[30, 7, 300, 31].sort( function(a,b) {return a - b;} )  numeric [7, 30, 31, 300]

Folosirea "pseudoprotocolului" javascript: permite verificarea imediată a unor astfel de exemplificări: tastaţi în bara de adresă a browserului javascript:alert([30, 7, 300, 31].sort(function(a,b){return a - b;})); - răspunsul va fi o fereastră de alertare, conţinând tabloul sortat.

Putem folosi pentru mici teste şi "Error Console" (a vedea meniul Firefox "Tools"). În imaginea alăturată este reprodus testul sortării obişnuite pentru cazul când numele ar începe cu litere "nestandard" şi ne dăm seama că funcţia noastră hash_cmp_lex() nu va asigura totdeauna, ordonarea aşteptată (va aşeza "Z..." înainte de "Ş...", de exemplu).

Pentru sortarea corectă a şirurilor care conţin şi alte caractere decât cele standard, trebuie folosită metoda localeCompare(), proprie obiectului javaScript String() - cum atestează următoarea imagine:

În baza acestui test, dacă dorim putem rescrie funcţia respectivă astfel:

function hash_cmp_lex(a, b) { 
   var aVal = a.valc, bVal = b.valc;  
   // return (aVal == bVal ? 0 : (aVal > bVal ? 1 : -1)); // Z ar fi după Ş
   return aVal.localeCompare(bVal);
}

Diagrama statistică a mediilor

Pentru reprezentarea grafică a unor statistici asupra mediilor vom apela un serviciu-web oferit de Google, anume Google Chart.

Google Chart API răspunde la un URL corespunzător, returnând o imagine în format PNG. În URL trebuie specificate: dimensiunea imaginii, tipul diagramei şi datele necesare (alte atribute sunt opţionale: culori, etichete, titlu, etc.).

Identificatorii prevăzuţi pentru atribute încep de obicei cu ch (de la "chart"); astfel, chs=300x100 identifică "size" (dimensiune), asignând 300 pixels pentru width (lăţime) şi 100 pentru height (înălţime); chd=t:20,30,40,10 identifică datele de construcţie a diagramei, specificând ca tip de codificare a acestora text: valori numerice pozitive între 0.0 şi 100.0 ("floating point"), separate prin virgulă; cht=p specifică tipul diagramei (p pentru "pie", adică diagramă circulară obişnuită; p3 pentru diagramă circulară "3D"; v pentru diagramă Venn; etc.).

URL-ul poate fi transmis şi din bara de adresă a browserului (selectaţi de aici, Copy, Paste în bara de adresă):
http://chart.apis.google.com/chart?chs=400x150&chd=t:20,30,40,10&cht=p3
unde ?chs=... indică parametrii cererii, separaţi prin & (formula obişnuită pentru QUERY STRING).

Dar mai obişnuit este ca diagrama returnată să fie inclusă într-un document HTML existent; în acest scop, URL-ul necesar trebuie specificat în atributul src al unui element <img>.

Pentru un exemplu concret, fie A mulţimea elevilor care au 10, fie B mulţimea elevilor care au 10 la Matematică şi C - a acelora care au 10 la Informatică; să zicem că ştim |A| = 100, |B| = 20, |C| = 40 şi |A∩B| = 10, |A∩C| = 20, |B∩C| = 15, iar |A∩B∩C| = 5.
Putem obţine diagrama Venn corespunzătoare formulând în bara de adresă a browserului http://chart.apis.google.com/chart?cht=v&chd=t:100,20,40,10,20,15,5&chs=200x100&chdl=A|B|C, sau folosind într-o pagină HTML un element <img> în care am fixat atributul src pe "adresa" tocmai menţionată (parametrul chdl specifică "legenda"):

diagramă Venn

Să presupunem că s-a apelat funcţia med_gen(), obţinând tabelul cu ID = "situatie", conţinând elevii şi mediile generale corespunzătoare. Următoarea funcţie (apelată fie din med_gen() în final, fie prin intermediul unui <button> corespunzător) obţine o reprezentare grafică a situaţiei mediilor pe grupe de valori.

var to_chart = function(dest) {
   var dest = document.getElementById(dest); // dest.innerHTML = '';
   var size = '400x150';
   var gourl = 'http://chart.apis.google.com/chart?cht=p3&chs=' + size + '&chd=t:';
   var labels = ['5-6', '6-7', '7-8', '8-9', '9-10']; 
   var data = [0, 0, 0, 0, 0]; // nr. medii 5-6, 6-7, 7-8, 8-9, repectiv 9-10

   var t = document.getElementById('situatie');
   var tds = t.getElementsByTagName('tbody')[0].getElementsByTagName('td');
   var i;
   for(i = 0; tds[i]; i += 2) {
      media = tds[i+1].innerHTML; // ca şir de caractere
      switch(media.charAt(0)) { // contorizează nr. medii, funcţie de "prima cifră"
         case '5': data[0]++; break;  // nr. medii 5-6 (încep cu cifra '5')
         case '6': data[1]++; break;
         case '7': data[2]++; break;
         case '8': data[3]++; break;
         case '9': data[4]++; break;
         case '1': data[4]++; break;  // dacă începe cu '1'... n-o fi media 1, ci 10
      }
   }

   for(i = 0; i < 5; i++) labels[i] += ' (' + data[i] + ')'; 

   var chart = document.createElement('img');
   chart.setAttribute('src', gourl + data.join(',') + '&chl=' + labels.join('|'));
   chart.setAttribute('alt', 'diagrama');
   
   dest.appendChild(chart);
}

În variabila tds se obţine tabloul tuturor elementelor <td> din <tbody>, în ordinea în care apar acestea în tabel; fiecare al doilea <td> conţine o medie, iar switch(media.charAt(0)) "repartizează" media în funcţia de prima cifră a ei, într-una dintre grupele 5-6, 6-7, etc. În final, se creează elementul <img>, se setează corespunzător atributele necesare şi se înscrie în DOM (drept "child" al elementului "dest" primit ca parametru iniţial).

docerpro | Prev | Next