Vorgefertigtes SVG transformieren

(Auszug aus "XSLT Kochbuch" von Sal Mangano)

Problem

Sie möchten Daten grafisch anzeigen, indem Sie ein vorhandenes SVG-Bild mit Daten füllen.

Lösung

XSLT 1.0

Stellen Sie sich vor, Sie müssten Daten in einem Balkendiagramm darstellen. Sie können sich ausmalen, wie Sie eine XSLT-Transformation erstellen, die eine SVG-Darstellung des Balkendiagramms von Grund auf produziert (siehe das Rezept Wiederverwendbare SVG-Hilfswerkzeuge für Diagramme erzeugen). Für all jene, die mit grafischen Manipulationen aber nur wenig vertraut sind, dürfte diese Aufgabe eine erhebliche Herausforderung bedeuten. In manchen Fällen liegen die zu plottenden Daten jedoch in einer festen Anzahl von Datenpunkten vor. Dann können Sie die SVG-Grafik mit einem Zeichenprogramm als vorgefertigtes Template erzeugen, das dann mittels XSLT mit den eigentlichen Daten instanziiert wird. Damit vereinfacht sich die Aufgabe ganz gewaltig.

Betrachten Sie folgendes Template für ein Balkendiagramm:

<svg width="650" height="500">
  <g id="axis" transform="translate(0 500) scale(1 −1)">
    <line id="axis-y" x1="30" y1="20" x2="30" y2="450" style="fill:none;stroke:rgb(0,0,0);stroke-width:2"/>
    <line id="axis-x" x1="30" y1="20" x2="460" y2="20" style="fill:none;stroke:rgb(0,0,0);stroke-width:2"/>
  </g>
  <g id="bars" transform="translate(30 479) scale(1 −430)">
    <rect x="30" y="0" width="50" height="0.25" style="fill:rgb(255,0,0);stroke:rgb(0,0,0);stroke-width:0"/>
    <rect x="100" y="0" width="50" height="0.5" style="fill:rgb(0,255,0);stroke:rgb(0,0,0);stroke-width:0"/>
    <rect x="170" y="0" width="50" height="0.75" style="fill:rgb(255,255,0);stroke:rgb(0,0,0);stroke-width:0"/>
    <rect x="240" y="0" width="50" height="0.9" style="fill:rgb(0,255,255);stroke:rgb(0,0,0);stroke-width:0"/>
    <rect x="310" y="0" width="50" height="1" style="fill:rgb(0,0,255);stroke:rgb(0,0,0);stroke-width:0"/>
  </g>
  <g id="scale" transform="translate(29 60)">
    <text id="scale1" x="0px" y="320px" style="text-anchor:end;fill:rgb(0,0,0);font-size:10;font-family:Arial">0.25</text>
    <text id="scale2" x="0px" y="215px" style="text-anchor:end;fill:rgb(0,0,0);font-size:10;font-family:Arial">0.50</text>
    <text id="scale3" x="0px" y="107.5px" style="text-anchor:end;fill:rgb(0,0,0);font-size:10;font-family:Arial">0.75</text>
    <text id="scale4" x="0px" y="0px" style="text-anchor:end;fill:rgb(0,0,0);font-size:10;font-family:Arial">1.00</text>
  </g>
  <g id="key">
    <rect id="key1" x="430" y="80" width="25" height="15" style="fill:rgb(255,0,0);stroke:rgb(0,0,0);stroke-width:1"/>
    <rect id="key2" x="430" y="100" width="25" height="15" style="fill:rgb(0,255,0);stroke:rgb(0,0,0);stroke-width:1"/>
    <rect id="key3" x="430" y="120" width="25" height="15" style="fill:rgb(255,255,0);stroke:rgb(0,0,0);stroke-width:1"/>
    <rect id="key5" x="430" y="140" width="25" height="15" style="fill:rgb(0,255,255);stroke:rgb(0,0,0);stroke-width:1"/>
    <rect id="key4" x="430" y="160" width="25" height="15" style="fill:rgb(0,0,255);stroke:rgb(0,0,0);stroke-width:1"/>
    <text id="key1-text" x="465px" y="92px" style="fill:rgb(0,0,0);font-size:18;font-family:Arial">schlüssel1</text>
    <text id="key2-text" x="465px" y="112px" style="fill:rgb(0,0,0);font-size:18;font-family:Arial">schlüssel2</text>
    <text id="key3-text" x="465px" y="132px" style="fill:rgb(0,0,0);font-size:18;font-family:Arial">schlüssel3</text>
    <text id="key4-text" x="465px" y="152px" style="fill:rgb(0,0,0);font-size:18;font-family:Arial">schlüssel4</text>
    <text id="key5-text" x="465px" y="172px" style="fill:rgb(0,0,0);font-size:18;font-family:Arial">schlüssel5</text>
  </g>
  <g id="title">
    <text x="325px" y="20px" style="text-anchor:middle;fill:rgb(0,0,0);font-size:24;font-family:Arial">Überschrift</text>
  </g>
</svg>

Wenn es gerendert wird, dann sieht das Template so aus wie in der folgenden Abbildung dargestellt.

Template für ein SVG-Balkendiagramm

Abbildung: Ein Template für ein SVG-Balkendiagramm.

Folgende Rahmenbedingungen wurden bei der Erstellung dieses SVG-Codes beachtet:

Als Erstes wussten Sie, dass Sie fünf Datenwerte plotten müssen, also haben Sie fünf Balken erzeugt. Diese Balken haben Sie in eine SVG-Gruppe mit id="bars" gesetzt. Und dann haben Sie etwas sehr Wichtiges gemacht: Sie haben das Koordinatensystem der Balken so transformiert, dass die Balkenhöhe dem Wert entspricht, den Sie plotten möchten. Vor allem haben Sie dabei transform="translate(30 479) scale(1−430)" benutzt. Das translate verschiebt den Ursprung der Balken auf den Achsenursprung, und transform spiegelt und skaliert die Y-Achse derart, dass die Höhe height="1" einen Balken maximaler Höhe erzeugt. Dabei besorgt der negative Wert die Spiegelung, und die 430 bewirkt die Skalierung (430 ist die Länge der Y-Achse). Der Wert scale(1,-1) im transform-Attribut ist einfach nur ein Platzhalter, der auch mit den Pseudodaten im SVG-Diagramm funktioniert. Ihr Stylesheet wird ihn durch einen geeigneten Wert ersetzen, sobald es die echten Daten verarbeitet.

Zweitens haben Sie mit id="key" einen Pseudoschlüssel innerhalb einer SVG-Gruppe erzeugt. Sie haben sichergestellt, dass die Elemente im Schlüssel mit der Reihenfolge der Balken übereinstimmend angeordnet waren. Auf diese Reihenfolge verlässt sich Ihre Transformation, um das Diagramm korrekt mit Inhalt zu füllen.

Drittens haben Sie vier Textelemente passend platziert, die die Skala auf der Y-Achse innerhalb einer Gruppe mit id="scale" darstellen. Jedem Textknoten haben Sie eine ID mit einer numerischen Erweiterung mitgegeben, die dem Zahlenviertel entspricht, dessen Position sie darstellt. So verwendet z.B. das Textelement 0.50 id="scale2", weil es 2/4 (bzw. 1/2) der Skala einnimmt. Ihr Stylesheet wird diese Zahl benutzen, wenn es die Skala wieder auf solche Werte abbildet, die zu den echten Daten passen.

Viertens haben Sie ein Textelement für eine Pseudo-Überschrift, ebenfalls in einer Gruppe, angelegt. Dieser Text wird im Zentrum der Grafik positioniert und verankert. Das bedeutet, er bleibt auch dann zentriert, wenn Sie seinen Text durch eine echte Überschrift ersetzen.

Die Tatsache, dass alle wesentlichen Komponenten des SVG-Balkendiagramms in einer Gruppe sind, vereinfacht das Stylesheet, das Sie erstellen, sodass es den Graphen mit echten Daten laden kann.

Die Daten, die Sie im Balkendiagramm einfügen, sind Verkaufszahlen für die fünf meistverkauften Produkte Ihrer Firma:

<product-sales description="Fünf meistverkauften Produke (in i1000)">
  <product name="Sockenwalter">
    <sales multiple="1000" currency="USD">70</sales>
  </product>
  <product name="Hosenjäger">
    <sales multiple="1000" currency="USD">880</sales>
  </product>
  <product name="Wunderhüte">
    <sales multiple="1000" currency="USD">1000</sales>
  </product>
  <product name="Schürzenträger">
    <sales multiple="1000" currency="USD">532</sales>
  </product>
  <product name="Nasenchlorer">
    <sales multiple="1000" currency="USD">100</sales>
  </product>
</product-sales>

Das Stylesheet kombiniert vorgefertigtes SVG mit den Daten in dieser Datei, um daraus einen Plot der echten Daten zu erstellen:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:math="http://www.exslt.org/math" exclude-result-prefixes="math">
  <!-- Kopiere standardmäßig das SVG auf die Ausgabe -->
  <xsl:import href="../util/copy.xslt"/>
  <!-- Wir brauchen max, um den maximalen Datenwert zu finden. Wir benutzen max für die Skalierung. -->
  <xsl:include href="../math/math.max.xslt"/>
  <!-- Der Dateiname wird als Parameter übergeben -->
  <xsl:param name="data-file"/>
  <!-- Wir definieren den Ausgabetyp als SVG-Datei und referenzieren die SVG-DTD. -->
  <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes" doctype-public="-//W3C//DTD SVG 1.0/EN" doctype-system="http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd"/>
  <!-- Wir laden alle Datenwerte in eine Knotenmengenvariable, um leichter darauf zugreifen zu können. -->
  <xsl:variable name="bar-values" select="document($data-file)/*/*/sales"/>
  <!-- Wir laden die Datennamen aller Balken in eine Knotenmengenvariable, um leichter darauf zugreifen zu können. -->
  <xsl:variable name="bar-names" select="document($data-file)/*/*/@name"/>
  <!-- Wir finden den maximalen Datenwert -->
  <xsl:variable name="max-data">
    <xsl:call-template name="math:max">
      <xsl:with-param name="nodes" select="$bar-values"/>
    </xsl:call-template>
  </xsl:variable>
  <!-- Aus rein ästhetischen Gründen skalieren wir das Diagramm, damit der maximal darstellbare Wert 10% größer ist als das echte Datenmaximum. -->
  <xsl:variable name="max-bar" select="$max-data + $max-data div 10"/>
  <!-- Da alle Komponenten des Diagramms in einer benannten Gruppe sind, können wir das Stylesheet leicht so strukturieren, dass es alle Gruppen findet und die passende Transformation anwendet. -->
  <!-- Wir kopieren die scale-Gruppe und ersetzen die Textwerte durch Werte, die den Wertebereich unserer Daten spiegeln. Wir nehmen den numerischen Teil einer id, um das richtige Viel-fache von 0.25 zu erzeugen. -->
  <xsl:template match="g[@id='scale']">
    <xsl:copy>
      <xsl:copy-of select="@*"/>
      <xsl:for-each select="text">
        <xsl:copy>
          <xsl:copy-of select="@*"/>
          <xsl:variable name="factor" select="substring-after(@id,'scale') * 0.25"/>
          <xsl:value-of select="$factor * $max-bar"/>
        </xsl:copy>
      </xsl:for-each>
    </xsl:copy>
  </xsl:template>
  <!-- Bei allen key-Komponenten ersetzen wir einfach den Textwert -->
  <xsl:template match="g[@id='key']">
    <xsl:copy>
      <xsl:copy-of select="@*"/>
      <xsl:apply-templates select="rect"/>
      <xsl:for-each select="text">
        <xsl:variable name="pos" select="position( )"/>
        <xsl:copy>
          <xsl:copy-of select="@*"/>
          <xsl:value-of select="$bar-names[$pos]"/>
        </xsl:copy>
      </xsl:for-each>
    </xsl:copy>
  </xsl:template>
  <!-- Wir ersetzen die Überschrift durch eine Beschreibung, die aus den Daten extrahiert wird. Diese Überschrift hätten wir auch als Parameter übergeben können. -->
  <xsl:template match="g[@id='title']">
    <xsl:copy>
      <xsl:copy-of select="@*"/>
      <xsl:for-each select="text">
        <xsl:copy>
          <xsl:copy-of select="@*"/>
          <xsl:value-of select="document($data-file)/*/@description"/>
        </xsl:copy>
      </xsl:for-each>
    </xsl:copy>
  </xsl:template>
  <!-- Die Balken werden erzeugt, indem 1) das transform-Attribut durch eines ersetzt wird, das abhängig vom Wert $max-bar skaliert, 2) der Datenwert als Höhe des Balkens gesetzt wird. -->
  <xsl:template match="g[@id='bars']">
    <xsl:copy>
      <xsl:copy-of select="@id"/>
      <xsl:attribute name="transform">
        <xsl:value-of select="concat('translate(60 479) scale(1 ', −430 div $max-bar,')')"/>
      </xsl:attribute>
      <xsl:for-each select="rect">
        <xsl:variable name="pos" select="position( )"/>
        <xsl:copy>
          <xsl:copy-of select="@*"/>
          <xsl:attribute name="height">
            <xsl:value-of select="$bar-values[$pos]"/>
          </xsl:attribute>
        </xsl:copy>
      </xsl:for-each>
    </xsl:copy>
  </xsl:template>
</xsl:stylesheet>

Wenn Sie dieses Stylesheet auf das SVG-Template anwenden, erhalten Sie als Ergebnis das Balkendiagramm, das in der folgenden Abbildung dargestellt ist.

Ein mit XSLT erzeugtes SVG-Balkendiagramm

Abbildung: Ein mit XSLT erzeugtes SVG-Balkendiagramm.

XSLT 2.0

Anstatt die gesamte Lösung für 2.0 zu wiederholen, werde ich hier nur die Änderungen hervorheben, die für diese Version notwendig bzw. wünschenswert sind.

Sie können die eingebaute XPath 2.0-Funktion max an Stelle derjenigen verwenden, die wir unter Zahlen und Berechnungen geschrieben haben:

<xsl:variable name="bar-values" select="document($data-file)/*/*/sales" as="xs:double*"/>
  <!-- Wir bestimmen den maximalen Datenwert -->
<xsl:variable name="max-data" select="max($bar-values)"/>

Aber in XSLT 2.0 können Sie kein Schindluder mit Typen treiben. Deswegen müssen Sie Konvertierungsfunktionen von Strings nach Zahlen und umgekehrt benutzen:

<xsl:template match="g[@id='scale']">
  <xsl:copy>
    <xsl:copy-of select="@*"/>
    <xsl:for-each select="text">
      <xsl:copy>
        <xsl:copy-of select="@*"/>
        <xsl:variable name="factor" select="number(substring-after(@id,'scale')) * 0.25"/>
        <xsl:value-of select="$factor * $max-bar"/>
      </xsl:copy>
    </xsl:for-each>
  </xsl:copy>
</xsl:template>

Wenn Sie wollen, können Sie auch die knappere Variante mit xsl:attribute verwenden. Beachten Sie dabei die notwendige Umwandlung einer Zahl in einen String:

<xsl:attribute name="transform" select="concat('translate(60 479) scale(1 ', string(-430 div $max-bar),')')"/>

Diskussion

Beim direkten Erzeugen von SVG aus Daten kommen schnell eine Menge mathematischer Operationen, viele Situationen der Art Versuch und Irrtum sowie eine allgemeine Frustration zusammen, insbesondere, wenn die Manipulation von Grafiken nicht Ihr Ding ist. Dieses Rezept bietet eine Möglichkeit, das Problem zu umgehen, indem es SVG verwendet, das zuvor manuell mit einem Editor und nicht mit einem Programm erzeugt wurde. Außerdem wurden die Transformationsmöglichkeiten von SVG genutzt, um die Abbildung der Daten in eine grafische Form zu vereinfachen.

Natürlich hat dieses Rezept einige offensichtliche Grenzen.

Zuallererst müssen Sie die Anzahl der zu plottenden Datenwerte im Voraus kennen. Ein allgemeineres Programm für Balkendiagramme würde die Balken automatisch berechnen und im Diagramm verteilen, nachdem es sich zur Laufzeit zuerst die Daten angeschaut hat. Dieses Problem können Sie teilweise dadurch beseitigen, indem Sie ein SVG-Template mit, sagen wir, zehn Balken erstellen und zur Laufzeit jene entfernen, die Sie nicht benötigen. Bei einer solchen Ersatzlösung müssten Sie aber Abstriche bei der Ästetik der Darstellung in Kauf nehmen, weil es zu Lücken kommen würde, wenn z.B. nur zwei Datenelemente auf ein Template mit einem Layout für zehn abgebildet würden.

Zweitens, auch wenn Sie eine ausgefeiltere Grafik als in diesem einfachen Beispiel erzeugen können, bleibt diese Technik weiterhin auf lineare Abbildungen von Größen auf eine grafische Anzeige beschränkt. So ist z.B. völlig unklar, ob Sie den gleichen Ansatz leicht auf Tortendiagramme übertragen könnten. Um die Fläche eines Tortenstücks zu verändern, muss schließlich mehr als der Wert eines einzigen Attributes verändert werden.

Eine dritte Einschränkung bei diesem Ansatz rührt daher, dass die Datenquelle, das SVG-Template und das Stylesheet miteinander gekoppelt sind. Das heißt, jedes Mal, wenn Sie diese Methode anwenden, erzeugen Sie ein neues SVG-Template und ein neues Stylesheet. Mit der Zeit kann diese Situation zu einem größeren Aufwand führen als wenn Sie einfach von vornherein eine allgemeinere Lösung erstellen würden.

Trotz dieser Einschränkungen hat dieses Beispiel seinen Wert, denn es bietet eine gute Ausgangsbasis für jemanden, der noch einiges über SVG-Grafik zu lernen hat. Insbesondere können Sie sich dabei auf die Möglichkeiten eines SVG-Editors bei der Ausrichtung, Platzierung und Proportionierung der Grafik verlassen.

  

zum Seitenanfang

<< zurück vor >>

 

 

 

Tipp der data2type-Redaktion:
Zum Thema XSLT bieten wir auch folgende Schulungen zur Vertiefung und professionellen Fortbildung an:

Copyright © 2006 O'Reilly Verlag GmbH & Co. KG
Für Ihren privaten Gebrauch dürfen Sie die Online-Version ausdrucken.
Ansonsten unterliegt dieses Kapitel aus dem Buch "XSLT Kochbuch" denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.

O'Reilly Verlag GmbH & Co. KG, Balthasarstraße 81, 50670 Köln, kommentar(at)oreilly.de