Example: A library of trigonometric functions
page 140 product name=Sandpiper34.3product
product name=Crane50.4product region
region nameMidwestname
product name=Heron44.5product product name=Kingfisher9.3product
product name=Pelican5.7product product name=Sandpiper28.8product
product name=Crane54.6product region
region nameNorthwestname
product name=Heron36.6product product name=Kingfisher5.4product
product name=Pelican9.1product product name=Sandpiper39.1product
product name=Crane58.2product region
sales
Our goal is to create an SVG file that looks like that in Figure 8-2
.
Figure 8-2. Target SVG file format
To make things really interesting, well generate an HTML page that embeds the SVG file. Well use the
Redirect
extension we used earlier to generate an HTML file and an SVG file in a single transformation. If we view the HTML page in a web browser, we can use Adobes
SVG plug-in to make the graphic interactive. If we move the mouse over a given slice of the pie, the legend will change to show the sales details for that region of the company. Thus,
well also have to create all the different legends and generate the JavaScript code to make the various SVG elements visible or hidden in response to mouse events.
Figure 8-3 shows how
the graphic looks if we move the mouse over the pie slice for the Southwest region.
Figure 8-3. SVG chart changes in response to mouse events
page 141
XPaths limited math functions wont allow us to calculate the dimensions of the various arcs that make up the pie chart, so well use extension functions to solve this problem. Fortunately
for us, Java provides all the basic trigonometric functions we need in the
java.lang.Math
class. Even better, Xalan makes it easy for us to load this class and execute its methods such as
sin
,
cos
, and
toRadians
. Well go over the relevant details as they appear in the stylesheet. First, we have to declare
our namespace prefixes, just as we did when we used an extension element:
xsl:stylesheet version=1.0 xmlns:xsl=http:www.w3.org1999XSLTransform
xmlns:redirect=org.apache.xalan.xslt.extensions.Redirect extension-element-prefixes=java redirect
xmlns:java=http:xml.apache.orgxsltjava
We associated the
java
namespace prefix with the string http:xml.apache.orgxsltjava; Xalan uses this string to support extension functions and elements written in Java. Before we
use the extension functions to generate the SVG file, well take care of the HTML. First, we generate the
head
section of the HTML document, including the JavaScript code we need to make the pie chart interactive:
xsl:template match=sales html
head title
xsl:value-of select=captionheading title
script language=JavaScript1.2 xsl:comment
xsl:call-template name=js xsl:text xsl:textxsl:comment
script head
body center
embed name=pie width=650 height=500 src=saleschart.svg center
body html
The HTML file we create generates an HTML
title
element from the XML document, calls the named template that generates the JavaScript code, then embeds the SVG file that
well generate in a minute. The template we use to generate the JavaScript code is worth a closer look. Heres the code:
xsl:template name=js xsl:text
function suppress_errors {
return true; }
function does_element_exist svg_name, element_name {
First, redirect the error handler so that if the SVG plug-in has not yet loaded or is not present, it doesnt cause the browser to
issue a JavaScript error. var old_error = window.onerror;
window.onerror = suppress_errors; Now attempt to get the SVG object.
var svgobj = document.embeds[svg_name]. getSVGDocument.getElementByIdelement_name;
Reset the error handler to the browsers default handler. window.onerror = old_error;
page 142 Return appropriate value.
if svgobj == null return false;
else return true;
} function mouse_over target_id
{ var svgdoc = document.pie.getSVGDocument;
var svgobj; var svgstyle;
var detail_name = details + target_id; svgobj = svgdoc.getElementByIddetail_name;
if svgobj = null {
svgstyle = svgobj.getStyle; svgstyle.setProperty visibility, visible;
} xsl:text
xsl:for-each select=salesregion xsl:textsvgobj = svgdoc.getElementByIdlegendxsl:text
xsl:value-of select=positionxsl:text;xsl:text xsl:text
if svgobj = null {
svgstyle = svgobj.getStyle; svgstyle.setProperty visibility, hidden;
} xsl:text
xsl:for-each xsl:text
Propagate the event to other handlers. return true;
} function mouse_out
{ var svgdoc = document.pie.getSVGDocument;
var svgobj; var svgstyle;
xsl:text xsl:for-each select=salesregion
xsl:textsvgobj = svgdoc.getElementByIdlegendxsl:text xsl:value-of select=positionxsl:text;xsl:text
xsl:text if svgobj = null
{ svgstyle = svgobj.getStyle;
svgstyle.setProperty visibility, visible; }
xsl:text xsl:textsvgobj = svgdoc.getElementByIddetailsxsl:text
xsl:value-of select=positionxsl:text;xsl:text xsl:text
if svgobj = null {
svgstyle = svgobj.getStyle; svgstyle.setProperty visibility, hidden;
} xsl:text
xsl:for-each xsl:text
Propagate the event to other handlers. return true;
} xsl:text
page 143 xsl:template
We begin with the functions
suppress_errors
and
does_element_exist
that well need for error checking and handling. The
mouse_over
function is more complicated. When the user moves the mouse over a particular section of the pie, we need to make some SVG
elements visible and others invisible. Well use a naming convention here; for each
region
in our original document, well generate a legend entry and a set of details. Originally, the legend is visible and all details are hidden. When our
mouse_over
function is called, it makes all the legend elements hidden and makes the appropriate details element visible.
Heres how the generated code looks:
function mouse_over target_id {
var svgdoc = document.pie.getSVGDocument; var svgobj;
var svgstyle; var detail_name = details + target_id;
svgobj = svgdoc.getElementByIddetail_name; if svgobj = null
{ svgstyle = svgobj.getStyle;
svgstyle.setProperty visibility, visible; }
svgobj = svgdoc.getElementByIdlegend1; if svgobj = null
{ svgstyle = svgobj.getStyle;
svgstyle.setProperty visibility, hidden; }
... Propagate the event to other handlers.
return true; }
The section that begins
svgdoc.getElementByIdlegend1
repeats for each
region
in the XML source document. The repeated code ensures that all legend elements are hidden. This
code handles the mouse over event; our final task is to handle the mouse out event. The generated
mouse_out
function looks like:
function mouse_out {
var svgdoc = document.pie.getSVGDocument; var svgobj;
var svgstyle; svgobj = svgdoc.getElementByIdlegend1;
if svgobj = null {
svgstyle = svgobj.getStyle; svgstyle.setProperty visibility, visible;
} svgobj = svgdoc.getElementByIddetails1;
if svgobj = null {
svgstyle = svgobj.getStyle; svgstyle.setProperty visibility, hidden;
} ...
Propagate the event to other handlers. return true;
}
page 144
The
mouse_out
function makes sure that all legend elements are visible and that all details elements are hidden. Although these two functions are relatively simple, they work together
to make our pie chart interactive and dynamic.
One final note about our generated JavaScript code; notice how we invoked the named template inside an
xsl:comment
element:
script language=JavaScript1.2 xsl:comment
xsl:call-template name=js xsl:text xsl:textxsl:comment
script
Scripting code is typically written inside a comment in an HTML file, allowing browsers that dont support scripting to safely ignore the code.
The end of the generated JavaScript code looks like this:
Propagate the event to other handlers. return true;
} --script
We used an
xsl:text
element to draw the double slash at the end of the script. The double slash is a JavaScript comment, which tells the JavaScript
processor to ignore the
--
at the end of the script. Without this slash, some JavaScript processors attempt to process the end of the comment and issue
an error message. Keep this in mind when youre generating JavaScript code with your stylesheets; if you dont, youll have trouble tracking down
the occasional errors that occur in some browsers.
Now that weve built the HTML file, heres how we draw each wedge of the pie: 1. Calculate the total sales for the entire company and store it in a variable. Calculating
total sales is relatively expensive because the XSLT processor has to navigate the entire tree. Well need to use this value many times, so we calculate it once and store
it away. Heres the calculation for total sales:
xsl:variable name=totalSales select=sumproduct
2. For each slice of the pie, we calculate certain values and pass them as parameters to the
region
template. First, we determine the color of the slice and the total sales for this particular region of the company. We use the
position
function and the
mod
operator to calculate the color, and we use the
sum
function to calculate the sales for this region of the company.
3. If this is the first slice of the pie, well explode it. That means that the first slice will be offset from the rest of the pie. We will set the variable
explode
as follows:
xsl:variable name=explode select=position=1
4. The last value we calculate is the total sales of all previous regions. When we draw each slice of the pie, we rotate the coordinate axis a certain number of degrees. The
amount of the rotation depends on how much of the total sales have been drawn so far. In other words, if weve drawn exactly 50 percent of the total sales so far, well
rotate the axis 180 degrees 50 percent of 360. Rotating the axis simplifies the trigonometry we have to do.
page 145
To calculate the total sales weve drawn so far, we use the
preceding-sibling
axis:
xsl:with-param name=runningTotal select=sumpreceding-sibling::regionproduct
5. Inside the template itself, our first step is to calculate the angle in radians of the current slice of the pie. This is the first time we use one of our extension functions:
xsl:variable name=currentAngle select=java:java.lang.Math.toRadiansregionSales div
totalSales 360.0
We store this value in the variable
currentAngle
; well use this value later with the
sin
and
cos
functions. 6. Now were finally ready to draw the pie slice. Well do this with an SVG
path
element. Heres what one looks like; well discuss what the attributes mean in a minute:
path onclick=return false; onmouseout=mouse_out; style=fill:orange; stroke:black; stroke-width:2; fillrule:evenodd; stroke-linejoin:bevel;
transform=translate100,160 rotate-72.24046757584189 onmouseover=return mouse_over2;
d=M 80 0 A 80 80 0 0 0 21.318586104178774 -77.10718440274366 L 0 0 Z path
The following stylesheet fragment generated this intimidating element:
path style=fill:{color}; stroke:black; stroke-width:2; fillrule:evenodd; stroke-linejoin:bevel;
onmouseout=mouse_out; onclick=return false; xsl:attribute name=transform
xsl:choose xsl:when test=explode
xsl:texttranslatexsl:text xsl:value-of
select=java:java.lang.Math.coscurrentAngle div 2 20 + 100
xsl:text,xsl:text xsl:value-of
select=java:java.lang.Math.sincurrentAngle div 2 -20 + 160
xsl:text xsl:text xsl:when
xsl:otherwise xsl:texttranslate100,160 xsl:text
xsl:otherwise xsl:choose
xsl:text rotatexsl:text xsl:value-of
select=-1 runningTotal div totalSales 360.0 xsl:textxsl:text
xsl:attribute xsl:attribute name=onmouseover
xsl:textreturn mouse_overxsl:text xsl:value-of select=positionxsl:text;xsl:text
xsl:attribute xsl:attribute name=d
xsl:textM 80 0 A 80 80 0 xsl:text xsl:choose
xsl:when test=currentAngle 3.14 xsl:text1 xsl:text
xsl:when xsl:otherwise
xsl:text0 xsl:text xsl:otherwise
xsl:choose
page 146 xsl:text0 xsl:text
xsl:value-of select=java:java.lang.Math.coscurrentAngle 80
xsl:text xsl:text xsl:value-of
select=java:java.lang.Math.sincurrentAngle -80 xsl:text L 0 0 Z xsl:text
xsl:attribute path
The
style
attribute defines various properties of the path, including the color in which the path should be drawn, with which the path should be filled, etc. With the
exception of the color, everything in the
style
attribute is the same for all slices of the pie. The
transform
attribute does two things: it moves the center of the coordinate space to a particular point, then it rotates the axes some number of degrees. The center
of the coordinate space is moved to a slightly different location if the
explode
variable is true. The extent to which the axes are rotated depends on the percentage of total sales represented by the previous regions of the company. Moving the center of
the coordinate space and rotating the axes simplifies the math we have to do later.
That brings us to the gloriously cryptic
d
attribute. This attribute contains a number of drawing commands; in our previous example, we move the current point to
80,0 M
stands for move, then we draw an elliptical arc
A
stands for arc with various properties. Finally, we draw a line
L
stands for line from the current point the end of our arc to the origin, then we use the
Z
command, which closes the path by drawing a line from wherever we are to wherever we started.
If you really must know what the properties of the
A
command are, they are the two radii of the ellipse, the degrees by which the x-axis should be rotated, two parameters
called the large-arc-flag and the sweep-flag that determine how the arc is drawn, and the x- and y-coordinates of the end of the arc. In our example here, the two radii of the
ellipse are the same we want the pie to be round, not elliptical. Next is the x-axis rotation, which is
. After that is the large-arc-flag, which is
1
if this particular slice of the pie is greater than 180 degrees,
otherwise. The sweep-flag is , and the last two
parameters, the x- and y-coordinates of the end point, are calculated. See the SVG specification for more details on the
path
and
shape
elements. 7. Our next task is to draw all of the legends. Well draw one legend to identify each
slice of the pie; after that, well create a separate legend for each slice of the pie. Initially, all of the separate legends will be invisible
g style=visibility:hidden
, in SVG parlance, and the basic legend for the pie will be visible. As we mouse over the various slices of the pie, different legends will
become visible or invisible. First, well draw the basic legend, using the
mode
attribute of the
apply-templates
element:
xsl:apply-templates select=. mode=legend xsl:with-param name=color select=color
xsl:with-param name=regionSales select=regionSales xsl:with-param name=y-legend-offset
select=90 + position 20 xsl:with-param name=position select=position
xsl:apply-templates
When we apply our template, we pass in several parameters, including the color of the box in the legend entry and the y-coordinate offset where the legend entry should be
drawn. We call this template once for each
region
element, ensuring that our legend identifies each slice of the pie, regardless of how many slices there are. For each slice,
we draw a box filled with the appropriate color, with the name of the region next to it.
page 147
8. Our final task is to draw the details for this region of the company. Well draw the name of the region in the same color we used for the pie slice, then list all sales
figures for the region. Heres what the template looks like:
xsl:template match=region mode=details xsl:param name=color select=black
xsl:param name=position select=0 xsl:param name=y-legend-offset
g style=visibility:hidden xsl:attribute name=id
xsl:textdetailsxsl:textxsl:value-of select=position xsl:attribute
text style=font-size:14; font-weight:bold; text-anchor:start; fill: {color} x=220
xsl:attribute name=y xsl:value-of select=y-legend-offset
xsl:attribute xsl:value-of select=namexsl:text Sales:xsl:text
text xsl:for-each select=product
text style=font-size:12; text-anchor:start x=220 xsl:attribute name=y
xsl:value-of select=y-legend-offset + position 20 xsl:attribute
xsl:value-of select=name xsl:text: xsl:textxsl:value-of select=.
text xsl:for-each
g xsl:template
Notice that we draw this item to be invisible
style=visibility:hidden
; well use our JavaScript effects to make the various legends and details visible or hidden. In our
stylesheet, we draw the title of the current region using the same color we used for the slice of the pie, followed by the sales figures for each product sold in this region.
Heres the complete stylesheet:
?xml version=1.0 encoding=ISO-8859-1? xsl:stylesheet version=1.0
xmlns:xsl=http:www.w3.org1999XSLTransform xmlns:redirect=org.apache.xalan.xslt.extensions.Redirect
extension-element-prefixes=java redirect xmlns:java=http:xml.apache.orgxsltjava
xsl:output method=html xsl:strip-space elements=
xsl:template name=js xsl:text
function suppress_errors {
return true; }
function does_element_exist svg_name, element_name {
First, redirect the error handler so that if the SVG plug-in has not yet loaded or is not present, it doesnt cause the browser to
issue a JavaScript error. var old_error = window.onerror;
window.onerror = suppress_errors; Now attempt to get the SVG object.
var svgobj = document.embeds[svg_name]. getSVGDocument.getElementByIdelement_name;
page 148 Reset the error handler to the browsers default handler.
window.onerror = old_error; Return appropriate value.
if svgobj == null return false;
else return true;
} function mouse_over target_id
{ var svgdoc = document.pie.getSVGDocument;
var svgobj; var svgstyle;
var detail_name = details + target_id; svgobj = svgdoc.getElementByIddetail_name;
if svgobj = null {
svgstyle = svgobj.getStyle; svgstyle.setProperty visibility, visible;
} xsl:text
xsl:for-each select=salesregion xsl:textsvgobj = svgdoc.getElementByIdlegendxsl:text
xsl:value-of select=positionxsl:text;xsl:text xsl:text
if svgobj = null {
svgstyle = svgobj.getStyle; svgstyle.setProperty visibility, hidden;
} xsl:text
xsl:for-each xsl:text
Propagate the event to other handlers. return true;
} function mouse_out
{ var svgdoc = document.pie.getSVGDocument;
var svgobj; var svgstyle;
xsl:text xsl:for-each select=salesregion
xsl:textsvgobj = svgdoc.getElementByIdlegendxsl:text xsl:value-of select=positionxsl:text;xsl:text
xsl:text if svgobj = null
{ svgstyle = svgobj.getStyle;
svgstyle.setProperty visibility, visible; }
xsl:text xsl:textsvgobj = svgdoc.getElementByIddetailsxsl:text
xsl:value-of select=positionxsl:text;xsl:text xsl:text
if svgobj = null {
svgstyle = svgobj.getStyle; svgstyle.setProperty visibility, hidden;
} xsl:text
xsl:for-each xsl:text
page 149 Propagate the event to other handlers.
return true; }
xsl:text xsl:template
xsl:template match= xsl:apply-templates select=sales
xsl:template xsl:template match=sales
html head
title xsl:value-of select=captionheading
title script language=JavaScript1.2
xsl:comment xsl:call-template name=js
xsl:text xsl:textxsl:comment script
head body
center embed name=pie width=650 height=500 src=saleschart.svg
center body
html redirect:write select=concatsales, chart, .svg
svg width=450 height=300 text style=font-size:24; text-anchor:middle;
font-weight:bold x=130 y=20 xsl:value-of select=captionheading
text text style=font-size:14; text-anchor:middle y=40 x=130
xsl:value-of select=captionsubheading text
xsl:variable name=totalSales select=sumproduct xsl:for-each select=region
xsl:variable name=regionSales select=sumproduct xsl:variable name=color
xsl:choose xsl:when test=position mod 6 = 1
xsl:textredxsl:text xsl:when
xsl:when test=position mod 6 = 2 xsl:textorangexsl:text
xsl:when xsl:when test=position mod 6 = 3
xsl:textpurplexsl:text xsl:when
xsl:when test=position mod 6 = 4 xsl:textbluexsl:text
xsl:when xsl:when test=position mod 6 = 5
xsl:textgreenxsl:text xsl:when
xsl:otherwise xsl:textorangexsl:text
xsl:otherwise xsl:choose
xsl:variable xsl:variable name=explode select=position=1
xsl:apply-templates select=. xsl:with-param name=color select=color
xsl:with-param name=regionSales select=regionSales
page 150 xsl:with-param name=totalSales select=totalSales
xsl:with-param name=runningTotal select=sumpreceding-sibling::regionproduct
xsl:with-param name=explode select=explode xsl:with-param name=position select=position
xsl:apply-templates xsl:apply-templates select=. mode=legend
xsl:with-param name=color select=color xsl:with-param name=regionSales select=regionSales
xsl:with-param name=y-legend-offset select=90 + position 20
xsl:with-param name=position select=position xsl:apply-templates
xsl:apply-templates select=. mode=details xsl:with-param name=color select=color
xsl:with-param name=position select=position xsl:with-param name=y-legend-offset select=110
xsl:apply-templates xsl:for-each
svg redirect:write
xsl:template xsl:template match=region
xsl:param name=color select=red xsl:param name=runningTotal select=0
xsl:param name=totalSales select=0 xsl:param name=regionSales select=0
xsl:param name=explode xsl:param name=position select=1
xsl:variable name=currentAngle select=java:java.lang.Math.toRadiansregionSales div
totalSales 360.0 path style=fill:{color}; stroke:black; stroke-width:2;
fillrule:evenodd; stroke-linejoin:bevel; onmouseout=mouse_out; onclick=return false;
xsl:attribute name=transform xsl:choose
xsl:when test=explode xsl:texttranslatexsl:text
xsl:value-of select=java:java.lang.Math.coscurrentAngle div 2 20 +
100 xsl:text,xsl:text
xsl:value-of select=java:java.lang.Math.sincurrentAngle div 2 -20 +
160 xsl:text xsl:text
xsl:when xsl:otherwise
xsl:texttranslate100,160 xsl:text xsl:otherwise
xsl:choose xsl:text rotatexsl:text
xsl:value-of select=-1 runningTotal div totalSales 360.0 xsl:textxsl:text
xsl:attribute xsl:attribute name=onmouseover
xsl:textreturn mouse_overxsl:text xsl:value-of select=positionxsl:text;xsl:text
xsl:attribute xsl:attribute name=d
xsl:textM 80 0 A 80 80 0 xsl:text xsl:choose
page 151 xsl:when test=currentAngle 3.14
xsl:text1 xsl:text xsl:when
xsl:otherwise xsl:text0 xsl:text
xsl:otherwise xsl:choose
xsl:text0 xsl:text xsl:value-of select=java:java.lang.Math.coscurrentAngle 80
xsl:text xsl:text xsl:value-of select=java:java.lang.Math.sincurrentAngle -80
xsl:text L 0 0 Z xsl:text xsl:attribute
path xsl:template
xsl:template match=region mode=legend xsl:param name=color select=red
xsl:param name=regionSales select=0 xsl:param name=y-legend-offset select=0
xsl:param name=position select=1 g
xsl:attribute name=id xsl:textlegendxsl:textxsl:value-of select=position
xsl:attribute text
xsl:attribute name=style xsl:textfont-size:12; text-anchor:startxsl:text
xsl:attribute xsl:attribute name=x
xsl:text240xsl:text xsl:attribute
xsl:attribute name=y xsl:value-of select=y-legend-offset
xsl:attribute xsl:value-of select=name
xsl:text xsl:text xsl:value-of select=regionSales
xsl:text xsl:text text
path xsl:attribute name=style
xsl:textstroke:black; stroke-width:2; fill:xsl:text xsl:value-of select=color
xsl:attribute xsl:attribute name=d
xsl:textM 220 xsl:text xsl:value-of select=y-legend-offset - 10
xsl:text L 220 xsl:text xsl:value-of select=y-legend-offset
xsl:text L 230 xsl:text xsl:value-of select=y-legend-offset
xsl:text L 230 xsl:text xsl:value-of select=y-legend-offset - 10
xsl:text Zxsl:text xsl:attribute
path g
xsl:template xsl:template match=region mode=details
xsl:param name=color select=black xsl:param name=position select=0
xsl:param name=y-legend-offset g style=visibility:hidden
page 152 xsl:attribute name=id
xsl:textdetailsxsl:textxsl:value-of select=position xsl:attribute
text style=font-size:14; font-weight:bold; text-anchor:start; fill: {color} x=220
xsl:attribute name=y xsl:value-of select=y-legend-offset
xsl:attribute xsl:value-of select=namexsl:text Sales:xsl:text
text xsl:for-each select=product
text style=font-size:12; text-anchor:start x=220 xsl:attribute name=y
xsl:value-of select=y-legend-offset + position 20 xsl:attribute
xsl:value-of select=name xsl:text: xsl:textxsl:value-of select=.
text xsl:for-each
g xsl:template
xsl:stylesheet
In this example, weve used XSLT extension functions to add new capabilities to the XSLT processor. We needed a couple of simple trigonometric functions, and Xalans ability to use
existing Java classes made adding new capabilities simple. You can use this technique to invoke methods of Java classes anywhere you need them. Best of all, we didnt have to write
any Java code to make this happen.