Recipe11.1.Transforming an Existing Boilerplate SVG


Recipe 11.1. Transforming an Existing Boilerplate SVG

Problem

You want to display data graphically by populating a pre-existing SVG image with data.

Solution

XSLT 1.0

Imagine that you need to display data in a bar graph. You can imagine creating an XSLT transformation that constructs the SVG representation of the bar graph from scratch (see Recipe 11.2). However, for those uncomfortable with graphical manipulation, this task may seem too daunting. In some cases, though, the data you need to plot is fixed in the number of data points. In this case, you can create the SVG graphic in a drawing program as a boilerplate template that will be instantiated with actual data using XSLT. Doing so greatly simplifies the task.

Consider the following bar-graph template:

<svg width="650" height="500">      <g  transform="translate(0 500) scale(1 -1)">           <line  x1="30" y1="20" x2="30" y2="450"                 style="fill:none;stroke:rgb(0,0,0);stroke-width:2"/>           <line  x1="30" y1="20" x2="460" y2="20"                 style="fill:none;stroke:rgb(0,0,0);stroke-width:2"/>      </g>      <g  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  transform="translate(29 60)">           <text  x="0px" y="320px"                  style="text-anchor:end;fill:rgb(0,0,0);font-size:10;font-family:                 Arial">0.25</text>           <text  x="0px" y="215px"                  style="text-anchor:end;fill:rgb(0,0,0);font-size:10;font-family:                 Arial">0.50</text>           <text  x="0px" y="107.5px"                 style="text-anchor:end;fill:rgb(0,0,0);font-size:10;font-family:                 Arial">0.75</text>           <text  x="0px" y="0px" style="text-anchor:end;fill:                 rgb(0,0,0);font-size:10;font-family:Arial">1.00</text>      </g>      <g >           <rect  x="430" y="80" width="25" height="15"                 style="fill:rgb(255,0,0);stroke:rgb(0,0,0);stroke-width:1"/>           <rect  x="430" y="100" width="25" height="15"                 style="fill:rgb(0,255,0);stroke:rgb(0,0,0);stroke-width:1"/>           <rect  x="430" y="120" width="25" height="15"                 style="fill:rgb(255,255,0);stroke:rgb(0,0,0);stroke-width:1"/>           <rect  x="430" y="140" width="25" height="15"                 style="fill:rgb(0,255,255);stroke:rgb(0,0,0);stroke-width:1"/>           <rect  x="430" y="160" width="25" height="15"                 style="fill:rgb(0,0,255);stroke:rgb(0,0,0);stroke-width:1"/>           <text  x="465px" y="92px"                 style="fill:rgb(0,0,0);font-size:18;font-family:Arial">key1</text>           <text  x="465px" y="112px"                 style="fill:rgb(0,0,0);font-size:18;font-family:Arial">key2</text>           <text  x="465px" y="132px"                 style="fill:rgb(0,0,0);font-size:18;font-family:Arial">key3</text>           <text  x="465px" y="152px"                 style="fill:rgb(0,0,0);font-size:18;font-family:Arial">key4</text>           <text  x="465px" y="172px"                 style="fill:rgb(0,0,0);font-size:18;font-family:Arial">key5</text>      </g>      <g >           <text x="325px" y="20px" style="text-anchor:middle;fill:rgb(0,0,0);font- size:24;font-family:Arial">Title</text>      </g> </svg>

When rendered, the template looks like Figure 11-1.

Figure 11-1. An SVG bar-graph template


This SVG was created using the following guidelines:

First, you knew that you needed to plot five data values, so you created five bars. You placed the bars in an SVG group with . You then did something very important: you transformed the coordinate system of the bars so that the bar height represents the value you want to plot. Specifically, you used transform="translate(30 479) scale(1 -430)". translate moves the origin of the bars to the origin of the axes. TRansform flips and scales the y-axis so that the height="1" creates a bar of the maximum height. The negative value does the flipping, and 430 does the scaling (430 comes from the length of the y-axis). The scale(1,-1) value within the TRansform attribute is simply a placeholder that works for the dummy data in the SVG graph. Your stylesheet will replace it with an appropriate value when processing the actual data.

Second, you created a dummy key within a SVG group with . You made sure the elements in the key were ordered to match the bar order. Your transformation will rely on this ordering to fill in the graph correctly.

Third, you created four appropriately spaced text elements that represent the scale on the y-axis within a group with . You gave each text node an ID with a numeric suffix that represents the number of quarters its position represents. For example, the text element 0.50 has because it is at 2/4 (or 1/2) of the scale. Your stylesheet will use the number when it remaps the scale to values appropriate to the actual data.

Fourth, you created a dummy title text element, also within a group. This text is positioned and anchored to the center of the graphic. This means that it will remain centered when you replace its text with an actual title.

The fact that each major component of the SVG bar graph is in a group simplifies the stylesheet you create so it can load the graph with real data.

The data you load into the bar graph is sales data for your company's five top-selling products:

<product-sales description="Top 5 Product Sales (in $1000)">   <product name="Widget">     <sales multiple="1000" currency="USD">70</sales>   </product>   <product name="Foo Bar">     <sales multiple="1000" currency="USD">880</sales>   </product>   <product name="Grunt Master 9000">     <sales multiple="1000" currency="USD">1000</sales>   </product>   <product name="Spam Slicer">     <sales multiple="1000" currency="USD">532</sales>   </product>   <product name="Wonder Bar">     <sales multiple="1000" currency="USD">100</sales>   </product> </product-sales>

The stylesheet merges boilerplate SVG with this data file to create a plot of the actual data:

<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">     <!-- By default, copy the SVG to the output --> <xsl:import href="../util/copy.xslt"/>     <!-- We need max to find the maximum data value. --> <!-- We use the max for scaling purposes --> <xsl:include href="../math/math.max.xslt"/>     <!-- The data file names is passed as a parameter --> <xsl:param name="data-file"/>     <!--We define the output type to be an SVG file and reference the 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"/>     <!-- We load all the data values into a node set variable for -->      <!-- easy access --> <xsl:variable name="bar-values" select="document($data-file)/*/*/sales"/>     <!-- We load all the data names of each bar into a node set variable for easy access  -->      <xsl:variable name="bar-names" select="document($data-file)/*/*/@name"/>     <!--We find the max data value --> <xsl:variable name="max-data">   <xsl:call-template name="math:max">     <xsl:with-param name="nodes" select="$bar-values"/>   </xsl:call-template> </xsl:variable>     <!-- For purely aesthetic reasons, we scale the graph so the maximum value --> <!-- that can be plotted is 10% greater than the true data maximum. --> <xsl:variable name="max-bar" select="$max-data + $max-data div 10"/>     <!-- Since we gave each component of the graph a named group, --> <!-- we can easily structure the stylesheet to match each --> <!-- group and perform the appropriate transformation. -->     <!-- We copy the scale group and replace the text values with values --> <!-- that reflect the range of our data. We use the numeric part --> <!-- of each id to create the correct multiple of 0.25 -->     <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>     <!--For the key component, we simply replace the text values --> <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>     <!--We replace the title with a description extracted from the data.  --> <!--We might also have allowed the title to be passed in as a parameter--> <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>     <!-- The bars are created by --> <!-- 1) replacing the transform attribute with one that scales based  on the value of $max-bar --> <!-- 2) Loads the data value into the height of the bar --> <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>

When you apply this stylesheet to the template SVG, the result is the bar graph shown in Figure 11-2.

Figure 11-2. An SVG bar graph generated using XSLT


XSLT 2.0

Rather than repeat the entire solution for 2.0, I highlight the changes that are either required or desirable.

You can use the built-in XPath 2.0 max function instead of the custom one we wrote in Chapter 3:

<xsl:variable name="bar-values" select="document($data-file)/*/*/sales"               as="xs:double*"/> <!--We find the max data value --> <xsl:variable name="max-data" select="max($bar-values)"/>

You can't play fast and loose with types in XSLT 2.0. You must use conversion functions from strings to numbers and visa versa:

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

You can also use the more concise form of xsl:attribute. Note also the required numeric to string conversion:

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

Discussion

Creating SVG directly from data can involve a great deal of mathematical manipulation, trial and error, and frustration, especially if graphics manipulation is not your cup of tea. This recipe provides a way around the problem by using SVG that was laid out visually rather than programmatically. Furthermore, the transformation facilities of SVG were exploited to simplify data to graphical mapping.

Of course, this recipe has some obvious limitations.

First, you must know beforehand the number of data points you need to plot. A more general bar-graph builder would compute and distribute the bars automatically based on runtime inspection of the data. You can partially get around this problem by creating an SVG template with, say, ten bars, and remove the ones you don't need at runtime. This workaround would detract from the presentation's aesthetics because obvious gaps would emerge if only two data elements were mapped into a template originally laid out for ten.

Second, although you can create graphics that are more sophisticated than this simple example, the technique is limited to linear mappings of magnitude to graphical display. For example, it is not obvious that you could easily use this approach with a pie chart; changing the area of a pie slice is not simply a matter of adjusting the value of a single attribute.

A third limitation of this approach stems from the fact that the data source, SVG template, and stylesheet tend to be coupled. Thus, each time you use this technique, you create a new SVG template and stylesheet. Over time, this situation can result in more effort than if you simply built a more generic solution in the first place.

Despite its limitations, this example is valuable because it provides a good starting point for someone just learning to exploit SVG graphics. Specifically, it lets you rely on the facilities of the SVG editor for aligning, distributing, and proportioning the graphics.




XSLT Cookbook
XSLT Cookbook: Solutions and Examples for XML and XSLT Developers, 2nd Edition
ISBN: 0596009747
EAN: 2147483647
Year: 2003
Pages: 208
Authors: Sal Mangano

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net