Distracție cu panza creați un plugin pentru grafice de bare, partea 2

În această serie din două părți, vom combina elementul versatil de canvas cu biblioteca robustă jQuery pentru a crea un plugin pentru graficele de bare. În această a doua parte, o vom converti într-un plugin jQuery și apoi vom adăuga niște bomboane ochi și caracteristici suplimentare.

Încheierea Distracție cu panza în două serii, astăzi vom crea un plugin pentru graficul bar; nu este un plug obișnuit, vă minte. Vom prezenta o dragoste jQuery la elementul de panza pentru a crea un plugin foarte robust.

În prima parte, ne-am uitat numai la implementarea logicii plug-in-ului ca scenariu independent. La sfârșitul primei părți, graficul nostru de bare a arătat așa.


Rezultat la sfârșitul părții 1

În această ultimă parte, vom lucra la convertirea codului nostru și la transformarea acestuia într-un plugin jQuery adecvat, adăugând câteva noduri vizuale și, în final, includerea unor caracteristici suplimentare. În cele din urmă, producția noastră va arăta astfel:


Produs finit

Toate încălzite? Permiteți-i să se scufunde în!


Plugin Formalities

Înainte de a începe să convertim codul într-un plugin, trebuie mai întâi să analizăm câteva formalități atunci când vine vorba de scrierea pluginurilor.


Denumirea pluginului

Începem prin alegerea unui nume pentru plugin. am ales grafic de bare și a redenumit fișierul JavaScript la jquery.barGraph.js. Acum închidem tot codul din articolul anterior din fragmentul următor.

 $ .fn.barGraph = funcție (setări) // cod aici

Setări conține toți parametrii opționali transferați pluginului.


Lucrați în jurul emisiunii de simboluri $

În jQuery plugin-ul de autor, este, în general, luând în considerare o bună practică de a utiliza jQuery în loc de aliasul $ din codul dvs., pentru a minimiza conflictele cu alte biblioteci Javascript. În loc să trecem prin toate aceste probleme, putem folosi doar pseudonime personalizate așa cum se menționează în documentele jQuery. Vom închide întregul cod de plugin în cadrul acestei funcții anonime, așa cum se arată mai jos:

 (funcția ($) $ .fn.barGraph = funcția (setările) // codul de implementare a pluginului aici) (jQuery);

În esență, încapsulăm tot codul nostru într-o funcție și îi transmitem jQuery. Suntem liberi să folosim aliasul $ în măsura în care ne dorim în interiorul codului nostru acum, fără a fi nevoiți să vă faceți griji în legătură cu acest lucru potențial în conflict cu alte biblioteci JavaScript.


Valorile implicite

Când proiectați un plugin, este bine să expuneți un număr rezonabil de setări la utilizator, în timp ce utilizați opțiuni implicite sensibile dacă utilizatorii utilizează plugin-ul fără a trece nici o opțiune. Având în vedere acest lucru, vom permite utilizatorului să schimbe fiecare dintre variabilele de variație ale graficelor pe care le-am menționat în acest articol anterior din această serie. A face acest lucru este ușor; noi definim fiecare dintre aceste variabile ca proprietăți ale unui obiect și apoi le accesăm.

 var defaults = barSpacing = 20, barWidth = 20, cvHeight = 220, numYlabels = 8, xOffset = 20, maxVal, gWidth = 550, gHeight = 200; ;

În cele din urmă, trebuie să fuzionăm opțiunile implicite cu opțiunile trecute, oferindu-le preferință celor care au trecut. Această linie are grijă de asta.

 var opțiune = $ .extend (setări implicite, setări);

Nu uitați să schimbați numele variabilelor ori de câte ori este necesar. Ca în -

 retur (param * barWidth) + (param + 1) * barSpacing) + xOffset;

… schimbări la:

 retur (param * option.barWidth) + (param + 1) * optiune.barSpacing) + option.xOffset;

refactorizare

Acesta este locul unde plugin-ul este ciocanit. Vechea noastră implementare ar putea produce doar un singur grafic într-o pagină și abilitatea de a crea mai multe grafice într-o pagină este principalul motiv pentru care creăm un plugin pentru această funcție. În plus, trebuie să ne asigurăm că utilizatorul nu are nevoie să creeze un element de panza pentru fiecare grafic care urmează să fie creat. Având în vedere acest lucru, vom crea elementele de pânză în mod dinamic, după cum este necesar. Să mergem. Vom analiza versiunile anterioare și actualizate ale porțiunilor relevante ale codului.


Invocând pluginul

Înainte de a începe, aș dori să subliniez modul în care plugin-ul nostru va fi invocat.

 $ ("# ani"). barGraph (barSpacing = 30, barWidth = 25, numYlabels = 12,);

Simplu ca asta. ani este ID-ul tabelului care deține toate valorile noastre. Trecem în opțiuni după cum este necesar.


Obținerea sursei de date

Pentru a începe lucrurile, avem nevoie mai întâi de o referire la sursa de date pentru grafice. Acum accesăm elementul sursă și obținem codul său de identificare. Adăugați următoarea linie la mulțimea de variabile de grafic pe care am declarat-o mai devreme.

 var dataSource = $ (acest) .attr ("id");

Definim o variabilă nouă și îi atribuim valoarea atributului ID al elementului trecut. În codul nostru, acest se referă la elementul DOM selectat curent. În exemplul nostru, se referă la tabelul cu un ID de ani.

În implementarea anterioară, ID-ul pentru sursa de date a fost greu codat. Acum îl înlocuim cu atributul ID extras mai devreme. Versiunea anterioară a grabValues funcția este mai jos:

 funcția grabValues ​​() // Accesați celula de tabelă cerută, extrageți și adăugați valoarea acesteia la matricea valorilor. $ ("# data tr td: nth-child (2)") fiecare (functie () gValues.push ($ (this) .text ()); // Accesați celula de tabelă cerută, extrageți și adăugați valoarea acesteia la matricea xLabels. $ ("# date tr td: nth-child (1)") fiecare (functie () xLabels.push ($ (this) .text ()); 

Se actualizează la aceasta:

 funcția grabValues ​​() // Accesați celula de tabelă cerută, extrageți și adăugați valoarea acesteia la matricea valorilor. $ ("#" + dataSource + "tr td: nth-child (2)") fiecare (function () gValues.push ($ (this). // Accesați celula de tabelă cerută, extrageți și adăugați valoarea acesteia la matricea xLabels. $ ("#" + dataSource + "tr td: nth-copil (1)") fiecare (funcția () xLabels.push ($ (this). 

Injectarea elementului de canvas

 funcția initCanvas () $ ("#" + dataSource) .after (" "); // Încercați să accesați elementul canvas cv = $ (" # bargraph - "+ dataSource) .get (0); if (! Cv.getContext) return; panza și aruncă o eroare dacă nu poate să ctx = cv.getContext ('2d'); dacă (! ctx) return;

Creăm un element de canvas și îl injectăm în DOM după tabel, care acționează ca sursă de date. lui jQuery după funcția vine într-adevăr la îndemână aici. Un atribut de clasă din grafic de bare și un atribut ID în format bargraf-dataSourceID este, de asemenea, aplicată pentru a permite utilizatorului să le eticheteze pe toate ca un grup sau individual, după cum este necesar.


Ciclism prin elementele trecute

Există două moduri în care puteți invoca pluginul de fapt. Puteți crea fiecărui grafic separat trecând doar într-o singură sursă de date sau puteți trece într-o serie de surse. În acest din urmă caz, construcția actuală va întâmpina o eroare și va renunța. Pentru a remedia acest lucru, folosim fiecare construi pentru a itera peste setul de elemente trecute.

 (funcții ($) $ .fn.barGraph = funcție (setări) // Variabile de variante var defaults = // opțiuni aici; // Merge parametrii parveniți cu valorile implicite var opțiune = $ .extend (implicite, ); // Ciclul prin fiecare obiect passed this.each (function () // Codul de implementare aici); // Returneaza obiectul jQuery pentru a permite inlocuibilitatea return it;) (jQuery);

Am încapsula tot codul după obținerea și îmbinarea setărilor din interiorul lui this.each construi. De asemenea, asigurați-vă că returnați obiectul jQuery la sfârșitul secțiunii pentru a activa grefa.

Cu aceasta, refactorizarea noastră este completă. Ar trebui să fim capabili să invocăm plugin-ul nostru și să creăm câte grafice este necesar.


Adăugarea de Candy Eye

Acum că convertirea noastră este completă, putem lucra la îmbunătățirea vizuală. Vom face o serie de lucruri aici. Ne vom uita la fiecare dintre ei separat.


tematică

Versiunea mai veche a folosit un gri greu pentru a desena graficele. Vom implementa un mecanism tematic pentru barele de acum. Aceasta, prin ea însăși, constă într-o serie de pași.


Ocean: Tema prestabilită
Frunziş
floare de cires
Spectru

Adăugându-l la Opțiuni

 var defaults = // Alte valori implicite aici tema: "Ocean",;

Adăugăm a temă opțiunea la valorile implicite, permițând utilizatorului să schimbe tema la oricare dintre cele patru presetări disponibile.

Setarea temei selectate în prezent

 funcția grabValues ​​() // Comutarea precedentă a codului (opțiunea.) caz "Ocean": gThe = thBlue; pauză; cazul "Foliaj": gTheme = thGreen; pauză; cazul "Cherry Blossom": gTheme = thPink; pauză; cazul Spectrum: gTheme = thAssorted; pauză; 

Un simplu intrerupator construi se uită la option.theme setarea și punctele gTheme variabilă la matricea de culori necesară. Folosim nume descriptive pentru teme în locul celor generice.

Definirea matricei de culori

 // Teme var thPink = ['# FFCCCC', '# FFCCCC', '# FFC0C0', '# FFB5B5', 'FFADAD', 'FFA4A4', FF9A9A, FF8989, FF6D6D „]; var thBlue = ['# ACE0FF', '# 9CDAFF', '# 90D6FF', '# 86D2FF', '# 7FCFFF', '# 79CDFF', '# 72CAFF', '# 6CC8FF', '# 57C0FF']; var thGreen = ['# D1FFA6', '# C6FF91', '# C0FF86', '# BCFF7D', '# B6FF72', '# B2FF6B', '# AAFE5D', '# A5FF51', '# 9FFF46']; var thAssorted = ['# FF93C2', '# FF93F6', '# E193FF', '# B893FF', '# 93A0FF', '# 93D7FF', '# 93F6FF', '# ABFF93', '# FF9B93'];

Apoi definim un număr de matrice, fiecare având o serie de nuanțe de culori specifice. Ele încep cu nuanța mai deschisă și continuă să crească. Vom trece prin aceste tablouri mai târziu. Adăugarea de teme este la fel de simplă ca adăugarea unei matrice pentru culoarea specifică de care aveți nevoie și apoi modificarea celei anterioare intrerupator pentru a reflecta schimbările.

Funcția Helper

 funcția getColour (param) retur Math.ceil (Math.abs (((gValues.length / 2) -param))); 

Aceasta este o funcție minuscule care ne permite să realizăm și să aplicăm un gradient asemănător grafurilor. În esență, calculăm diferența absolută dintre jumătate din numărul de valori care trebuie redate și parametrul trecut, care este indicele elementului selectat curent în matrice. În acest fel, putem crea un gradient neted. Deoarece am definit doar nouă culori în fiecare dintre matricea de culori, suntem limitați la optsprezece valori într-un grafic. Extinderea acestui număr ar trebui să fie destul de banal.

Setarea stil de completare

 funcția drawGraph () pentru (index = 0; index 

Aici plasăm graficele. În loc să setați o valoare statică la stil de completare proprietate, folosim getColour pentru a extrage indicele necesar al elementului în matricea temei selectată în mod curent.


Opacitate

În continuare, vom oferi utilizatorului posibilitatea de a controla opacitatea barelor trase. Setările sunt un proces în două etape.


Fără transparență
Cu o valoare de 0,8

Adăugându-l la Opțiuni

 var defaults = // Alte valori implicite aici barOpacity: 0.8,;

Adăugăm a barOpacity opțiunea la valorile implicite, permițând utilizatorului să modifice opacitatea graficelor la o valoare de la 0 la 1, unde 0 este complet transparent și 1 este complet opac.

Setarea globalAlpha

 funcția drawGraph () pentru (index = 0; index 

globalAlpha proprietatea controlează opacitatea sau transparența elementului randat. Am setat valoarea acestei proprietăți la valoarea trecută sau la valoarea implicită pentru a adăuga un pic de transparență. Ca implicit sensibil, folosim o valoare de 0,8 pentru ca aceasta să fie doar un mic fragment transparent.


Grilă

O grilă poate fi extrem de utilă în prelucrarea datelor prezentate într-un grafic. Deși am vrut inițial o rețea adecvată, am stabilit mai târziu pentru o serie de linii orizontale care se aliniau cu etichetele axei Y și au aruncat complet liniile verticale, deoarece tocmai le-au blocat datele. Cu asta, să mergem să punem în aplicare o modalitate de ao face.


Cu grila dezactivată
Cu grila activată

Crearea liniilor folosind căi și lineTo metoda părea a fi cea mai evidentă soluție pentru desenarea graficelor, dar am întâmpinat o eroare de redare care a făcut această abordare inadecvată. Prin urmare, eu sunt lipit cu fillRect metodă de a crea și aceste linii. Aici este funcția în întregime.

 funcția drawGrid () pentru (index = 0; index 

Aceasta este foarte asemănătoare cu desenarea etichetelor axei Y, cu excepția faptului că, în loc de redarea unei etichete, desenăm o linie orizontală care se întinde pe lățimea graficului cu o lățime de 1 px. y ne ajută în poziționarea.

Adăugându-l la Opțiuni

 var defaults = // Alte valori implicite aici disableGrid: false,;

Adăugăm a disableGrid opțiunea la valorile implicite, permițând utilizatorului să controleze dacă o rețea este redată sau nu. Implicit, este redat.

 // Funcționează dacă (! Option.disableGrid) drawGrid (); 

Verificăm doar dacă utilizatorul dorește ca rețeaua să fie redată și să procedeze corespunzător.


contururi

Acum, că toate barele sunt colorate, este lipsit de accent pe fundalul mai deschis. Pentru a remedia acest lucru, avem nevoie de un accident vascular cerebral de 1px. Există două moduri de a face acest lucru. Prima și cea mai ușoară cale ar fi să adăugăm doar o strokeRect metoda pentru a drawGraph metodă; sau, am putea folosi lineTo metoda de a cursa rapid dreptunghiurile. Am ales calea anterioară de când înainte lineTo metoda mi-a aruncat o eroare de interpretare ciudată.


Cu nici un rost
Cu mângâiere

Adăugându-l la Opțiuni

Mai întâi îl adăugăm la implicite Obiectivul este acela de a oferi utilizatorului posibilitatea de a controla dacă acest lucru este aplicat sau nu.

 var defaults = // Alte valori implicite aici showOutline: true,;
 funcția drawGraph () // Codul anterior dacă (opțiunea.showOutline) ctx.fillStyle = "# 000"; ctx.strokeRect (x (index), y (gValues ​​[index]), lățime (), înălțime (gValues ​​[index]));  // Restul codului

Verificăm dacă utilizatorul dorește să redea contururile și, dacă da, vom continua. Acest lucru este aproape la fel ca redarea barelor reale, cu excepția faptului că în loc să se folosească fillRect metoda pe care o folosim strokeRect metodă.


Umbrire

În implementarea inițială, nu există nicio diferențiere între elementul de pânză în sine și spațiul real de redare al barelor. O vom rectifica acum.


Fără umbrire
Cu umbrire
 funcția shadeGraphArea () ctx.fillStyle = "# F2F2F2"; ctx.fillRect (opțiunea.xOffset, 0, gWidth-option.xOffset, gHeight); 

Aceasta este o funcție minusculă care umple zona dorită. Acoperim elementul de pânză minus zona acoperită de etichetele ambelor axe. Primii doi parametri indică coordonatele x și y ale punctului de pornire, iar ultimele două indică lățimea și înălțimea necesare. Începând de la option.offset, eliminăm zona acoperită de etichetele axei Y și prin limitarea înălțimii la gHeight, eliminăm etichetele axei X.


Adăugarea de funcții

Acum că graficul nostru arată destul de bine, ne putem concentra pe adăugarea unor noi caracteristici în pluginul nostru. Ne vom uita la fiecare separat.

Luați în considerare acest grafic al celebrului vârf de 8K.

Când valoarea cea mai mare este suficient de mare și majoritatea valorilor se încadrează în 10% din valoarea maximă, graficul nu mai este util. Avem două modalități de a rectifica acest lucru.


ShowValue

Vom începe mai întâi soluția mai ușoară. Redând valoarea grafurilor respective în partea de sus, problema este rezolvată practic, deoarece valorile individuale pot fi ușor diferențiate. Iată cum este implementată.

 var defaults = // Alte valori implicite aici showValue: true,;

Mai întâi adăugăm o intrare la implicite obiect pentru a permite utilizatorului să-l pornească și să-l oprească la alegere.

 // Funcționează dacă (opțiunea.showValue) drawValue (); 

Verificăm dacă utilizatorul dorește ca valoarea să fie afișată și să procedeze corespunzător.

 funcția drawValue () pentru (index = 0; index 

Noi iteram prin gValues și face fiecare valoare în mod individual. Calculele care implică valAsString și valX sunt niște calcule minuscule care ne ajută în indentările corecte, așa că nu arată prea mult.


Scară

Aceasta este cea mai dificilă dintre cele două soluții. În această metodă, în loc de a începe etichetele axei Y la 0, vom începe mult mai aproape de valoarea minimă. O să-ți explic cum mergem. Rețineți că, în exemplul de mai sus, diferența dintre valorile ulterioare în raport cu valoarea maximă este destul de nesemnificativă și nu demonstrează eficiența acesteia la fel de mult. Alte seturi de date ar trebui să ofere o mai bună analiză a rezultatelor.

Adăugându-l la Opțiuni

 var defaults = // Alte valori implicite aici scale: false;

Actualizarea funcției Scală

Din moment ce scară funcția este o parte integrantă a procesului de redare, trebuie să îl actualizăm pentru a permite caracteristica de scalare. Actualizăm astfel:

 (param-minVal) / (maxVal-minVal)) * gHeight): Math.round ((param / maxVal) * gHeight)); 

Știu că acest lucru pare puțin complicat, dar se pare că se datorează numai utilizării operatorului condițional ternar. În esență, verificăm valoarea lui option.scale și dacă se spune că este fals, codul mai vechi este executat. Dacă este adevărat, în loc de a normaliza valoarea ca o funcție a valorii maxime în matrice, acum o normalizăm ca fiind o funcție a diferenței dintre valorile maxime și cele minime. Care ne aduce la:

Actualizarea mesajului maxValues Funcţie

Acum trebuie să aflăm atât valoarea maximă cât și cea minimă, spre deosebire de maximul pe care trebuie să-l avem înainte. Funcția este actualizată la aceasta:

 funcția minmaxValues ​​(arr) maxVal = 0; pentru (i = 0; iparseInt (arr [i])) minVal = parseInt (arr [i]);  maxVal * = 1,1; minVal = minVal - Math.round ((maxVal / 10)); 

Sunt sigur că ați putea realiza același lucru într-o singură buclă fără a utiliza cât mai multe linii de cod ca mine, dar m-am simțit deosebit de nerecunoscător la acea vreme, deci purtați-mă cu mine. Odată cu eliminarea formalităților de calcul, vom emite o creștere cu 5% a maxval variabilă și la minval variabilă, scădem o valoare egală cu 5% din lui maxval valoare. Aceasta este de a asigura ca barele să nu atingă vârful în fiecare moment, iar diferențele dintre fiecare etichetă a axei Y să fie uniformă.

Actualizarea mesajului drawYlabels Funcţie

Cu toate lucrările de bază, noi continuăm să actualizăm rutina de redare a etichetelor pentru axa Y pentru a reflecta scalarea.

 funcția drawYlabels () ctx.save (); pentru (indice = 0; index 

Actualizați destul de catifelat dacă mă întrebați! Miezul funcției rămâne același. Trebuie doar să verificăm dacă utilizatorul a permis activarea de scalare și deformarea codului după cum este necesar. Dacă este activată, modificăm modul în care etichetele Y sunt atribuite pentru a vă asigura că respectă noul algoritm. În loc de valoarea maximă împărțită în numărul n de numere egal distanțate, compunem acum diferența dintre valoarea maximă și cea minimă, împărțim-o în numere distanțate uniform și adăugăm-o la valoarea minimă pentru a construi matricea etichetelor axei Y. După aceasta, procedăm în mod normal, redând fiecare etichetă în mod individual. Din moment ce am redat manual cel mai de jos 0, trebuie să verificăm dacă scalarea este activată și apoi să se facă valoarea minimă în locul ei. Nu vă supărați micile adăugări numerice la fiecare parametru trecut; este doar să vă asigurați că fiecare element al graficului se poziționează conform așteptărilor.


Redimensionarea dinamică

În implementarea anterioară, am codificat greu dimensiunile graficului, ceea ce reprezintă o dificultate semnificativă atunci când se modifică numărul de valori. Vom remedia acest lucru acum.

Adăugându-l la Opțiuni

 var defaults = // Alte valori implicite aici cvHeight: 250, // In px;

Am lăsat utilizatorul să stabilească numai înălțimea elementului de panza. Toate celelalte valori sunt calculate dinamic și aplicate după cum este necesar.

Actualizarea mesajului initCanvas Funcţie

initCanvas funcția de manipulare toate inițializarea panza, și, prin urmare, trebuie să fie actualizate pentru a implementa noua funcționalitate.

 funcția initCanvas () $ ("#" + dataSource) .after (" "); // Încercați să accesați elementul canvas cv = $ (" #bargraph - "+ dataSource) .get (0); cv.width = gValues.length * (opțiunea.barSpacing + opțiune.barWidth) + opțiune.xOffset + optiunea.barSpacing; cv.height = option.cvHeight; gWidth = cv.width; gHeight = option.cvHeight-20; daca (! cv.getContext) return; // Incercati sa obtineti un context 2D pentru panza si aruncați o eroare dacă nu puteți să ctx = cv.getContext ('2d'); dacă (! ctx) return;

După injectarea elementului de panza, obținem o referință la elementul creat. Lățimea elementului de panza este calculată în funcție de numărul elementelor din matrice - gValues , spațiul dintre fiecare bară - option.barSpacing, lățimea fiecărei bare în sine - option.barWidth și, în sfârșit option.xOffset. Lățimea graficului se modifică dinamic pe baza fiecăruia dintre acești parametri. Înălțimea este modificabilă de utilizator și implicită la 220px, cu suprafața de randare pentru ca barele să fie 220px. Cele 20 de pixeli sunt alocate etichetelor axei X.


Ascunderea sursei

Este logic ca utilizatorul să vrea să ascundă tabela sursă odată ce graficul a fost creat. Având în vedere acest lucru, lăsăm utilizatorului să decidă dacă să elimine masa sau nu.

 var default = // Alte valori implicite aici ascundeDataSource: true,;
 dacă (opțiunea.hideDataSource) $ ("#" + dataSource) .remove ();

Verificăm dacă utilizatorul dorește să ascundă masa și, dacă da, îl eliminăm complet din DOM folosind jQuery elimina metodă.


Optimizarea Codului nostru

Acum, că toată munca grea a fost făcută, putem examina modul de optimizare a codului nostru. Deoarece acest cod a fost scris în întregime pentru scopuri didactice, cea mai mare parte a lucrării a fost încapsulată ca și funcții separate și, în plus, este mult mai detaliată decât trebuie să fie.

Dacă doriți cu adevărat cel mai mic cod posibil, întregul plugin, cu excepția inițializării și a calculului, poate fi rescris în două bucle. Un buclă prin gValues pentru a desena barele și etichetele axei X; iar a doua bucla iterând de la 0 la numYlabels pentru a face grila și etichetele axei Y. Codul ar arăta mult mai confuz, cu toate acestea, ar trebui să conducă la o bază de cod semnificativ mai mică.


rezumat

Asta-i oameni buni! Am creat un plugin de nivel înalt complet de la zero. Am analizat o serie de subiecte din această serie, printre care:

  • Privind schema de redare a elementului de panza.
  • Unele dintre metodele de randare ale elementului canvas.
  • Valorile normalizatoare care ne permit să o exprimăm ca o funcție a unei alte valori.
  • Unele tehnici utile de extragere a datelor folosind jQuery.
  • Logica de bază a redării graficului.
  • Conversia script-ul nostru la un plug-in jQuery cu drepturi depline.
  • Cum să o îmbunătățiți vizual și să o extindeți și mai mult în funcție de caracteristici.

Sper că ai avut atât de multă distracție citește acest lucru ca și cum l-am scris. Aceasta fiind o lucrare de 270 de linii, sunt sigur că am lăsat ceva. Simțiți-vă libertatea de a lovi comentariile și întrebați-mă. Sau mă critică. Sau laudă-mă. Știi, e apelul tău! Codificare fericită!

Cod