Spaces. Smartes Cloud Hosting für anspruchsvolle Webprojekte. Loslegen und Spaces testen. Von Mittwald.
Sven Drieling 1. Juli 2009

JavaScript: Animiertes Säulendiagramm mit HTML Canvas

Mit HTML Canvas braucht man nicht mehr auf Tricks zurück­grei­fen, son­dern kann direkt in JavaScript punkt­ge­naue Grafiken zeich­nen. Wir stel­len das anhand eines ani­mier­ten Säulendiagramms vor. Schauen wir uns an wie Rechtecke inklu­si­ve Farbverlauf gezeich­net wer­den.

In JavaScript kön­nen mit HTML Canvas im Webbrowser kom­ple­xe Grafiken pro­gram­miert wer­den – dazu ste­hen Methoden wie fillRect(),bezierCurveTo() und createLinearGradient() zur Verfügung. Am Beispiel eines ani­mier­ten Säulendiagramms stellt die­ses Tutorial die Grafikprogrammierung mit HTML Canvas vor und beschreibt wie Rechtecke inklu­si­ve Farbverlauf gezeich­net wer­den sowie der Ursprung des Koordinatensystems ver­scho­ben wird.

Beispiel: Animiertes Säulendiagramm – Live Demo plus Quelltext (neu­es Fenster)

Welche Webbrowser unterstützen HTML Canvas?

HTML Canvas wur­de von Apple mit dem Dashboard von Mac OS X ein­ge­führt. Heute wird es unter ande­rem von den Webbrowsern Chrome, Safari 1.3+, Firefox 1.5+, Konqueror und Opera 9.00+ unter­stützt. Für den Internet Explorer steht HTML Canvas mit dem ExplorerCanvas-JavaScript von Google zur Verfügung.

Spezifiziert ist HTML Canvas der­zeit als Entwurf in HTML 5 von der WHATWG und dem W3C.

Rechteck zeichnen

Erster Schritt für das ani­mier­te Säulendiagramm ist das Zeichnen eines gefüll­ten Rechtecks mit fillRect():

fillRect(in float x, in float y, in float w, in float h)

Die ers­ten bei­den Werte x, y geben die lin­ke obe­re Ecke des Rechtecks und die bei­den fol­gen­den w, h die Breite und Höhe des Rechtecks an. Alle Angaben erfol­gen dabei in der Punkteinheit des Koordinatensystems von Canvas, des­sen Ursprung links oben liegt:

var canvas = document.getElementById('canvas');
    if(canvas && canvas.getContext) {
        var ctx = canvas.getContext('2d');
        if(ctx) {
            ctx.fillRect(50, 25, 100, 150); // x, y, w, h
        }
}

Beispiel: fillRect() – Live Demo plus Quelltext (neu­es Fenster)

Canvas und der Grafikkontext

Im Beispiel sieht man, dass fillRect() und auch alle ande­ren Zeichenmethoden nicht direkt zum Canvas-Objekt gehö­ren, son­dern in einen 2D-Grafikkontext lie­gen, den man mit ctx = canvas.getContext(‘2d’) erhält. Dadurch wird das Canvas-Element fle­xi­bler und lässt sich z.B. auch für 3D-Grafik nut­zen: moz-glwe­b20- und ope­ra-3d-Kontext.

Das HTML Canvas-Element selbst wird mit dem Startag <can­vas>, dem Endtag </canvas> sowie der Breite und Höhe des Elements im width- und height-Attribut defi­niert:

<canvas id="canvas" width="200" height="200">
</canvas>

Webbrowser ohne Canvas-Unterstützung zei­gen alles an, was zwi­schen dem Start- und End-Canvas-Tag liegt. Dies soll­te man für sinn­vol­le Fallbacklösungen nut­zen. Im Fall eines Diagramms zum Beispiel für ein Bild:

<canvas id="canvas" width="510" height="200">
  <p><img src="image/Umsatz_2042.png" width="510" height="200" alt="Umsatz 2042" /></p>
</canvas>

Ursprung verschieben

Um zur Ausgabe der auf­recht­ste­hen­de Säulen die Koordinaten nicht selbst umrech­nen zu müs­sen, wird die Transformationsmatrix ver­än­dert. Dazu wird der Ursprung mit translate(20, canvas.height – 30) in den unte­ren Bereich der Zeichenfläche ver­scho­ben und die Koordinaten mit scale(1, -1) an der x-Achse gespie­gelt.


Beispiel: trans­la­te() – Live Demo plus Quelltext (neu­es Fenster)

Farbverlauf

Farbverläufe sind eine ein­fa­che Möglichkeit, eine Grafik anspre­chen­der aus­se­hen zu las­sen. Ein linea­rer Farbverlauf wird im Canvas mit createLinearGradient(x1, y1, x2, y2) erzeugt. Die x/y-Koordinaten geben den Start- und Endpunkt der Linie an über die sich der Farbverlauf erstreckt.

Wichtig ist dabei, dass die­se Punkte in dem Bereich lie­gen, in dem das Zeichnen erfolgt. Im Fall der maxi­mal 120 Punkte hohen Säulen ist dies eine Linie von 0, 0 nach 0, 120 zum Füllen der Säulen von unten nach oben. Liegen die Endpunkte des Farbverlaufs außer­halb der gezeich­ne­ten Figur, dann bleibt die Figur ein­far­big wie das createLinearGradient()-Beispiel zeigt:

Beispiel: createLinearGradient() – Live Demo plus Quelltext (neu­es Fenster)

Die Farben des Farbverlaufs wer­den mit addColorStop(offset, color) gesetzt. Mit off­set wird die Position der Farbe auf der Linie beschrie­ben. Dieser Wert liegt zwi­schen 0.0 und 1.0. 0.0 steht für den Startpunkt der Linie, 1.0 für den Endpunkt. Die Farbe wird als String mit einem CSS 3-Farbwert ange­ge­ben. Folgender Programmcode defi­niert einen Farbverlauf von Gelb, nach Rot in der Mitte, zu Blau im Endpunkt:

var fill = ctx.createLinearGradient(0, 0, 0, canvas.height - 1);

fill.addColorStop(0.0, 'yellow');       // Gelb
fill.addColorStop(0.5, '#ff0000');      // Rot
fill.addColorStop(1.0, 'rgb(0,0,255)'); // Blau

Beispiel: addColorStop() – Live Demo plus Quelltext (neu­es Fenster)

Status der Transformationsmatrix für Hintergrund retten

Während für die Säulen der Ursprung in den unte­ren Bereich ver­scho­ben wird, soll sich der Farbverlauf für den Hintergrund dage­gen über den gesam­ten Canvas von der lin­ken obe­ren bis zur rech­ten unte­ren Ecke erstre­cken. Es wird also ein­mal eine Transformationsmatrix für den Ursprung links oben im Canvas-Element (die vor­ge­ge­be­ne Transformationsmatrix) und eine zwei­te für das Zeichnen der Säulen mit dem ver­scho­be­nen Ursprung benö­tigt. Dies kann man im Canvas leicht mit den save()- und res­to­re()-Methoden errei­chen:

save() ret­tet alle Eigenschaften des Canvas auf einen Stack. Dazu gehö­ren u.a. die gesetz­ten Farben, die Strichstärke, die Schattenwerte und die Transformationsmatrix.

res­to­re() holt die­se Werte wie­der vom Stack run­ter und setzt die Eigenschaften auf ihre zuvor geret­te­ten Werte zurück. Damit redu­ziert sich der Wechsel des Ursprungs auf fol­gen­den Programmcode:

Hintergrund zeichnen;

save(); // Ursprung retten

  translate(); // Ursprung verschieben
  scale();     // Koordinaten spiegeln

  Säulen zeichnen;

restore(); // Ursprung wiederherstellen

Beispiel: Animiertes Säulendiagramm – Live Demo plus Quelltext (neu­es Fenster)

Animation

Im letz­ten Schritt wer­den die Säulen ani­miert. Eine ein­fa­che Schleife für Animationen ist fol­gen­de:

forever {
  Neue Positionen berechnen;
  Objekte zeichnen;
  40 Millisekunden warten; // für 25 Bilder/Sekunde
}

In JavaScript gibt es jedoch kei­ne Methoden wie waitTime() oder delay(), um ein Skript 40 Millisekunden war­ten zu las­sen. Stattdessen stellt JavaScript setTimeout() und setTimeInterval() zur Verfügung:

timer = setTimeout(expression, time­out) führt nach Ablauf der in Millisekunden ange­ge­be­nen time­out-Zeit ein­ma­lig den JavaScript-Ausdruck expres­si­on aus.

Beispiel: setTimeout() – Live Demo plus Quelltext (neu­es Fenster)

timer = setInterval(expression, inter­val) führt dage­gen den Ausdruck im Abstand von inter­val Millisekunden wie­der­holt aus, bis der so gestar­te­te Zeitgeber mit clearInterval(timer) wie­der gelöscht wird – genau das, was für eine Animation benö­tigt wird.

function startTimer() {
    timer = setInterval(logTimeDiff, 1000 / 25);
}

function logTimeDiff() {
    if(count++ > 20) {
        clearInterval(timer); // Zeitgeber stoppen
    }

    // Zeitdifferenz ausgeben
}

Beispiel: setInterval() – Live Demo plus Quelltext (neu­es Fenster)

Stopp nach 2 Sekunden Laufzeit

Die Säulenanimation soll nicht end­los lan­ge lau­fen, son­dern nach 2 Sekunden sol­len die Säulen kom­plett sicht­bar sein. Zum Stoppen der Animation merkt sich das Skript daher mit startTime = new Date().getTime() den Startzeitpunkt der Animation, ver­gleicht ihn in ani­ma­te() mit der aktu­el­len Zeit und been­det die Animation durch das Löschen des Timers mit cleanInterval(timer).

var timer    = null; // JavaScript Timer
var duration =  2;   // Laufzeit in Sekunden
var fps      = 25;   // Bilder pro Sekunde

function animStart() {
    startTime = new Date().getTime();
    timer = setInterval(animate, 1000 / fps);
}

function animate() {
    var diffTime = new Date().getTime() - startTime;

    // Ende?
    if(diffTime >= 1000 * duration) {
        clearInterval(timer);
    }

    draw();
}

Flüssige Animation

Am setInterval()-Beispiel wird man wohl auch auf dem eige­nen Rechner sehen kön­nen, dass die logTimeDiff()-Funktion nicht immer exakt nach 40 Millisekunden auf­ge­ru­fen wird:

  • Timer start
  • 47
  • 49
  • 40
  • 41
  • 39
  • 57
  • 40

Die Unterschiede erge­ben sich aus Faktoren wie der Genauigkeit des Zeitgebers des Betriebssystems, dem Verhalten des Webbrowsers, dem was sonst noch auf dem Rechner und im Webbrowser gera­de abläuft. Damit die Animation trotz­dem mög­lichst flüs­sig aus­sieht benutzt das Skript einen Skalierungsfaktor, der abhän­gig von der ver­stri­che­nen Zeit zwi­schen 0.0 und 1.0 liegt und in ani­ma­te() berech­net wird: sca­le = diffTime / (1000 * dura­ti­on). Mit die­sem Faktor wird die Höhe der Säulen gezeich­net: fillRect(0, 0, 30, sca­le * value)

Am Ende scale auf 1.0 setzen

Da durch die Ungenauigkeit des Zeitgebers die ver­stri­che­nen Zeit am Ende über 2 Sekunden lie­gen kann und der Skalierungsfaktor dann grö­ßer als 1.0 ist, wird er zum Schluss auf den gewünsch­ten Endwert von 1.0 gesetzt. So wird das Skript robust gegen­über dem unge­nau­en Zeitgeber und das Diagramm zeigt kei­ne fal­schen Werte an:

// Ende?
if(diffTime >= 1000 * duration) {
    scale = 1.0; // Auf 1.0 setzen, damit die Säulen am Schluss mit
                 // Sicherheit mit dem richtigen Wert gezeichnet werden
    clearInterval(timer);
}

Namensraum per Objektliteral

Damit das fer­ti­ge Skript nicht mit ande­ren gleich­zei­tig auf einer HTML-Seite ein­ge­bun­de­nen Skripts kol­li­die­ren kann, ist es mit Hilfe des Objektliterals im eige­nen Namensraum ein­ge­schlos­sen. Aus:

var timer     = null; // JavaScript Timer
var startTime =  0;   // Startzeitpunkt der Animation
var fps       = 25;   // Bilder pro Sekunde

function anim() {
}

function animStart() {
  startTime = new Date().getTime();
  timer = setInterval(animate, 1000 / fps);
}

Wird im Objektliteral:

var bc = {
   timer    : null, // JavaScript Timer
   startTime:  0,   // Startzeitpunkt der Animation
   fps      : 25;   // Bilder pro Sekunde

   anim: function() {
   },

   animStart: function() {
       bc.startTime = new Date().getTime();
       bc.timer = setInterval(bc.animate, 1000 / bc.fps);
   }
};

Tipp: Das Objektliteral und dar­über hin­aus­ge­hen­de Möglichkeiten für den Aufbau von JavaScripts beschreibt Mathias Schäfer in Organisation von JavaScripten.

Zusammenfassung

Beispiel: Animiertes Säulendiagramm – Live Demo plus Quelltext (neu­es Fenster)

Dieses Tutorial hat anhand eines ani­mier­ten Säulendiagramms vor­ge­stellt wie in HTML Canvas direkt mit fillRect() gezeich­net, ein Farbverlauf mit createLinearGradient() und addColorStop() beschrie­ben, der Ursprung und die Ausrichtung der Koordinatenachsen mit trans­la­te() und sca­le() ver­än­dert, mit Hilfe von setInterval() die Ausgabe ani­miert und der Programmcode mit Hilfe des Objektliterals im eige­nen Namensraum ein­ge­schlos­sen wird.

Die Demos, Anwendungen und Tutorials in der Linkliste zei­gen wei­te­re Möglichkeiten des HTML Canvas-Elements.

Material zum Download

Zip-Archiv mit allen aus­führ­ba­ren Beispielen.

Links

Sven Drieling

**** 38911 BASIC BYTES FREE ****

READY.
[]

14 Kommentare

  1. Super ele­gan­te Lösung, die ich ger­ne in mei­ne Seite ein­bau­en möch­te.

    Nun aber zu fol­gen­der Problematik.
    Ich habe x1-xn Links, die jeweils ein Diagramm aus­ge­ben sol­len.
    Sprich Link1 zeigt Diagramm1; Link2 zeigt Diagramm2 usw.
    Das Ganze kann ich per Parameterübergabe bei onClick rea­li­sie­ren, jedoch schei­te­re ich dabei, die Values ent­spre­chend anzu­pas­sen.
    Damit ich nicht für jedes Digramm das Skript neu ein­bin­den muss, möch­te ich ein­fach nur die values aus­tau­schen.
    An die­ser Position: values : [87, 70, 90, 50, 105, 120, 75, 83, 110, 76, 100, 120], // Angezeigte Werte
    wer­den die Werte eines Diagramms ange­zeigt, ger­ne hät­te ich aber etwas in einer Form wie:

    values : [87, 70, 90, 50, 105, 120, 75, 83, 110, 76, 100, 120];
    [87, 70, 90, 50, 105, 120, 75, 83, 110, 76, 100, 120];
    [87, 70, 90, 50, 105, 120, 75, 83, 110, 76, 100, 120],

    Wie wür­de müss­te das syn­tak­tisch rich­tig aus­se­hen und wie die Schleife dazu?

    Versuch (der wis­sent­lich nicht stimmt):
    switch (values){
    case “1”: [x1, x2,…,xn];
    break;
    case “2”: [y1, y2,…,yn];
    break;
    case “n”: [z1, z2,…,zn];
    break;
    default:
    echo (“Es sind kei­ne Werte vor­han­den”);
    }
    So in etwa hät­te ich das ger­ne. Aber wie?

  2. Ich hab grad im Rahmen einer Projektarbeit mei­ner Uni eine Javascript-Bibliothek geschrie­ben mit der man sehr ein­fach Formen auf dem can­vas Element zei­chenn und ani­mie­ren kann.

    In den Beispielen hab ich auch ein Chart mit drin. Schaut es euch doch mal an ;) Wenn ihr was mit dem can­vas ele­ment machen wollt, dann spart ihr euch mit “cajal” vie­le Zeilen Code!

    Das gan­ze is open-source. Also pro­biert es ein­fach mal aus und wenn ihr fin­det, dass es Verbesserungswürdig ist, dann schreibt ein­fach ein “issue” oder for­ked es und macht nen push request :P

    Grüße, ren­dro

    http://github.com/dotsunited/cajal

  3. Sicherheitslücken und Javascript: das ich nicht lache!
    Mit Javascript kann man aller­höchs­tens den Browserverlauf bedingt aus­le­sen.

    Und Im Moment ist mir der IE sowas von egal!
    Ich pro­gram­mie­re die Site z.Zt. nur für mich und will nun mal ein Malprogramm mit Canvas pro­bie­ren zu schrei­ben.

  4. Google ExplorerCanvas bringt die HTML5 Canvas Funktionalität in den Internet Explorer.
    http://code.google.com/p/explorercanvas/

  5. Die Werte ste­hen doch im Quelltext der Seite und Google juckt doch das CSS nicht!? Link mal ange­klickt? :-)

    Google dürf­te eher noch dank­bar sein, weil die Werte struk­tu­riert sind. Der Hauptgedanke war aber, dass ich die Seite auch mit einem “Konsolenbrowser” les­bar hal­ten woll­te.

  6. @Oliver:
    Tabellen für das Darstellen von Werten zu benut­zen, ist ja eigent­lich im Sinne des Erfinders. Diese Werte aber in der pad­ding-Höhe der Span-Klasse zu ver­ste­cken ist schon grenz­wer­tig. Was soll ein Google-Bot mit so einer Darstellung anfan­gen ?

  7. Ich habe es jetzt mal so geän­dert, dass die Balken in der Tabelle noch ani­miert wer­den, indem das pad­ding der spans um die Werte geän­dert wird. Es funk­tio­niert in allen getes­te­ten Browsern wun­der­bar. :-)

    Getestet:
    – IE 6/7/8
    – FF 2/3
    – Opera 9.5/9.6/10
    – Safari 4
    – Chrome 2

    Für die Animation ein­fach einen ande­ren Zeitraum wäh­len. Beim nor­ma­len Aufruf wird nichts ani­miert, weil das Javascript erst ganz unten auf der Seite gela­den wird und dann macht die Animation kei­nen Sinn. Und ohne Javascript ist halt nichts ani­miert und die Balken sofort da.

  8. Man kann ja auch Tabellen und CSS ver­wen­den um schi­cken Säulendiagramme zu machen. Wenn man das will, kann man die­se ja auch ani­mie­ren, indem man pad­ding und mar­gin ändern. vom “Aufwand” her kommt das aufs glei­che raus wie mit Canvas, nur dass man sich das Fallback spa­ren kann.

    kein JS => kei­ne Anmination
    kei­ne Grafiken => Alles ohne Verläufe
    kein CSS => Daten in Tabelle

    Das habe ich z. B. da ver­wen­det (ohne Animation der Säulen)
    -> http://parteigezwitscher.de/#stats_hl

    Das Ding ist eine ganz nor­ma­le Tabelle mit ein paar CSS Tweaks.

  9. Für IE6 eine abge­speck­te Version der Seite anzei­gen las­sen und damit sanf­ten Upgrade-Druck aus­üben könn­te den Prozess etwas beschleu­ni­gen.

  10. Es gibt für alles Gesetzte.….wieso keins um den IE6 ENDLICH zu ent­fer­nen? ;)

  11. Wenn man Firmen als Zielkunden/Besucher avi­siert, ist es eigent­lich immer eine gute Idee, wenn Webseiten auch ohne Javascript funk­tio­nie­ren. Das Plugin “Noscript” wird ja auch immer belieb­ter.

    Welche Fachzeitschriften emp­feh­len denn, Javascript abzu­schal­ten? Das wäre ja mal ein Grund, in die­se dann viel­leicht doch nicht ganz schlech­ten Zeitschriften einen Blick zu wer­fen…

  12. DeFacto-Standard bei Mittelständlern ist IE 6 , in 50% der Fälle mit Javascript.

  13. @Turicon: Die Admins fol­gen dem Leitspruch: “Never chan­ge a run­ning sys­tem”. Das Problem ist, dass mit “run­ning” kei­ne Rücksicht auf Webseiten genom­men wird. Und mit deak­ti­vier­tem JavaScript sind sie auch gleich das Problem mög­li­cher Sicherheitslücken los. Auch heut­zu­ta­ge emp­feh­len ja immer wie­der mal irgend­wel­che Fachzeitschriften oder ande­re Quellen JavaScript abzu­schal­ten.

  14. Vielen Dank für den Beitrag. Bis wir das aber wirk­lich “ohne Probleme” ein­set­zen kön­nen, wer­den noch vie­le Jahre ins Land gehen. Wozu soll ich den Aufwand betrei­ben, wenn ich eh noch eine “Fallback-Grafik” ein­bin­den muss?

    Habe es eben erst wie­der in einer grö­ße­ren Firma und deren Partner erlebt: ein­zig ver­füg­ba­rer Browser an den Arbeitsplatz-PCs? Klar, IE6! Und natür­lich ohne JavaScript.

    Irgendwie fehlt mir dann das Verständnis für die Admins die­ser Netzwerke, immer noch (und über­haupt) die­sen Browser ein­zu­set­zen…

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.