Wednesday, April 02, 2008

Simple SVG chart generation with XSLT

This week, for my job, I have to create a report generator for a financial company. The reports must be in PDF, so I naturally decided to use XSL-FO. Among other things, the reports contain graphical charts with, you know, financial stuff. The client wants its developers to be able to generate JPEG files themselves, so for the charts I just have to include external graphic files.

But I was curious to see if SVG was adapted to fit in this scenario. So this evening, after my working hours, I created a sample input files and started to learn a little bit about SVG. It was incredible as I was able to quickly get the result I wanted for a static SVG document.

Then the fun part started: create the XSLT stylesheet to transform the input document to the final SVG chart. The goal is of course to have a simple stylesheet that is generic enough to not be bound to specific lengths or other magic values.

And I think the result is quite interesting, assuming it was written in a few hours, without knowledge of SVG at the beginning. Of course the kind of chart is fixed, as well the input format is fixed. But it is flexible enough to adapt to various lengths, various Y axis scales, and such.

This stylesheet is not at all aimed to be used as such, but I think it can be a good strating point for similar SVG charts generation with XSLT. If I have the time, and if I want to, I'd try to make it more configurable, especially in the way the input is provided (I think FXSL can be of great help here to provide adapters for any input document type.)

Basically the input looks like the following. Those numbers are the number of post to XSL List by month, for 2007 (stolen from MarkMail.org):

<input min-value="500" step-value="100" step-number="5">
   <val v="784">Jan-07</val>
   <val v="765">Feb-07</val>
   <val v="910">Mar-07</val>
   <val v="734">Apr-07</val>
   <val v="907">May-07</val>
   <val v="626">Jun-07</val>
   <val v="865">Jul-07</val>
   <val v="682">Aug-07</val>
   <val v="790">Sep-07</val>
   <val v="725">Oct-07</val>
   <val v="649">Nov-07</val>
   <val v="577">Dec-07</val>
</input>

The result of the transformation looks like this (screenshot of the Firefox rendering of the SVG document):

And finally here is the stylesheet itself:

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
                xmlns:xs="http://www.w3.org/2001/XMLSchema"
                xmlns:svg="http://www.w3.org/2000/svg"
                xmlns:my="http://www.fgeorges.org/TMP/svg/charts#internals"
                version="2.0">

   <xsl:output indent="yes"/>

   <!--
       +- - - - - - - - - - - - - - - - - - - - - - - -+
       |  +- - - - - - - - - - - - - - - - - - - -+ nn |
       |  |                                       |    |
       |  |                                   o   |    |
       |  | . . . . . . . . . . .o. . . . . oo. . | nn |
       |  |                    oo oo      oo      |    |
       |  |         o        oo     o   oo        |    |
       |  | . . . oo oo . ooo . . . .ooo. . . . . | nn |
       |  |    ooo     ooo                        |    |
       |  |   o                                   |    |
       |  +- -|- -|- -|- -|- -|- -|- -|- -|- -|- -+ nn |
       |      x   x   x   x   x   x   x   x   x        |
       +- - - - - - - - - - - - - - - - - - - - - - - -+
       
       The outer box is the whole space that the diagram will occupy.
       The length between that imaginary box and the top-left corner
       of the diagram is represented by '$init-x' and '$init-y'.
       
       The length of the rectangle of the diagram itself (the inner
       box in the picture) is represented by '$width' and '$height'.
       
       The number of steps on the right-hand Y axis (in the picture
       there are 3 steps, that is 3 steps "between" the various "nn"s)
       is represented by @step-number in the input document.  The
       numeric value between two steps is @step-value.
       
       The diagram's plot (the "o"s) and the X axis (the "x"s) are
       represented by the input document.  Exemple of input:
       
           <input min-value="100" step-value="5" step-number="5">
              <val v="110">Jan-08</val>
              <val v="107">Feb-08</val>
              <val v="123">Mar-08</val>
           </input>
       
       In this example, the Y axis will be from 100 to 125, with a
       line (and a label) from 5 to 5.  The X axis will have 3 labels
       (from Jan to Mar) and the plot will be computed from 3 values:
       110, 107 and finally 123.
   -->
   <xsl:param name="init-x" as="xs:double" select="10"/>
   <xsl:param name="init-y" as="xs:double" select="10"/>
   <xsl:param name="width"  as="xs:double" select="500"/>
   <xsl:param name="height" as="xs:double" select="250"/>

   <xsl:variable name="baseline"  select="$height + $init-y"/>

   <!--
       For test purpose: an root SVG element that should be ok for the
       default values of the global parameters, provided an input of
       10 or 20 values.
   -->
   <xsl:template match="/">
      <svg:svg width="540" height="300">
         <svg:g>
            <xsl:apply-templates select="*"/>
         </svg:g>
      </svg:svg>
   </xsl:template>

   <!--
       The Y axis, the X axis and the plot line.
   -->
   <xsl:template match="input">
      <!-- the diagram's box -->
      <svg:rect x="{ $init-x }" y="{ $init-y }"
                width="{ $width }" height="{ $height }"
                fill="#fff" stroke="#000"/>
      <!-- the Y axis's labels and their lines -->
      <xsl:sequence select="my:lines(@min-value, @step-value, @step-number)"/>
      <xsl:variable name="len" select="$width div count(*)"/>
      <!-- the X axis's labels -->
      <xsl:apply-templates select="*">
         <xsl:with-param name="len" select="$len"/>
      </xsl:apply-templates>
      <!-- the plot line -->
      <svg:path stroke="blue" stroke-width="1" fill="none">
         <xsl:attribute name="d">
            <xsl:apply-templates select="*" mode="path">
               <xsl:with-param name="len"       select="$len"/>
               <xsl:with-param name="min"       select="@min-value"/>
               <xsl:with-param name="one-y-len" select="
                   ( $height div @step-number ) div @step-value"/>
            </xsl:apply-templates>
         </xsl:attribute>
      </svg:path>
   </xsl:template>

   <!--
       Draw a label on the X axis.
   -->
   <xsl:template match="val">
      <xsl:param name="len" as="xs:double"/>
      <xsl:variable name="x" select="my:x-pos($len, position())"/>
      <svg:path d="M { $x },{ $baseline } L { $x },{ $baseline + 5 }" stroke="#000"/>
      <svg:text x="{ $x + 10 }" y="{ $baseline + 15 }"
                transform="rotate(-45 { $x + 10 } { $baseline + 15 })"
                font-size="10px" text-anchor="end">
         <xsl:value-of select="."/>
      </svg:text>
   </xsl:template>

   <!--
       Compute one single step of an SVG path's @d, to draw the plot.
   -->
   <xsl:template match="val" mode="path">
      <xsl:param name="len"       as="xs:double"/>
      <xsl:param name="min"       as="xs:double"/>
      <xsl:param name="one-y-len" as="xs:double"/>
      <xsl:value-of select="if ( position() eq 1 ) then 'M' else 'L'"/>
      <xsl:text> </xsl:text>
      <xsl:value-of select="my:x-pos($len, position())"/>
      <xsl:text>,</xsl:text>
      <xsl:value-of select="my:y-pos($one-y-len, @v, $min)"/>
      <xsl:text> </xsl:text>
   </xsl:template>

   <!--
       The lines for each Y step, as well as the label for each Y
       step.
   -->
   <xsl:function name="my:lines" as="element()+">
      <xsl:param name="min"      as="xs:double"/>
      <xsl:param name="step-val" as="xs:double"/>
      <xsl:param name="step-num" as="xs:integer"/>
      <xsl:variable name="step-len" select="$height div $step-num"/>
      <!-- the N - 1 lines -->
      <xsl:for-each select="1 to ($step-num - 1)">
         <xsl:variable name="y" select="(. * $step-len) + $init-y"/>
         <svg:path d="M { $init-x },{ $y } L { $width + $init-x },{ $y }" stroke="#AAA"/>
      </xsl:for-each>
      <!-- the N + 1 labels -->
      <xsl:for-each select="0 to $step-num">
         <xsl:variable name="y" select="(. * $step-len) + $init-y"/>
         <svg:text x="{ $width + $init-x + 25 }" y="{ $y + 4 }" font-size="10px"
                   text-align="end" text-anchor="end" font-family="Helvetica Condensed">
            <xsl:value-of select="$min + ($step-num - .) * $step-val"/>
         </svg:text>
      </xsl:for-each>
   </xsl:function>

   <!--
       Compute the absolute X position from the ordinal position and
       the length of one X step.
   -->
   <xsl:function name="my:x-pos" as="xs:double">
      <xsl:param name="step-len" as="xs:double"/>
      <xsl:param name="position" as="xs:integer"/>
      <xsl:sequence select="($step-len * $position) - ($step-len div 2) + $init-x"/>
   </xsl:function>

   <!--
       Compute the absolute Y position for one point of the diagram's
       plot.  $one-len is the length of 1 on the Y axis, $value is the
       Y value of the plot's point, and $min is the minimal value on
       the Y axis.
   -->
   <xsl:function name="my:y-pos" as="xs:double">
      <xsl:param name="one-len" as="xs:double"/>
      <xsl:param name="value"   as="xs:double"/>
      <xsl:param name="min"     as="xs:double"/>
      <xsl:variable name="mid" select="$height div 2"/>
      <!-- scale the value to the scale [min - max] -->
      <xsl:variable name="val" select="($value - $min) * $one-len"/>
      <!-- reverse 0->$height and $height->0, 'cause in SVG y=0 is at top -->
      <xsl:variable name="rev" select="(- ($val - $mid)) + $mid"/>
      <!-- slide because our graph begins at y=$init-y -->
      <xsl:value-of select="$rev + $init-y"/>
   </xsl:function>

</xsl:stylesheet>

Labels: ,

9 Comments:

Anonymous Anonymous said...

DC gave me quite a bit of help to do this with 1.0 some time back. SVG is great for this class of visualisation! More important, tiny graphs can be zoomed to see all the detail, the biggest advantage of SVG IMHO.

10:34  
Blogger Florent Georges said...

Hi Dave,

Yes, that's the advantage of vectorial drawing. For what I've seen, SVG has powerful tools and allows you to focus on the drawing of your shapes. I am not a very good mathematician, so I was happy to discover facilities as switching between absolute and relative coordinates, the powerful path commands, etc.

I've written a similar stylesheet for pie charts (without any extensions, thanks to the trigonometric functions from FXSL!) If I find the time, I'll try to generalize them and provide a few high-level chart drawing facilities.

Regards, -- Florent

15:22  
Blogger bla bla said...

Thank you very much, this example helped me a lot with a similar problem with paths.

11:18  
Blogger Paolo Corno said...

I tried to replicate the transform:

1) copied the whole xsl into test.xsl.

2) copied th exml fragment into test.xml

3)addded the lines

<?xml version="1.0" encoding="ISO-8859-1"?>

<?xml-stylesheet type="text/xsl" href="test.xsl"?>
at the beginnig of test.xml

I tried to poen it with firefox but I got an error "XSLT transform failed" Do I need something more?

17:10  
Blogger Florent Georges said...

Hi Paolo,

I am sorry, I've just seen your pending comment (I have to accept comments, to filter spam.) Sorry for the delay.

For your problem, this is because FF does not support XSLT 2.0. But this example could be adapted to XSLT 1.0. At first glance, the only XSLT 2.0 specific feature I see is xsl:function. You can use named templates instead. Or you can use Saxon to transform the XML, save the resulting SVG, then open it with FF, as I did.

Regards, -- Florent

22:46  
Blogger Dan McCreary said...

A very nice example. I think that SVG is a great technology and I am very happy to see that the Google people are using Flash to render it in IE. Hopefully we will see an XQuery module for SVG in the near future.

16:03  
Blogger James said...

The xsl:sequence tag bombs out FF, xsltproc and saxon-xslt.

03:48  
Blogger James said...

xsl:sequence bombed out FF, xsltproc and saxon-xslt.

03:49  
Blogger Florent Georges said...

Yes indeed. xsl:sequence is an XSLT 2.0 instruction, and FF still implementes only 1.0. This stylesheet is then aimed to be used on the server-side, or with a browser supporting 2.0 (I am aware of none at the moment), or e.g. with Saxon-CE (a cross-compilation to Javascript of the excellent Saxon processor, specifically designed to provide XSLT 2.0 to browsers).

If you investigate into using Saxon-CE, I am interested to see your feedback here ;-)

Thanks for your comment, -- Florent

10:39  

Post a Comment

<< Home