Maintained by: David J. Birnbaum (djbpitt@gmail.com) Last modified: 2021-12-27T22:03:54+0000
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).
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="3.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)"/>
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')"/>
<g transform="translate(0,{$maxHeight + 20})">
@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"/>
<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()"/>
<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>
@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>
<stooge>
element, which is a number) and then sticking a
minus sign in front of that sum.(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.)
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:
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: