Aus Linux-Magazin 07/2009

Perl-Skript lässt Google Diagramme zeichnen

© Utzel-Butzel, Photocase.com

Googles Chart-Service zeichnet optisch ansprechende Diagramme, ganz gleich, woher die Daten dafür stammen. Ein CPAN-Modul gibt die Zeichenanweisungen dafür in objektorientiertem Perl weiter, statt vom Programmierer URL-Fitzelei zu verlangen .

Schon geraume Zeit liebäugelte ich heimlich mit einem Ersatz für meinen alten Laptop und als dann neulich die Firma Dell eines dieser putzigen Mini-9-Zoll-Ubuntu-Netbooks (Abbildung 1) zum unschlagbaren Preis von nur 230 Dollar feilbot, gab es kein Halten mehr und ich schlug endlich zu. Mein Arbeitskollege Leif fand dann auch noch einen lustigen Namen für den Winzling: “Mini-Me” – nach dem gleichnamigen winzigen Klon von Dr. Evil aus dem zweiten Austin-Powers-Film.

Abbildung 1: Michael Schillis kleines Dell-Netbook mit Ubuntu und ursprünglich nur 512 MByte RAM.

Abbildung 1: Michael Schillis kleines Dell-Netbook mit Ubuntu und ursprünglich nur 512 MByte RAM.

Der erste Eindruck war berauschend, es funktionierte sogar alles halbwegs! Als ich dann noch die mageren 512 MByte RAM mit 2 GByte eines Billiganbieters für 9,95 Dollar nachrüstete, war mein Glück perfekt, allerdings schlich sich ein verstörender Gedanke ein: Würde das Netbook mit dem größeren Speicherbaustein im Suspend-Mode vielleicht mehr Strom schlucken, also die Batterie vorzeitig entleeren? Dem Ingenieur ist nichts zu schwör – ich ging der Sache auf den Grund.

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 Ladezustand der Batterie ab. In Abbildung 2 sind die handgeschriebenen Notizzettel zu sehen, die das aktuelle Ergebnis jeweils neben der Uhrzeit auflisten.

Abbildung 2: Die gemessenen Daten auf handgeschriebenen Notizzetteln.

Abbildung 2: Die gemessenen Daten auf handgeschriebenen Notizzetteln.

Vom Zettel zum Diagramm

Das gleiche Verfahren wiederholte ich eineinhalb Tage später noch einmal mit dem dazu wieder eingebauten 2-GByte-Baustein. Beide Messreihen haben wegen des unorthodoxen Verfahrens unterschiedlich Messzeitpunkte. Um die Entladungskurven – wie in Abbildung 3 zu sehen – grafisch nebeneinander darzustellen, muss man die Daten erst mit dem Skript in Listing 1 normalisieren, bevor sie sich anschließend mit dem Skript in Listing 2 durch Googles Chart-Service zeichnen lassen.

Das Ergebnis zeigt, dass die Entladung mit beiden Speichermodulen anfangs in etwa gleich schnell vonstatten geht. Mit schwächerer Batterie saugt das größere Speichermodul später sogar etwas mehr Saft 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 könnte.

Listing 1:
»data-normalize«

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 }

Listing 2:
»graph-discharge«

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

Diagrammgestaltung outsourcen

Kein Programm auf dem lokalen Rechner erzeugt in diesem Fall das Diagramm, sondern ein Clusterrechner der Firma Google. Das Perl-Skript baut lediglich eine URL nach Abbildung 4 zusammen, schickt sie an den Google-Chart-Service, und zurück kommt ein Bild im PNG-Format, das der Abbildung 3 entspricht. Google beschränkt die Zugriffe auf maximal 50000 pro Tag, das sollte aber für private Spielereien fürs Erste noch reichen. Ein anderer Perl-Snapshot [5] benutzte den gleichen Service schon einmal, um Spammer auf einer Weltkarte einzuzeichnen.

Abbildung 3: Google Charts zeichnet die Entladung des Netbooks mit den verschieden großen Speicherbausteinen.

Abbildung 3: Google Charts zeichnet die Entladung des Netbooks mit den verschieden großen Speicherbausteinen.

Abbildung 4: Die an Google geschickte URL, nach deren Angaben der Chart-Service das Diagramm in Abbildung 3 zeichnet. Manuell wäre sie nur mit großem Aufwand zu konstruieren.

Abbildung 4: Die an Google geschickte URL, nach deren Angaben der Chart-Service das Diagramm in Abbildung 3 zeichnet. Manuell wäre sie nur mit großem Aufwand zu konstruieren.

Objekttechnik statt URL-Dschungel

Um allerdings von den Anforderungen eines Diagramms wie in Abbildung 3 auf die URL in Abbildung 4 zu kommen, muss der Diagrammkonstrukteur zunächst die Bedienungsanleitung auf [4] genau studieren und die verschiedenen Regeln und Kürzel verinnerlichen. Einfacher geht das mit dem CPAN-Modul Google::Chart, das eine objektorientierte Schnittstelle zur Diagramm-Definition anbietet und die URL Schritt für Schritt mittels leicht verständlicher Methodenaufrufe zusammensetzt. Doch vor der eigentlichen Diagrammdefinition steht erst noch die nötige Konsolidierung der Messdaten an.

Der Batterietester maß in unregelmäßigen Zeitabständen 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 1 die Messzeitpunkte auf eine gemeinsame Zeitachse. Beide Messreihen starten dann zu einer virtuellen Stunde null. Das Skript rechnet die absoluten Messzeitpunkte in relative Zeitpunkte um.

Auf Zeitachse verschoben

Enthält die erste Messreihe also zum Beispiel Messpunkte zu den Zeitpunkten 8:00 und 9:00 Uhr, während die zweite Reihe um 11:00 und 11:30 Uhr gemessene Werte enthält, startet die virtuelle gemeinsame Zeitachse zum Beispiel um 0:00 Uhr und zeigt um 0:30 Uhr den Messpunkt der Messreihe 2 und um 1:00 Uhr den Messpunkt der Messreihe 1.

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 ein Array von Messpunkten mit Wertepaaren aus Zeitpunkt und Batteriekapazität enthält.

Normen von 2 bis 98

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 so.

Das Skript führt die Zeitrechnung mit Hilfe des CPAN-Moduls »DateTime« durch und legt den Zeitpunkt der ersten (zufällig gewählten) Messreihe als Anfang 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 Tagesdatum der letzten Messung.

Stellt das Skript fest, dass zwischen zwei Messdaten ein ja nicht explizit angegebener Tagessprung liegt (zum Beispiel wenn es nach einer Messung um 23:00 Uhr eine um 8:00 Uhr gibt), addieren die Zeilen 44 und 45 einen Datumstag zu den Zählern.

Die Anzahl der Minuten seit der letzten Messung bestimmt die Zeile 50. Sie speichert diesen Wert in der Variablen »$x« (für x-Achsen-Werte) ab, von wo er in Zeile 53 zusammen mit dem Namen der Messreihe und dem aktuellen Ladezustands-Messwert in das Ergebnis-Array »@results« wandert.

Die Zeilen 55 bis 62 halten die bis zu diesem Zeitpunkt höchsten x- und y-Werte fest und legen sie in den Hash-Einträgen »$max->{x}« und »$max->{y}« ab. Diese Werte 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, letztlich 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 heranreichen. Das Ergebnis schickt »print« in Zeile 70 nach Stdout, und die Ausgabe sieht so aus, wie es Abbildung 5 zeigt.

Abbildung 5: Die zwei Messreihen mit unterschiedlichen Zeitachsen rechnet das Skript »data-normalize« auf eine gemeinsame virtuelle Zeitachse um.

Abbildung 5: Die zwei Messreihen mit unterschiedlichen Zeitachsen rechnet das Skript »data-normalize« auf eine gemeinsame virtuelle Zeitachse um.

Auf der Suche nach einem passenden Diagrammformat findet sich auf [4] der Typ “lxy”, der für jede gezeichnete Linie je einen Satz x- und y-Koordinaten entgegennimmt. 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 Listing 2 legt deshalb »XY« für die Option »type« im Konstruktoraufruf für ein neues Objekt vom Typ Google::Chart fest.

Format XY

Listing 2 liest nun in Zeile 8 zunächst 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 512 MByte, 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-Objekts in Zeile 21 eine Referenz auf einen Array von x/y-Datensätzen so zu übergeben, wie der Charttyp XY dies wünscht.

Professioneller Schnickschnack

Der Parameter »size« setzt die Ausmaße des Diagramms, 700 mal 400 Pixel ist ziemlich das Maximum, bei größeren Dimensionen schickt Google einen Bad Request zurück. 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 Colors-Rubrik nachzulesen, verlangt dieses Verfahren den Wert »c« für das Füllen des Chartbereichs 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.

Das Stützliniengitter im Diagramm zeichnet die Option »grid«, die den dargestellten Zeitraum von etwa 36 Stunden in x-Richtung in drei Teile à 33 von 100 Normpunkten teilt. In y-Richtung markieren waagerechte 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 Linienfarben der beiden 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 darstellt. Damit der Betrachter die beiden Linien auch den Datensätzen zuordnen kann, bestimmt »legend« in Zeile 61, dass am rechten Rand des Diagramms zusammen mit einem zur Linie passenden Farbquadrat der Name der zugehörigen Messreihe erscheint.

Luft lassen

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. Die letzten beiden Parameter 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 fest.

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« bestimmen die Kreuzchen des ersten und des zweiten Datensatzes, den sie jeweils mit »dataset => 0« und »dataset => 1« festlegen. Ist »datapoint« auf -1 gesetzt, zeichnet der Chartservice jeden Messpunkt ein, der Benutzer kann aber auch eine Auswahl festlegen. Marker mit der Priorität 1 zeichnet »graph-discharge« zuletzt und stellt so sicher, dass die Linien am Ende nicht etwa die kleinen Kreuzchen verdecken.

Entsprechen die gezeichneten Diagrammlinien nicht den ästhetischen Bedürfnissen des Betrachters, der vielleicht dickere Linien wünscht, geht auch dies (etwas kontra-intuitiv) mit »marker«. Statt eines Kreuzchens mit »x« als »marker_type« bestimmt ein Eintrag mit dem Typ »D« Linieneigenschaften wie Dicke oder Farbe. Wer also die Diagrammlinien einfach verdicken möchte, legt 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 die URL nach Abbildung 4 zusammen, schickt sie an Google, und nach etwa einer Sekunde kommt das fertige Bild zurück, das die Methode gleich unter dem angegebenen Dateinamen auf der Festplatte im PNG- Format ablegt.

Das Modul Google::Chart ist beim CPAN erhältlich und benötigt das postmoderne Objektsystem »Moose«, das einen ganzen Rattenschwanz an Abhängigkeiten hinterherschleift, also ist eine CPAN-Shell oder der Package-Manager der Distribution hilfreich.

Github sei Dank

Zum Zeitpunkt der Fertigstellung dieses Artikels fehlten Google::Chart allerdings noch einige Funktionen. Aber nach kurzer Konversation mit den Entwicklern übernahmen sie freundlicherweise schnell meine Patches auf der Kollaborationsseite Github.com und spielten sie in ihre Entwicklerversion 0.05014_01 ein, die unter [3] neben der stabilen Release zum Download bereit steht. (jcb)

Infos

[1] Listings zu diesem Artikel: [ftp://www.linux-magazin.de/pub/listings/magazin/2009/07/Perl]

[2] Github-Projekt des Moduls Google::Chart: [http://github.com/lestrrat/google-chart/tree/master]

[3] CPAN-Version des Moduls Google::Chart (einschließlich Testversion 0.05014_01): [http://search.cpan.org/dist/Google-Chart]

[4] Google-Chart-API, Developer’s Guide: [http://code.google.com/apis/chart]

[5] “Spam kartieren”: Linux-Magazin 2/09, [https://www.linux-magazin.de/heft_abo/ausgaben/2009/02/spam_kartieren]

Der Autor


Michael Schilli arbeitet 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.

LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Inline Feedbacks
Alle Kommentare anzeigen
Nach oben