Create a graphic representation of any XML document using SVG

Using the power of Scalable Vector Graphics (SVG), a programmer can create a graphic representation of any XML document. This document shows you how it works and provides several code examples to get you started on the right path.

Some wise individual or maybe it was some advertising executive, once said that a picture is worth a thousand words. Whatever the source, this phrase has become one of those proverbs that is instantly recognizable as true. Say it and everyone nods in agreement, people are basically visual creatures. This could be one of the reasons why graphical representations of information are so popular.

Recently I was working on a personal project where it was necessary to show graphic representations of XML documents. While this might not seem like an issue to people with drawing talent, like my children or my wife, I found myself speechless. Each and every one of those thousand words dried up. It's not that I can’t appreciate drawing, I’m a big fan of M. C. Escher but I couldn’t tell you how he does it even if my life depended on it. You see there is a little thing called talent, which I totally lack when it comes to drawing.

Tools


Fortunately, however, I am pretty good when it comes to what a former manager referred to as "that X stuff" referring to XML and XSLT. This gave me an idea as to how to work around my lack drawing of talent, I would use SVG. Rather than code the SVG by hand, which would have required me to hand code a document for each XML document, I chose to write an XSL style sheet to handle the grunt work. After all, I am a programmer and when in doubt I write code.

However, like all good crafts people it was necessary to gather the tools to do the job. The tools where as follows:

  • The XML document
  • XMLSpy 2006
  • A Web browser that can display SVG graphically, like X-Smiles

Odd list isn’t it. The first two tools are easy to figure-out; however, the last one is kind of curious being that it is a Web browser. What good is a Web browser? Simple, unlike Microsoft Internet Explorer which requires a plug-in to view SVG as graphics, a Mozilla based browser like Firefox and Flock can do it innately.

With the tools out of the way the next step was to decide on which shapes and which font to use for my graphical representation of an XML document. In keeping with my theme of being more likely to impale myself on a Conte Pencil than to produce a usable drawing I settled on the following:

  • Rectangle produced using the rect element
  • Line produced using the line element
  • Tahoma font in the text element

One of the interesting things about SVG is that it is possible to predefine both basic shapes and text. These shape elements and text elements can then be used together to define graphic more complex elements which can then be placed with precision on the document. For example, using the rect (rectangle) and text elements grouped together using the g element creates a single graphical node.

Connecting the elements


Once the individual elements are created the only remaining task is to place and connect the elements. Rather than get into the geometry involved I’ll just resort to showing the XML input document which is shown in Listing A.

Listing A Source XML document


<?xml version="1.0" encoding="UTF-8"?>
<root>
  <child>
    <grandchild/>
  </child>
  <child/>
</root>

While the XML document above is simple as is the graphical representation shown in Figure A, it does prove that the concept is possible.

Figure A

Graphic source XML document

The only question is how to get from Listing A to Figure A? The answer requires a combination of XSLT and XLink. The XSLT is used to create the SVG from the input XML and the XLink is used to create the links between the individual SVG elements. The end result of this endeavor is the XSL style sheet shown in Listing B.

Listing B XSL style sheet


<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:svg="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
  <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes" doctype-public="text/svg"/>
  <xsl:param name="offsetY" select="10"/>
  <xsl:param name="rectX" select="7"/>
  <xsl:param name="rectY" select="5"/>
  <xsl:param name="rectStroke" select="'black'"/>
  <xsl:param name="rectStrokeWidth" select="1"/>
  <xsl:param name="lineStroke" select="'black'"/>
  <xsl:param name="lineStrokeWidth" select="1"/>
  <xsl:param name="textY" select="16"/>
  <xsl:param name="fontFamily" select="'tahoma'"/>
  <xsl:param name="fontSize" select="10"/>
  <xsl:param name="fontWeight" select="'bold'"/>
  <xsl:template match="/">
    <svg xmlns:svg="http://www.w3.org/2000/svg">
      <xsl:comment>SVG tree generated from XML using tree.xslt</xsl:comment>
       <xsl:attribute name="width">100%</xsl:attribute>
       <xsl:attribute name="height">100%</xsl:attribute>
       <xsl:element name="defs">
         <xsl:comment>Shape definition(s)</xsl:comment>
         <xsl:call-template name="rect"/>
           <xsl:comment>Text definition(s)</xsl:comment>
           <xsl:apply-templates mode="text"/>
           <xsl:comment>Graphic definition(s)</xsl:comment>
           <xsl:apply-templates mode="rect"/>
         </xsl:element>
         <xsl:element name="g">
           <xsl:comment>Graphic reference(s)</xsl:comment>
           <xsl:apply-templates select="*" mode="g">
             <xsl:with-param name="offsetLeft" select="0"/>
             <xsl:with-param name="offsetRight" select="100"/>
             <xsl:with-param name="parentX" select="0"/>
             <xsl:with-param name="parentY" select="0"/>
           </xsl:apply-templates>
         </xsl:element>
       </svg>
    </xsl:template>
  <xsl:template match="*" mode="text">
    <xsl:apply-templates select="." mode="def_text"/>
    <xsl:if test="count(./child::*) != 0">
      <xsl:apply-templates mode="text"/>
    </xsl:if>
  </xsl:template>
  <xsl:template match="*" mode="rect">
    <xsl:apply-templates select="." mode="def_g"/>
    <xsl:if test="count(./child::*) != 0">
      <xsl:apply-templates mode="rect"/>
    </xsl:if>
  </xsl:template>
  <xsl:template name="rect">
    <xsl:element name="rect">
      <xsl:attribute name="x">0</xsl:attribute>
      <xsl:attribute name="y">0</xsl:attribute>
      <xsl:attribute name="width">
        <xsl:value-of select="concat(string($rectX),'%')"/>
      </xsl:attribute>
      <xsl:attribute name="height">
        <xsl:value-of select="concat(string($rectY),'%')"/>
      </xsl:attribute>
      <xsl:attribute name="rx">5</xsl:attribute>
      <xsl:attribute name="ry">5</xsl:attribute>
      <xsl:attribute name="fill">white</xsl:attribute>
      <xsl:attribute name="stroke">
        <xsl:value-of select="$rectStroke"/>
      </xsl:attribute>
      <xsl:attribute name="stroke-width">
        <xsl:value-of select="$rectStrokeWidth"/>
      </xsl:attribute>
      <xsl:attribute name="id">rect</xsl:attribute>
    </xsl:element>
  </xsl:template>
  <xsl:template match="*" mode="def_text">
    <xsl:element name="text">
<xsl:attribute name="font-family">
<xsl:value-of select="$fontFamily"/>
</xsl:attribute>
<xsl:attribute name="font-size">
<xsl:value-of select="$fontSize"/>
</xsl:attribute>
<xsl:attribute name="font-weight">
<xsl:value-of select="$fontWeight"/>
</xsl:attribute>
<xsl:attribute name="style">text-anchor: middle</xsl:attribute>
<xsl:attribute name="id"><xsl:value-of select="concat('t',string(generate-id(.)))"/></xsl:attribute>
<xsl:value-of select="name(.)"/>
</xsl:element>
</xsl:template>
<xsl:template match="*" mode="def_g">
<xsl:element name="g">
<xsl:attribute name="id"><xsl:value-of select="concat('g',string(generate-id(.)))"/></xsl:attribute>
<xsl:element name="use">
<xsl:attribute name="xlink:href">#rect</xsl:attribute>
<xsl:attribute name="x">0</xsl:attribute>
<xsl:attribute name="y">0</xsl:attribute>
</xsl:element>
<xsl:element name="use">
<xsl:attribute name="xlink:href"><xsl:value-of select="concat('#t',string(generate-id(.)))"/></xsl:attribute>
<xsl:attribute name="x">
<xsl:value-of select="concat(string($rectX div 2),'%')"/>
</xsl:attribute>
<xsl:attribute name="y">
<xsl:value-of select="$textY"/>
</xsl:attribute>
</xsl:element>
</xsl:element>
</xsl:template>
<xsl:template match="*" mode="g">
<xsl:param name="offsetLeft"/>
<xsl:param name="offsetRight"/>
<xsl:param name="parentX"/>
<xsl:param name="parentY"/>
<xsl:variable name="width" select="$offsetRight - $offsetLeft"/>
<xsl:variable name="center" select="$width div 2"/>
<xsl:variable name="ancestors" select="count(./ancestor::*)"/>
<xsl:variable name="preceding" select="count(./preceding-sibling::*)"/>
<xsl:variable name="following" select="count(./following-sibling::*)"/>
<xsl:variable name="siblings" select="$preceding + $following + 1"/>
<xsl:variable name="area" select="$width div $siblings"/>
<xsl:variable name="x">
<xsl:choose>
<xsl:when test="$preceding = $following">
<xsl:choose>
<xsl:when test="$parentX = 0">
<xsl:value-of select="$center - $rectX div 2"/>
</xsl:when>
                                      <xsl:otherwise>
                                             <xsl:value-of select="$parentX"/>
                                      </xsl:otherwise>
                                </xsl:choose>
                          </xsl:when>
                          <xsl:otherwise>
                                <xsl:value-of select="$area * ($preceding + 0.5) + $offsetLeft"/>
                          </xsl:otherwise>
                   </xsl:choose>
             </xsl:variable>
             <xsl:variable name="y" select="($ancestors + 1) * $offsetY"/>
             <xsl:element name="use">
                   <xsl:attribute name="xlink:href"><xsl:value-of select="concat('#g',string(generate-id(.)))"/></xsl:attribute>
                   <xsl:attribute name="x">
                          <xsl:value-of select="concat(string($x),'%')"/>
                   </xsl:attribute>
                   <xsl:attribute name="y">
                          <xsl:value-of select="concat(string($y),'%')"/>
                   </xsl:attribute>
             </xsl:element>
             <xsl:call-template name="line">
                          <xsl:with-param name="x" select="$x"/>
                          <xsl:with-param name="y" select="$y"/>
                          <xsl:with-param name="parentX" select="$parentX"/>
                          <xsl:with-param name="parentY" select="$parentY"/>
             </xsl:call-template>
             <xsl:if test="count(./child::*) != 0">
                   <xsl:apply-templates mode="g">
                          <xsl:with-param name="offsetLeft" select="$x - $area div 2"/>
                          <xsl:with-param name="offsetRight" select="$x + $area div 2"/>
                          <xsl:with-param name="parentX" select="$x"/>
                          <xsl:with-param name="parentY" select="$y"/>
                   </xsl:apply-templates>
             </xsl:if>
      </xsl:template>
      <xsl:template name="line">
             <xsl:param name="x"/>
             <xsl:param name="y"/>
             <xsl:param name="parentX"/>
             <xsl:param name="parentY"/>
             <xsl:if test="($parentX + $parentY) != 0">
                   <xsl:comment>
                          <xsl:value-of select="concat('Line from (',string($parentX),'%,',string($parentY),'%) to (',string($x),'%,',string($y),'%).')"/>
                   </xsl:comment>
                   <xsl:element name="line">
                          <xsl:attribute name="x1">
                                <xsl:value-of select="concat(string($parentX + $rectX div 2),'%')"/>
                          </xsl:attribute>
                          <xsl:attribute name="y1">
                                <xsl:value-of select="concat(string($parentY + $rectY),'%')"/>
                          </xsl:attribute>
                          <xsl:attribute name="x2">
                                <xsl:value-of select="concat(string($x + $rectX div 2),'%')"/>
                          </xsl:attribute>
                          <xsl:attribute name="y2">
                                <xsl:value-of select="concat(string($y),'%')"/>
                          </xsl:attribute>
                          <xsl:attribute name="stroke">
                                <xsl:value-of select="$lineStroke"/>
                          </xsl:attribute>
                          <xsl:attribute name="stroke-width">
                                <xsl:value-of select="$lineStrokeWidth"/>
                          </xsl:attribute>
                   </xsl:element>
             </xsl:if>
      </xsl:template>
</xsl:stylesheet>

One thing about the style sheet shown in Listing B, there is an obscene number of parameters which are used for fine adjustments to the output document. This result of the transformation using the two documents shown thus far is represented in Listing C.

Listing C Resulting SVG


<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">  <!--SVG tree generated from XML using tree.xslt-->

      <defs>       <!--Shape definition(s)-->

             <rect x="0" y="0" width="7%" height="5%" rx="5" ry="5" fill="white" stroke="black" stroke-width="1" id="rect"/>        <!--Text definition(s)-->

             <text font-family="tahoma" font-size="10" font-weight="bold" style="text-anchor: middle" id="tidroot46943992">root</text>
             <text font-family="tahoma" font-size="10" font-weight="bold" style="text-anchor: middle" id="tidchild64287488">child</text>
             <text font-family="tahoma" font-size="10" font-weight="bold" style="text-anchor: middle" id="tidgrandchild47649632">grandchild</text>
             <text font-family="tahoma" font-size="10" font-weight="bold" style="text-anchor: middle" id="tidchild47755280">child</text>         <!--Graphic definition(s)-->

             <g id="gidroot46943992">
                   <use xlink:href="#rect" x="0" y="0"/>
                   <use xlink:href="#tidroot46943992" x="3.5%" y="16"/>
             </g>
             <g id="gidchild64287488">
                   <use xlink:href="#rect" x="0" y="0"/>
                   <use xlink:href="#tidchild64287488" x="3.5%" y="16"/>
             </g>
             <g id="gidgrandchild47649632">
                   <use xlink:href="#rect" x="0" y="0"/>
                   <use xlink:href="#tidgrandchild47649632" x="3.5%" y="16"/>
             </g>
             <g id="gidchild47755280">
                   <use xlink:href="#rect" x="0" y="0"/>
                   <use xlink:href="#tidchild47755280" x="3.5%" y="16"/>
             </g>
      </defs>
      <g>          <!--Graphic reference(s)-->

             <use xlink:href="#gidroot46943992" x="46.5%" y="10%"/>
             <use xlink:href="#gidchild64287488" x="21.5%" y="20%"/>         <!--Line from (46.5%,10%) to (21.5%,20%).-->

             <line x1="50%" y1="15%" x2="25%" y2="20%" stroke="black" stroke-width="1"/>
             <use xlink:href="#gidgrandchild47649632" x="21.5%" y="30%"/>          <!--Line from (21.5%,20%) to (21.5%,30%).-->

             <line x1="25%" y1="25%" x2="25%" y2="30%" stroke="black" stroke-width="1"/>
             <use xlink:href="#gidchild47755280" x="71.5%" y="20%"/>         <!--Line from (46.5%,10%) to (71.5%,20%).-->

             <line x1="50%" y1="15%" x2="75%" y2="20%" stroke="black" stroke-width="1"/>
      </g>
</svg>

Extreme lengths


Although this might at first glance be one of those times where a developer goes to extreme lengths to avoid a particular task, I’d rather look upon it in a different manner. My friend, Dave, is fond of saying that if the data is tortured enough eventually it will confess. From my experience with working on this problem he is right.