Googles Chart-Service zeichnet optisch ansprechende Diagramme, auch von unkonventionell organisierten Datensätzen. Ein CPAN-Modul gibt die Anweisungen in objektorientiertem Perl durch, statt vom Programmierer URL-Fitzelei zu verlangen.
Auf Open-Source-Konferenzen kann man heutzutage ja kaum noch mit einem Windows-Laptop antreten, es sei denn, man möchte als Steinzeitmensch und Lachnummer bestaunt werden. Deswegen liebäugelte ich schon länger mit einem Ersatz, und als dann neulich die Firma Dell eines dieser putzigen Mini-9-Ubuntu-Netbooks zum unschlagbaren Preis von 230 Dollarn feilbot, schlug ich endlich zu. Mein Arbeitskollege Leif fand dann auch noch einen lustigen Namen für den Winzling: ``Mini-Me'', nach dem Klon von Dr. Evil aus dem zweiten Austin-Powers-Film.
Abbildung 1: Das kleine Dell Netbook mit Ubuntu |
Der erste Eindruck war berauschend, es funktionierte sogar alles halbwegs! Als ich dann noch die mageren 512MB RAM mit 2GB eines Billiganbieters für $9.95 nachrüstete, war mein Glück perfekt, allerdings schlich sich ein wüster Gedanke ein: Würde das Netbook mit dem größeren Speicherbaustein im Suspend-Mode mehr Strom schlucken, also die Batterie vorzeitig entleeren? Dem Ingenieur ist nichts zu schwör, also ging ich der Sache auf den Grund.
Abbildung 2: Die gemessenen Daten auf handgeschriebenen Notizzetteln |
Also baute ich zunächst wieder das alte Speichermodul ein, suspendierte
den Rechner und las in unregelmäßigen Abständen über die nächsten
36 Stunden auf dem dazu kurz reanimierten
Gerät den Batteriestand ab. In Abbildung 2 sind die handgeschriebenen
Notizzettel zu sehen, die das aktuelle Ergebnis jeweils neben der
Uhrzeit auflisten.
Das gleiche Verfahren wiederholte ich eineinhalb Tage später noch
einmal mit dem dazu wieder eingebauten 2GB-Baustein. Beide Messreihen
weisen wegen des unorthodoxen Verfahrens unterschiedlich Messzeitpunkte
auf. Um die Entladungskurven wie in Abbildung 3 nebeneinander grafisch
darzustellen, werden die Daten erst mit dem Skript in Listing data-
normalize
normalisiert und anschließend mit dem Skript in Listing
graph-discharge
mit Googles Chart-Service gezeichnet.
Das Ergebnis zeigt, dass
die Entladung mit beiden Speichermodulen anfangs in etwa gleich
schnell verläuft. Und bei schwächer werdender Batterie saugt das
größere Speichermodul sogar etwas mehr Saft ab und sorgt für eine schnellere
Entladung des Akkus. Kein besorgniserregender Vorgang, aber schön,
wenn man harte Daten in einem ansprechend gestalteten Diagramm
vorweisen kann.
Abbildung 3: Google Charts zeichnet die Entladung des Netbooks |
Das Diagramm erzeugt kein Programm auf dem lokalen Rechner, sondern ein Clusterrechner der Firma Google. Das Perl-Skript erzeugt lediglich einen URL nach Abbildung 4, schickt ihn an den Google-Chart-Service und zurück kommt ein Bild im PNG-Format, das genau wie in Abbildung 3 aussieht. Google beschränkt die Zugriffe auf 50.000 pro Tag, das sollte also für private Spielereien für's Erste reichen. In [5] wurde der Service schon einmal vorgestellt, um Spammer auf einer Weltkarte einzuzeichnen.
Um allerdings von den Anforderungen eines Diagramms wie in Abbildung 3 auf den URL in Abbildung 4 zu kommen, muss der Diagrammkonstrukteur die Bedienungsanleitung auf [4] genau studieren und die verschiedenen Regeln und Kürzel herausfieseln. Einfacher geht das mit dem CPAN-Modul Google::Chart, das eine objektorientierte Schnittstelle zur Diagramm-Definition anbietet und den URL Schritt für Schritt mittels leicht verständlicher Methodenaufrufe zusammensetzt. Doch vor der eigentlichen Diagrammdefinition steht noch eine Konsolidierung der Messdaten an.
Abbildung 4: Der an Google geschickte URL, auf den hin der Chart Service das Diagramm in Abbildung 3 zeichnet. |
Die Messungen wurden in unregelmäßigen Zeitabständen durchgeführt und
auch nicht zeitgleich für beide Speicherbausteine. Um die beiden
Entladungskurven nun so nebeneinander zu stellen, dass ein direkter
Vergleich möglich ist, verschiebt das Skript in Listing data-normalize
die Messzeitpunkte auf eine gemeinsame Zeitachse. Beide Messreihen
starten dann an einem virtuell gewählten Zeitpunkt. Das Skript
rechnet die absoluten Messzeitpunkte durch Abziehen des virtuellen
Startzeitpunkts in relative Zeitpunkte bezüglich des gemeinsamen
Startzeitpunkts um. Enthält die erste Messreihe also zum Beispiel
Messpunkte zu den Zeitpunkten 8:00 und 9:00, während die zweite
Messreihe um 11:00 und 11:30 gemessene Werte enthält, startet die
virtuelle gemeinsame Zeitachse zum Beispiel um 0:00 und zeigt
um 0:30 den Messpunkt der Messreihe 2 und um 1:00 den Messpunkt
der Messreihe 1.
Abbildung 5: Die zwei Messreihen mit unterschiedlichen Zeitachsen rechnet das Skript data-normalize auf eine gemeinsame virtuelle Zeitachse um. |
01 #!/usr/local/bin/perl -w 02 use strict; 03 use DateTime; 04 05 my @result = (); 06 my $max = {}; 07 08 my $data = { 09 "2gb" => [qw( 10 21:33 100 08:18 83 10:52 80 11 18:40 57 08:36 35 12:21 28 12 )], 13 "0.5gb" => [qw( 14 14:44 100 16:09 97 18:08 95 15 20:43 88 22:19 86 08:47 73 16 15:19 65 17:52 61 21:19 56 17 23:04 55 07:35 43 18 )]}; 19 20 for my $conf (keys %$data) { 21 22 my $points = $data->{ $conf }; 23 my $day_start; 24 my $day_current; 25 26 while( my($time, $charge) = 27 splice( @$points, 0, 2 ) ) { 28 29 my($hour, $minute) = split /:/, $time; 30 31 if(!defined $day_start) { 32 $day_start = DateTime->today(); 33 $day_start->set_hour( $hour ); 34 $day_start->set_minute( $minute ); 35 $day_current = $day_start->clone(); 36 } 37 38 my $time_current = 39 $day_current->clone(); 40 $time_current->set_hour( $hour ); 41 $time_current->set_minute( $minute ); 42 43 if($time_current < $day_current) { 44 $time_current->add( days => 1 ); 45 $day_current->add( days => 1 ); 46 } 47 48 $day_current = $time_current->clone(); 49 50 my $x = (($time_current->epoch() - 51 $day_start->epoch()) / 60); 52 53 push @result, [ $conf, $x, $charge ]; 54 55 if(!exists $max->{x} or 56 $max->{x} < $x) { 57 $max->{x} = $x; 58 } 59 if(!exists $max->{y} or 60 $max->{y} < $charge) { 61 $max->{y} = $charge; 62 } 63 } 64 } 65 66 my $margin = 2; 67 68 for my $result (@result) { 69 my($symbol, $x, $y) = @$result; 70 print "$symbol ", 71 int($x*(100-2*$margin)/ 72 $max->{x})+$margin, 73 " ", 74 int($y*(100-2*$margin)/ 75 $max->{y})+$margin, 76 "\n"; 77 }
Hierzu bemüht das Skript data-normalize
eine Datenstruktur $data,
die eine Referenz auf einen Hash enthält, der unter den Schlüsseln
2gb
und 0.5gb
jeweils einen Array von Messpunkten mit Wertepaaren
aus Zeitpunkt und gemessener Batteriekapazität enthält.
Ziel des Verfahrens ist es, beide Messkurven an einem gemeinsamen Zeitpunkt zu starten und sowohl die Zeitwerte in X-Richtung als auch die Messwerte in Y-Richtung auf den Integerbereich zwischen 0 und 100 zu normieren, denn der Chart-Service erwartet die Daten in diesem Format.
Das Skript führt die Zeitrechnung mit dem CPAN-Modul DateTime durch
und legt den Zeitpunkt der ersten (zufällig gewählten) Messreihe als
Startpunkt der gemeinsamen Zeitachse in $day_start
fest.
In der Variablen $time_current
speichert es den Zeitpunkt
des aktuell bearbeiteten Messpunkts ab. In $day_current
hingegen
liegt das Datum des aktuellen Tages des letzten Messpunktes.
Stellt das Skript fest, dass zwischen
zwei Messdaten ein ja nicht explizit angegebener Tagessprung liegt
(zum Beispiel wenn nach einer Messung um 23:00 eine um 8:00 vorliegt),
addieren die Zeilen 44 und 45 einen Datumstag zu den Zählern.
Die Anzahl der Minuten seit der letzten Messung bestimmt Zeile 50,
legt sie in der Variablen $x (für X-Achsen-Werte)
ab, von wo sie in Zeile 53 zusammen
mit dem Namen der Messreihe und dem aktuellen Messwert in den
Ergebnis-Array @results
wandert.
Die Zeilen 55 bis 62 halten die bislang höchsten X- und Y-Werte
fest und legen sie in den Hash-Einträgen $max->{x}
und $max->{y}
fest. Sie dienen später in der for-Schleife ab Zeile 68 dazu, alle
X/Y-Werte auf den Bereich zwischen 0 und 100 zu normieren. Zusätzlich
spart die Variable $margin
am rechten und linken Rand noch einen
Bereich der Breite 2 aus, letztendlich normiert data-normalize
also
X/Y-Werte auf den Bereich zwischen 2 und 98. Der Grund hierfür ist
später im Diagramm zu sehen, es sieht einfach schöner aus, wenn die
Kurven nicht ganz an die horizontalen und vertikalen Achsen hinreichen.
Das Ergebnis schickt print
in Zeile 70 nach STDOUT, und
die Ausgabe sieht wie in Abbildung 5 gezeigt aus.
Auf der Suche nach einem passenden Diagrammformat findet sich auf [4]
der Typ ``lxy'', der für jede gezeichnete Linie einen Satz X- und
einen Satz Y-Koordinaten entgegen nimmt. Er unterscheidet sich damit
fundamental von anderen Formaten, die annehmen, dass Messpunkte für
alle dargestellten Datensätze an den gleichen X-Werten vorliegen.
Zeile 19 in graph-discharge
legt deshalb ``XY'' für die Option
type
im Konstrukturaufruf für ein neues Objekt vom Typ
Google::Chart fest.
Listing graph-discharge
liest nun zunächst in Zeile 8 die Ausgabe
des Skripts data-normalize
ein und sortiert sie nach X- und Y-Werten
getrennt in eine Datenstruktur $data
um. Am Ende der while
-Schleife
in Zeile 15 liegen demnach in $data->{"0.5gb"}->{x}
alle
normierten Zeitstempel für die Konfiguration mit 512MB, während in
$data->{"0.5gb"}->{y}
ein Array aller zugehörigen Batteriestände
liegt. Damit ist es einfach, dem Parameter data
des
Google::Chart-Objektes in Zeile 21 eine Referenz auf einen Array von
X/Y-Datensätzen zu übergeben, so, wie der Charttyp XY dies wünscht.
Der Parameter size
setzt die Ausmaße des Diagramms, 700 mal 400 ist
so ziemlich das Maximum, bei größeren Dimensionen stellt sich Google
quer und schickt einen ``Bad Request'' zurückt. Der Titel des Diagramms,
den es am oberen Rand darstellt, setzt die Option title
in Zeile 29.
Damit der Diagrammhintergrund nicht weiß bleibt, sondern einen
professionell angehauchten Übergang von weiß nach olivgrün zeigt, setzt
die Option fill
in Zeile 33 ihn auf ``LinearGradient''. Wie auf
[4] unter der Rubrik ``Colors'' nachzulesen, verlangt dieses Verfahren
den Wert ``c'' für das Füllen des Chart-Bereichs und nimmt in
color1
und color2
zwei hexkodierte RGB-Farben entgegen. Hat
der jeweilige offset
den Wert 0, ist die zugehörige Farbe am linken
Diagrammrand pur und am rechten Rand ausgewaschen. Der Wert 1 hingegen
bestimmt einen Farbübergang von rechts nach links. Die Werte in den Zeilen
36 bis 41 geben also an, dass der Diagrammhintergrund von links nach
rechts von weiß auf ein sattes olivgrün umschwenkt. Ein Winkel von 45
Grad bestimmt, dass sich der Übergang diagonal von links unten nach rechts
oben vollzieht.
001 #!/usr/local/bin/perl -w 002 use strict; 003 use Google::Chart; 004 use Google::Chart::Marker; 005 006 my $data = {}; 007 008 open PIPE, "./data-normalize |" or die; 009 while(<PIPE>) { 010 chomp; 011 my($symbol, $x, $y) = split ' ', $_; 012 next unless $y; 013 push @{ $data->{ $symbol }->{x} }, $x; 014 push @{ $data->{ $symbol }->{y} }, $y; 015 } 016 close PIPE or die; 017 018 my $graph = Google::Chart->new( 019 type => 'XY', 020 021 data => [$data->{"0.5gb"}->{x}, 022 $data->{"0.5gb"}->{y}, 023 $data->{"2gb"}->{x}, 024 $data->{"2gb"}->{y}, 025 ], 026 027 size => '750x400', 028 029 title => { 030 text => "Dell Mini Standby Discharge" 031 }, 032 033 fill => { 034 module => "LinearGradient", 035 args => { 036 target => "c", 037 angle => 45, 038 color1 => "abbaab", 039 offset1 => 1, 040 color2 => "FFFFFF", 041 offset2 => 0, 042 } 043 }, 044 045 grid => { 046 x_step_size => 33, 047 y_step_size => 20, 048 }, 049 050 axis => [ 051 { location => 'x', 052 labels => [1..36], 053 }, 054 { location => 'y', 055 labels => [0,25,50,75,100], 056 }, 057 ], 058 059 color => ['E6E9FD', '4D89F9'], 060 061 legend => ['0.5gb', '2gb'], 062 063 margin => [50, 50, 50, 50, 100, 100], 064 065 marker => Google::Chart::Marker->new( 066 markerset => [ 067 { marker_type => 'x', 068 color => 'FFCC33', 069 dataset => 0, 070 datapoint => -1, 071 size => 15, 072 priority => 1, 073 }, 074 { marker_type => 'x', 075 color => 'FF0000', 076 dataset => 1, 077 datapoint => -1, 078 size => 15, 079 priority => 1, 080 }, 081 { marker_type => 'D', 082 color => 'E6E9FD', # light blue 083 dataset => 0, 084 datapoint => -1, 085 size => 4, 086 priority => -1, 087 }, 088 { marker_type => 'D', 089 color => '4D89F9', # blue 090 dataset => 1, 091 datapoint => -1, 092 size => 4, 093 priority => -1, 094 }, 095 ]), 096 ); 097 098 $graph->render_to_file(filename => 099 "chart.png"); 100 system("xv chart.png"); 101
Das Stützliniengitter im Diagramm zeichnet die Option grid
, die
den dargestellten Zeitraum von etwa 36 Stunden in X-Richtung in drei
Teile a 33 von 100 Normpunkten teilt. In Y-Richtung bestimmen waagrechte
Gitterlinien jeweils einen Abstand von 20 Punkten.
Die Beschriftung der Diagrammachsen bestimmt die Option axis
in Zeile 50.
Die X-Achse erhält die Stundenwerte von 1 bis 36 in gleichen Abständen
zugeordnet, die Y-Achse die Werte 0 bis 100 in 25er-Schritten. Zu beachten
ist, dass diese Beschriftungen völlig unabhängig von den eingezeichneten
Datensätzen sind, die ja in beiden Richtungen zwischen 0 bis 100
normiert sind.
Die Farben der beiden Linien der dargestellten Datensätze setzt die
Option color
in Zeile 59 in der Reihenfolge der eingespeisten Daten.
E6E9FD
ist ein ganz leichtes Blau, während 4D89F9
ein satteres
Himmelblau bestimmt. Damit der Betrachter die beiden Linien auch den
Datensätzen zuordnen kann, bestimmt legend
in Zeile 61, dass am
rechten Rand des Diagramms nebst einem zur Linie passenden Farbbatzen
der Name der zugehörigen Messreihe erscheint.
Damit das Diagrammbild die Achsen nicht direkt an den Rand des
Bildes knallt, bestimmt die Option margin
in Zeile 63 jeweils einen
Rand von 50 Pixeln in allen Richtungen fest. Die letzten beiden Werte
mit einem Wert von jeweils 100 legen
den Abstand der Legende in X-Richtung vom rechten Bildrand und in Y-Richtung
vom automatisch gewählten Darstellungspunkt nach oben an.
Wer nicht nur die aus den Messpunkten konstruierte Diagrammlinie sehen
möchte, sondern auch noch die einzelnen Messpunkte als Kreuzchen, setzt
wie in Zeile 65 gezeigt, die Option marker
. Die ersten beiden Elemente
des markerset
s bestimmen die Kreuzchen des ersten und des zweiten
Datensatzes, das sie jeweils mit dataset => 0
und dataset => 1
festlegen. Ist datapoint
auf -1 gesetzt, zeichnet der Chartservice
jeden Messpunkt ein, man kann aber auch eine Auswahl festlegen. Marker
mit der Priorität 1 werden als letztes gezeichnet, so stellt graph-discharge
sicher, dass die Kreuzchen nicht durch Linien verdeckt werden. Entsprechen
die gezeichneten Diagrammlinien nicht den ästhetischen Bedürfnissen
des Betrachters und werden dickere Linien gewünscht, geht auch dies
(etwas kontraintuitiv) mit marker
. Statt einem Kreuzchen mit ``x'' als
marker_type
bestimmt ein Eintrag mit dem Typ ``D'' Linieneigenschaften
wie Dicke oder Farbe. Möchte man also die Diagrammlinien einfach verdicken,
legt man einfach die gleiche Farbe wie vorher mit der color
-Option
in Zeile 59 fest und bestimmt eine Markerdicke von 4. Die Priorität -1
stellt sicher, dass die Linien sich nicht mit den Kreuzchen ins
Gehege kommen und immer unter letzteren bleiben.
Die schließlich in Zeile 98 aufgerufene Methode render_to_file
stellt den URL nach Abbildung 4 zusammen, schickt ihn an Google und
nach etwa einer Sekunde kommt das fertige Bild zurück, das die
Methode unter dem angegebenen Dateinamen auf der Festplatte im PNG-
Format ablegt.
Das Modul Google::Chart ist vom CPAN erhältlich und benötigt das
postmoderne Objektsystem Moose
, das einen ganzen Rattenschwanz
an Abhängigkeiten hinterherschleift, also ist eine CPAN-Shell oder
ein Package-Manager hilfreich. Zum Zeitpunkt der Fertigstellung dieses
Artikels fehlten Google::Chart allerdings noch einige Funktionen, aber
nach kurzer Konversation mit den Entwicklern übernahmen diese
freundlicherweise schnell meine Patches auf der
Kollaborationsseite Github.com und spielten sie in die Entwicklerversion
0.05014_01, die unter [3] neben dem stabilen Release zum Download
bereit steht. Lang lebe Github, ein neues Zeitalter der
Open-Source-Kollaboration ist angebrochen!
Listings zu diesem Artikel: ftp://www.linux-magazin.de/pub/listings/magazin/2009/07/Perl
Github-Projekt des Moduls Google::Chart: http://github.com/lestrrat/google-chart/tree/master
CPAN-Version des Moduls Google::Chart (einschließlich Testversion 0.05014_01) http://search.cpan.org/dist/Google-Chart/
Google Chart API, Developer's Guide: http://code.google.com/apis/chart/
``Spam kartieren'', Linux-Magazin 02/2009, http://www.linux-magazin.de/heft_abo/ausgaben/2009/02/spam_kartieren
Michael Schilliarbeitet als Software-Engineer bei Yahoo! in Sunnyvale, Kalifornien. Er hat "Goto Perl 5" (deutsch) und "Perl Power" (englisch) für Addison-Wesley geschrieben und ist unter mschilli@perlmeister.com zu erreichen. Seine Homepage: http://perlmeister.com. |