Mini-Me (Linux-Magazin, Juli 2009)

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

Vom Zettel zum Diagramm

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

Diagramm outgesourced

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.

Objekttechnik statt URL-Dschungel

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.

Zeitachsen schieben

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.

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 }

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.

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

Typ XY

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.

Professioneller Schnickschnack

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.

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 

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

Installation

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!

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 02/2009, http://www.linux-magazin.de/heft_abo/ausgaben/2009/02/spam_kartieren

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. Seine Homepage: http://perlmeister.com.