Digital humanities


Maintained by: David J. Birnbaum (djbpitt@gmail.com) [Creative Commons BY-NC-SA 3.0 Unported License] Last modified: 2017-03-21T20:56:36+0000


Using XSLT to generate SVG

Overview

Your first SVG homework assignment was to use the data from a hypothetical best stooge ever contest to draw (by hand) an SVG bar chart of the results. In XML terms, they were:

<results>
  <stooge name="Curly">50</stooge>
  <stooge name="Larry">35</stooge>
  <stooge name="Moe">15</stooge>
</results>

For your bar chart, we asked you to draw an x axis and y axis (using the SVG <line> element), three bars of different colors (using the SVG <rect> element), and labels on the axes or the bars for the stooges and the numerical values of their vote counts (using the SVG <text> element).

Using XSLT to generate SVG

Drawing the SVG by hand is inefficient, error-prone, and fragile, and in the Real World we would solve a problem like this by using XSLT to generate the SVG. Once we’ve fine-tuned and debugged our XSLT, we can be confident that the data in the SVG will always match the data in the input XML, and that should the XML change, we’ll be able to just rerun the XSLT transformation to bring the SVG in sync with the new information. New scores? More or different stooges? Not a problem with a dynamic, XSLT-driven system.

The XSLT that we produced in class (with a few corrections and enhancements; see below) looked as follows:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0">
  <xsl:output method="xml" indent="yes"/>
  <xsl:template match="/">
    <svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%">
      <xsl:variable name="maxHeight" select="max(//stooge)"/>
      <xsl:variable name="colors" select="('red','blue','green')"/>
      <g transform="translate(0,{$maxHeight + 20})">
        <line x1="0" y1="0" x2="150" y2="0" stroke="purple" stroke-width="2"/>
        <line x1="0" y1="-150" x2="0" y2="0" stroke="purple" stroke-width="2"/>
        <xsl:for-each select="//stooge">
          <xsl:sort select="@name" order="descending"/>
          <xsl:variable name="xPosition" select="(position() - 1) * 40"/>
          <xsl:variable name="position" select="position()"/>
          <rect x="{$xPosition}" y="-{.}" height="{.}" width="35" 
            fill="{$colors[$position]}"/>
          <text x="{$xPosition}" y="15">
            <xsl:value-of select="@name"/>
          </text>
          <text x="{$xPosition}" y="-{. + 5}">
            <xsl:value-of select="."/>
          </text>
        </xsl:for-each>
      </g>
    </svg>
  </xsl:template>
</xsl:stylesheet>

Here are the details:

<xsl:variable name="maxHeight" select="max(//stooge)"/>
Instead of hard-coding the extent to which we’re going to shift the image downward, as we did in class, we calculate the maximum height of a bar by taking the maximim stooge value. We’ll use that to determine the y part of the translate(x,y) attribute value. Since our three stooges (see the XML, above) have values of 15, 35, and 50, max(//stooge) will give us 50.
<xsl:variable name="colors" select="('red','blue','green')"/>
In class we made all of the rectangles the same color. This time we’ll create a sequence of three color values and use a different one for each stooge. This bit of coding will break if we add more stooges without also adding more colors, and in the Real World we might want to adopt a more robust strategy, one that, for example, would restart the color cycle upon running out, instead of just coming up empty-handed. To keep the example simple, we’re not doing that here.
<g transform="translate(0,{$maxHeight + 20})">
In class we used the @transform attribute to shift the drawing down from the upper right quadrant (which isn’t rendered on the screen) to the lower right (which is), and we hard-coded how far down to shift it. In this enhanced version we use the $maxHeight variable that determines the height of the tallest bar and add a bit to it. This means that should the values of the voting change, the amount of shifting will also change, since the extent of the shift is calculated dynamically on the basis of the largest vote (= tallest bar).
<xsl:variable name="xPosition" select="(position() - 1) * 40"/>
Once we’ve set up our general framework and drawn the x and y axes, we enter an <xsl:for-each> loop and iterate over our stooges. For each stooge, we calculate how to position the rectange that we use for the bar that will represent the vote. Since we want the bars next to one another, we take the position of the stooge in the sequence of 3, subtract 1, and multiply that value by 40. This means that the x coordinate for the starting point (upper left corner) of the first stooge will be 0, the next will be 40, and the third will be 80. Because XML-related technologies start counting from 1 (some programming languages count from 0 instead), we have to subtract 1 from the value of position() to ensure that our first rectangle starts at the 0 position on the x axis, and not at 40.
<xsl:variable name="position" select="position()"/>
We didn’t do this in class, but it’s what lets us choose different colors easily. We’re going to assign each stooge a color based on position, so that the first stooge will get the value of the first of the three colors in the <xsl:variable name="colors" select="('red','blue','green')"/> we set up earlier, the second stooge will get the second value, etc. For somewhat complicated XPathy reasons it can be difficult to use position() directly in a predicate when we choose the color below (see the example), so we assign it to a variable now and then use the variable instead.
<rect x="{$xPosition}" y="-{.}" height="{.}" width="35" fill="{$colors[$position]}"/>

The @x and @y attributes of a <rect> element determine the upper left corner of the rectangle (remember that in the SVG coordinate space, counting begins from the origin and moves left and down, so the upper left corner represents the lowest values for both). We draw the rectangles with the left edge at 0, 40, and 80, values that we calculated above and assigned to the variable $xPosition. The textual value of the <stooge> element in the XML is the height of the bar, so we use that as the value of the @height attribute. We stick a minus sign in front of that value to determine the @y attribute because we’re drawing in the upper right quadrant, where the y values are negative. This means that when a stooge gets, say, 20% of the vote, the bar will start at a y value of -20 and be of height 20, so that it will end at the x axis, where y = 0.

We set the @width value to 35. Since we’re spacing the rectangles 40 units apart, this means that there will be 35 units of color for each one (its width) and then 5 units of white space to separate the bars. This makes the chart easier to read.

We determine the color of the rectangle by using a numerical predicate to index into our list of colors. The variable $colors, which we set up earlier, holds a sequence of 3 strings, each of which is a color term. The variable $position, which we determine for each stooge early inside the loop, tells us whether we’re dealing with stooge #1, #2, or #3. By using the value of $position as a numerical predicate, we select a different color from the sequence for each stooge (we get the first color for stooge #1, the second for stooge #2, etc.).

<text x="{$xPosition}" y="15"><xsl:value-of select="@name"/></text>
We’re looping through our stooges, drawing a bar for each, and we label each bar with the name of the stooge, which we retrieve from the @name attribute of the stooge we are currently looking at. The left edge of that text is the same as the left edge of the bar. Since the bars all bottom out at y = 0, we can position the text a little below that by setting the @y attribute to 15. Recall that y values in SVG get larger as one moves down (contrary to what users expect), so a positive 15 value is lower than the 0 value that represents the bottom of the bar.
<text x="{$xPosition}" y="-{. + 5}"><xsl:value-of select="."/></text>
We position the text that represents the numerical value of the bar (its height) above the top of the bar. The top of the bar was the negative value of the vote count, so that, for example, a vote of 20 has a starting y position of -20. To put the numerical value above that on the graph, we have to increase the negative value, which moves the image upward in the coordinate space. We do that by adding 5 to the actual value (the dot here represents the content of the <stooge> element, which is a number) and then sticking a minus sign in front of that sum.

The result

(If you don’t see a bar chart below, you aren’t using an SVG-compliant browser. Try the most recent versions of IE, Firefox, Opera, Chrome, or Safari.)

Moe 50 Larry 30 Curly 20

What next?

Note that the spacing of the text in the image above is a bit awkward. The textual labels begin at the left edge of the rectangle, but we’d really like them to be centered. We can fix that by changing the <text> elements to add the @text-anchor attribute, which dictates which part of the text is positioned at the value of the @x attribute of the <text> element. For example, <text x="100" y="100" text-anchor="middle"> means that the middle of whatever the text is will be at the 100,100 position in the coordinate space. Without the @text-anchor attribute, the left side of the text would be at x = 100 (by default the alignment point is the left edge, which is why the left edge of all of our text is aligned with the left edge of the rectangle in the image above). If we specify text-anchor='middle' inside the <text> element, the middle of the text will be aligned with whatever we specify as the value of the @x attribute.

Note, though, that this won’t give us what we want. It will align the middle of the text not with the middle of the rectangle, but with the left edge of the rectangle. This will produce:

Moe 50 Larry 30 Curly 20

To fix that we need to set the value of the @x attribute of the <text> element (but not the <rect>) to the midpoint of the bar, rather than the left edge. The bar stays in the same place, but now the middle of the text will be aligned with the middle of the bar because that’s what the @x attribute specifies. Since the width of our bar is 35, the midpoint is equal to the starting point + 17.5. When we change the XSLT accordingly, we get the positioning we want:

Moe 50 Larry 30 Curly 20