Digital humanities


Maintained by: David J. Birnbaum (djbpitt@gmail.com) [Creative Commons BY-NC-SA 3.0 Unported License] Last modified: 2021-04-14T15:02:20+0000


SVG assignment #2 answers

Overview

The basic task of SVG assignment 2 was to create an SVG bar graph of the Democratic results of the 2012 US presidential election. See the assignment page for a more detailed description, suggestions, and a link to the input data in XML form.

Simple solution

Here is one possible solution. It isn’t easy to follow because we’ve used a lot of hard-coded numbers, and we provide a more legible version below:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns="http://www.w3.org/2000/svg"
    version="3.0" xmlns:xs="http://www.w3.org/2001/XMLSchema" exclude-result-prefixes="#all">
    <xsl:output method="xml" indent="yes"/>
    <xsl:template match="/">
        <svg height="375">
            <g transform="translate(30, 330)">
                <line x1="20" x2="20" y1="0" y2="-320" stroke="black" stroke-width="1"/>
                <line x1="20" x2="1550" y1="0" y2="0" stroke="black" stroke-width="1"/>
                <line x1="20" x2="1550" y1="-150" y2="-150" stroke="black" opacity="0.5"
                    stroke-dasharray="8 4" stroke-width="1"/>
                <text x="10" y="5" text-anchor="end">0%</text>
                <text x="10" y="-145" text-anchor="end">50%</text>
                <text x="10" y="-295" text-anchor="end">100%</text>
                <xsl:apply-templates select="//state"/>
            </g>
        </svg>
    </xsl:template>
    <xsl:template match="state">
        <xsl:variable name="xPosition" select="(position() - 1) * 30"/>
        <xsl:variable name="totalVotes" select="sum(candidate)"/>
        <xsl:variable name="demVotes" select="candidate[@party = 'Democrat']"/>
        <xsl:variable name="demPer" select="$demVotes div $totalVotes"/>
        <xsl:variable name="acro" select="@acro"/>
        <rect x="{$xPosition + 22}" y="-{$demPer * 300}" stroke="black" stroke-width=".5"
            fill="blue" width="{20}" height="{$demPer * 300}"/>
        <text x="{$xPosition + 20 div 2 + 22}" y="20" text-anchor="middle">
            <xsl:value-of select="$acro"/>
        </text>
    </xsl:template>
</xsl:stylesheet>

Using variables to improve legibility, development, and maintenance

Here we replace a lot of the hard-coded numbers with variables, which we find easier to read. They’re also easier to maintain; should you decide during development to change the width of the bars, for example, it’s easier to change the value you’ve used to declare a variable called $barWidth than to pick apart all of the literal numbers in the code to find the one where you specify that width. We’ve documented the use of variables in XML comments, which is what we’d do in Real Life, as well.

Weve also specified the datatype of the variable, that is, whether it’s supposed to be a string, an integer (whole number), a double (number that may have digits after the decimal point), etc. If you don’t specify the datatype, XSLT will usually figure it out for you, but sometimes it guesses wrong, so in our own work we always specify the type explicitly by using @as variable. The most common datatypes are strings (as="xs:string"), integers (as="xs:integer), and doubles (as="xs:double"). Here’s our second solution with datatype information added to the variable declarations, and with additional variables and labels:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns="http://www.w3.org/2000/svg"
    version="3.0" xmlns:xs="http://www.w3.org/2001/XMLSchema" exclude-result-prefixes="#all">
    <!-- ========================================================== -->
    <!-- SVG is XML, so use the "xml" output method                 -->
    <!-- ========================================================== -->
    <xsl:output method="xml" indent="yes"/>
    <!-- ========================================================== -->
    <!-- Stylesheet variables                                       -->
    <!--                                                            -->
    <!-- $barWidth : width of rectangular bar                       -->
    <!-- $interbarSpacing : space between the right edge of one bar -->
    <!--    and the left edge of the next                           -->
    <!-- $barInterval : distance between the left sides of adjacent -->
    <!--    bars, equal to $barWidth plus $interbarSpacing          -->
    <!-- $yScale : multiplier so that bars are not too short        -->
    <!-- ========================================================== -->
    <xsl:variable name="barWidth" select="20" as="xs:integer"/>
    <xsl:variable name="interbarSpacing" select="$barWidth div 2" as="xs:double"/>
    <xsl:variable name="barInterval" select="$barWidth + $interbarSpacing" as="xs:double"/>
    <xsl:variable name="yScale" select="300" as="xs:integer"/>
    <!-- ========================================================== -->
    <!-- Templates                                                  -->
    <!-- ========================================================== -->
    <xsl:template match="/">
        <svg height="{1.5 * $yScale}">
            <g transform="translate(100, 330)">
                <!-- ============================================== -->
                <!-- Plot in upper right, move down with @transform -->
                <!-- Draw in z-index order:                         -->
                <!--     ruling, bars, axes and labels              -->
                <!-- ============================================== -->
                <!-- Dashed line at 50% on the y axis               -->
                <!-- ============================================== -->
                <!-- Ruling line                                    -->
                <line x1="0" x2="{count(//state) * $barInterval + $interbarSpacing}"
                    y1="-{$yScale div 2}" y2="-{$yScale div 2}" stroke="lightgray"
                    stroke-dasharray="8 4" stroke-width="1"/>
                <!-- ============================================== -->
                <!-- Bars for states                                -->
                <!-- ============================================== -->
                <xsl:apply-templates select="//state"/>
                <!-- ============================================== -->
                <!-- Axes and labels                                -->
                <!-- X axis length computed from number of states   -->
                <!-- Y axis height computed from $yScale, with      -->
                <!--     added space for labels                     -->
                <!-- ============================================== -->
                <!-- Axis lines (X, then Y)                         -->
                <!-- ============================================== -->
                <xsl:variable name="xAxisLength" as="xs:double"
                    select="count(//state) * $barInterval + $interbarSpacing"/>
                <line x1="0" x2="{$xAxisLength}" y1="0" y2="0" stroke="black" stroke-width="1"
                    stroke-linecap="square"/>
                <line x1="0" x2="0" y1="0" y2="-{$yScale}" stroke="black" stroke-width="1"
                    stroke-linecap="square"/>
                <!-- ============================================== -->
                <!-- Y axis interval labels (0%, 50%, 100%)         -->
                <!-- ============================================== -->
                <text x="-5" y="0" text-anchor="end" dominant-baseline="middle">0%</text>
                <text x="-5" y="-{$yScale div 2}" text-anchor="end" dominant-baseline="middle"
                    >50%</text>
                <text x="-5" y="-{$yScale}" text-anchor="end" dominant-baseline="middle">100%</text>
                <!-- ============================================== -->
                <!-- General labels (X, Y, graph)                   -->
                <!-- ============================================== -->
                <text x="{$xAxisLength div 2}" y="50" text-anchor="middle" font-size="x-large"
                    >State</text>
                <text x="-50" y="-{$yScale div 2}" text-anchor="middle" writing-mode="tb"
                    font-size="x-large">Democratic percentage</text>
                <text x="{$xAxisLength div 2}" y="90" text-anchor="middle" font-size="xx-large"
                    >Percentage of Democratic votes by state in 2012 US presidential election</text>
            </g>
        </svg>
    </xsl:template>
    <xsl:template match="state">
        <!-- ====================================================== -->
        <!-- Template variables are different for each state        -->
        <!-- ====================================================== -->
        <xsl:variable name="statePos" select="position() - 1" as="xs:integer"/>
        <xsl:variable name="xPosition" select="$statePos * $barInterval + $interbarSpacing"
            as="xs:double"/>
        <xsl:variable name="midBarPostion" as="xs:double" select="$xPosition + $barWidth div 2"/>
        <xsl:variable name="totalVotes" select="sum(candidate)" as="xs:double"/>
        <xsl:variable name="demVotes" select="candidate[@party = 'Democrat']" as="xs:integer"/>
        <xsl:variable name="demPer" select="$demVotes div $totalVotes" as="xs:double"/>
        <xsl:variable name="barHeight" as="xs:double" select="$demPer * $yScale"/>
        <xsl:variable name="acro" select="@acro" as="xs:string"/>
        <!-- We’ve included the $barColor variable to illustrate how -->
        <!-- to set a variable conditionally. In Real Life we        -->
        <!-- color the Democratic bar red under any circumstances    -->
        <!-- because that would be cognitively jarring for a human.  -->
        <xsl:variable name="barColor" as="xs:string" select="
                if ($demPer ge 0.5) then
                    'blue'
                else
                    'red'"/>
        <!-- ====================================================== -->
        <rect x="{$xPosition}" y="-{$barHeight}" stroke="black" stroke-width=".5" fill="{$barColor}"
            width="{$barWidth}" height="{$barHeight}"/>
        <text x="{$midBarPostion}" y="20" text-anchor="middle" fill="black" font-size="small">
            <xsl:value-of select="$acro"/>
        </text>
        <text x="{$midBarPostion}" y="-{$barHeight + 5}" text-anchor="middle" fill="black"
            font-size="small">
            <xsl:value-of select="format-number($demPer * 100, '0.0')"/>
        </text>
    </xsl:template>
</xsl:stylesheet>

When we developed the preceding version, we initially specified the datatype of the $totalVotes variable as an integer because we knew that all of the input values to the sum() function (the votes for all of the candidates in a particular state) were integers, which means that their sum would also be an integer. To our surprise, when we ran the transformation, <oXygen/> reported an error, saying that although the type was supposed to be an integer, it was actually a double. To figure out what was going on, we looked up the sum() function in Michael Kay (p. 889), where we learned that when sum() is applied to values that don’t have an explicit datatype (and the numbers in XML in this case don’t), the sum() function converts them to doubles before adding them.

So why doesn’t the XSLT transformation engine know that the vote counts in the XML are integers and convert them to integers instead of doubles? It turns out that although it’s clear to a human that they’re integers because they look like whole numbers, they could have more than one logical datatype (for example, they could be doubles that happen to have nothing but implicit zeroes after an implicit decimal point; they could also be strings of characters to be printed, and not any numeric type), and XSLT deals with that by treating all literal values that don’t have an explicit datatype as untyped (the technical explanation is that, since everything has to have a datatype, it regards their type as xs:untypedAtomic, which means … er … that they don’t have an explicit datatype and have to be converted to something explicit internally before you add them or otherwise process them). If you try to perform arithmetic on them, such as with with sum() function, the XSLT engine has to convert them to a type that’s explicitly numeric first, since you can perform arithmetic only on numbers. So should they be converted to integers or doubles? Since you can treat integers like doubles that happen to have only zeroes after the decimal point and get the right answer, but the reverse isn’t the case, XSLT decided that it would always treat numbers to be added as doubles, whether they could alternatively be regarded as integers or not.

To correct our error, we changed the datatype of our $totalVotes to xs:double, since that was the value that the sum() function was going to return. A double is a number that may have digits to the right of the decimal point, but those digits can be zeroes (or, in this case, potential zeroes), and in that case the double has the same numeric value as the corresponding integer. In other words, if the total number of votes in a particular state is equal to, say, the integer 100, it’s also equal to the double 100..