Search code examples
xmlxsltsvggraphml

XSLT stylesheet to convert Graphml to SVG


I am having more trouble than expected to find an XSLT stylesheet that converts a simple Graphml document to an SVG diagram. I've searched quite widely, and so far I have only partial success. Here's my input Graphml file:

<?xml version="1.0" encoding="UTF-8"?>
<graphml xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<graph id="G" edgedefault="directed">
  <node id="d1e2"/>
  <node id="d1e4"/>
  <node id="d1e7"/>
  <node id="d1e9"/>
  <node id="d1e11"/>
  <node id="d1e14"/>
  <node id="d1e17"/>
  <node id="d1e21"/>
  <node id="d1e23"/>
  <node id="d1e26"/>
  <node id="d1e29"/>
  <node id="d1e33"/>
  <node id="d1e35"/>
  <node id="d1e38"/>
  <node id="d1e41"/>
  <edge source="d1e2" target="d1e4"/>
  <edge source="d1e2" target="d1e7"/>
  <edge source="d1e7" target="d1e9"/>
  <edge source="d1e9" target="d1e11"/>
  <edge source="d1e9" target="d1e14"/>
  <edge source="d1e9" target="d1e17"/>
  <edge source="d1e7" target="d1e21"/>
  <edge source="d1e21" target="d1e23"/>
  <edge source="d1e21" target="d1e26"/>
  <edge source="d1e21" target="d1e29"/>
  <edge source="d1e7" target="d1e33"/>
  <edge source="d1e33" target="d1e35"/>
  <edge source="d1e33" target="d1e38"/>
  <edge source="d1e33" target="d1e41"/>
</graph>
</graphml>

and here's my stylesheet (from http://www.svgopen.org/2003//papers/ComparisonXML2SVGTransformationMechanisms/index.html#S2). Note that I have temporarily disabled edge processing to focus on the placement of the nodes:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns="http://www.w3.org/2000/svg">
  <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>

  <!-- for a 'graph' element, creates an 'svg' element -->
  <xsl:template match="graph">
  <!-- first give a CSS reference for the generated SVG -->
    <xsl:processing-instruction name="xml-stylesheet">type="text/css" href="default.css"</xsl:processing-instruction> 
    <svg>
      <!-- defs section for the arrow -->
      <!-- ... -->
      <!-- recurse 'node' elements of the graph to find graph root -->
      <xsl:apply-templates select="node"/>
    </svg>
  </xsl:template>

  <!-- recurse 'node' element to find graph root -->
  <xsl:template match="node">
    <!-- check if the first 'edge' has current 'node' as target -->
    <xsl:apply-templates select="../edge[1]">
       <xsl:with-param name="n" select="."/>
    </xsl:apply-templates>
  </xsl:template>

  <!-- check if a 'node' ($n) is a target of the current 'edge' -->
  <xsl:template match="edge">
    <xsl:param name="n">null</xsl:param>
    <!-- if the 'node' is not a target of the current 'edge' -->
    <xsl:if test="not(@target=$n/@id)">
      <!-- advance to the next edge -->
      <xsl:apply-templates select="following-sibling::edge[position()=1]">
        <xsl:with-param name="n" select="$n"/>
      </xsl:apply-templates>
      <!-- if all edges have been queried  -->
      <xsl:if test="not(following-sibling::edge[position()=1])">
        <!-- the 'node' ($n) is the root, create it -->
        <xsl:call-template name="create-node">
          <xsl:with-param name="n" select="$n"/>
        </xsl:call-template>
      </xsl:if>
    </xsl:if>
  </xsl:template>

  <!-- transform a 'node' to SVG and recurse through its children -->
  <xsl:template name="create-node">
    <xsl:param name="n">null</xsl:param>
    <xsl:param name="level">0</xsl:param>
    <xsl:param name="count">0</xsl:param>
    <xsl:param name="edge">null</xsl:param>
    <xsl:param name="x1">0</xsl:param>
    <xsl:param name="y1">0</xsl:param>
    <!-- some helpers -->
    <xsl:variable name="side" select="1-2*($count mod 2)"/>
    <xsl:variable name="x" select="$level*150"/>
    <xsl:variable name="y" select="$y1 - 50+$side*ceiling($count div 2)*150"/>
    <!-- create the 'node' itself and position it -->
    <g class="node">
      <rect x="{$x}" y="{$y}" width="100" height="100"/>
      <text text-anchor="middle" x="{$x+50}" y="{$y+55}">
        <xsl:value-of select="$n/@id"/>
      </text>
    </g>
    <!-- if there is an 'edge' ($edge) draw it -->
    <xsl:if test="$edge!='null'">
      <!-- the 'edge' position goes from previous 'node' position to $n one -->
      <!--
      <line class="edge" x1="{$x1}" y1="{$y1}" x2="{$x}" y2="{$y+50}">
        <xsl:attribute name="style">marker-end:url(#arrow)</xsl:attribute>
      </line>
      -->
    </xsl:if>
    <!-- now that the 'node' is created, recurse to children through edges -->
    <xsl:call-template name="query-edge">
      <xsl:with-param name="edge" select="$n/../edge[@source=$n/@id][1]"/>
      <xsl:with-param name="x1" select="$x+100"/>
      <xsl:with-param name="y1" select="$y+50"/>
      <xsl:with-param name="n" select="$n"/>
      <!-- going to the upper level, increment level -->
      <xsl:with-param name="level" select="$level+1"/>
      <!-- going to the first child, set counter to 0 -->
      <xsl:with-param name="count" select="0"/>
    </xsl:call-template>
  </xsl:template>

  <!-- recurse a 'node' ($n) edges to find 'node' children -->
  <xsl:template name="query-edge">
    <xsl:param name="edge">null</xsl:param>
    <xsl:param name="x1">0</xsl:param>
    <xsl:param name="y1">0</xsl:param>
    <xsl:param name="n">null</xsl:param>
    <xsl:param name="level">0</xsl:param>
    <xsl:param name="count">0</xsl:param>
    <xsl:variable name="target" select="$edge/@target"/>
    <!-- if there is an 'edge' -->
    <xsl:if test="$edge!='null'">
      <!-- go down the tree, create the 'node' of the 'edge' target -->
      <xsl:call-template name="create-node">
        <xsl:with-param name="n" select="$edge/../node[@id=$target]"/>
        <xsl:with-param name="level" select="$level"/>
        <xsl:with-param name="count" select="$count"/>
        <xsl:with-param name="edge" select="$edge"/>
        <xsl:with-param name="x1" select="$x1"/>
        <xsl:with-param name="y1" select="$y1"/>
      </xsl:call-template>
      <!-- go to the next 'edge' that has also the 'node' ($n) has source -->
      <xsl:variable name="next-edge" select="$edge/following-sibling::edge[position()=1][@source=$n/@id]"/>
      <xsl:call-template name="query-edge">
       <xsl:with-param name="edge" select="$next-edge"/>
       <xsl:with-param name="x1" select="$x1"/>
       <xsl:with-param name="y1" select="$y1"/>
       <xsl:with-param name="n" select="$n"/>
       <xsl:with-param name="level" select="$level"/>
       <!-- next 'edge', increment counter -->
       <xsl:with-param name="count" select="$count+1"/>
     </xsl:call-template>
    </xsl:if>
  </xsl:template>
</xsl:stylesheet>

When I run this through Saxon, I get the following SVG file:

    <?xml version="1.0" encoding="UTF-8"?>
   <?xml-stylesheet type="text/css" href="default.css"?><svg xmlns="http://www.w3.org/2000/svg">
   <g class="node">
      <rect x="0" y="-50" width="100" height="100"/>
      <text text-anchor="middle" x="50" y="5">d1e2</text>
   </g>
   <g class="node">
      <rect x="150" y="-50" width="100" height="100"/>
      <text text-anchor="middle" x="200" y="5">d1e4</text>
   </g>
   <g class="node">
      <rect x="150" y="-200" width="100" height="100"/>
      <text text-anchor="middle" x="200" y="-145">d1e7</text>
   </g>
   <g class="node">
      <rect x="300" y="-200" width="100" height="100"/>
      <text text-anchor="middle" x="350" y="-145">d1e9</text>
   </g>
   <g class="node">
      <rect x="450" y="-200" width="100" height="100"/>
      <text text-anchor="middle" x="500" y="-145">d1e11</text>
   </g>
   <g class="node">
      <rect x="450" y="-350" width="100" height="100"/>
      <text text-anchor="middle" x="500" y="-295">d1e14</text>
   </g>
   <g class="node">
      <rect x="450" y="-50" width="100" height="100"/>
      <text text-anchor="middle" x="500" y="5">d1e17</text>
   </g>
</svg>

This is pretty good, but it has lost all of the nodes after the d1e17 node, and I can't work out why.

Can anyone spot the bug in the stylesheet? Or does anyone have a better stylesheet for this purpose?

Thanks for any help, Martin


Solution

  • If you transform your flat list of nodes into a tree first, then you can use the axes to find information about the structure.

    Try something like this:

    <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" version="2.0">
      <xsl:output method="xml" omit-xml-declaration="yes" encoding="UTF-8" indent="yes" />
    
      <xsl:template match="/graphml/graph">
    
        <!-- Find the root ID -->
        <xsl:variable name="rootId">
          <xsl:for-each select="node">
            <xsl:variable name="nodeId" select="@id"/>
            <xsl:if test="not(../edge[@target=$nodeId])">
              <xsl:value-of select="$nodeId"/>
            </xsl:if>
          </xsl:for-each>
        </xsl:variable>
    
        <!-- Turn flat list into a tree -->
        <xsl:variable name="tree">
          <xsl:apply-templates select="node[@id=$rootId]"/>
        </xsl:variable>
    
        <!-- Turn tree into a flat list of svg elements -->
        <svg>
          <g transform="translate(50 50)">
            <xsl:apply-templates select="$tree"/>
          </g>
        </svg>
      </xsl:template>
    
      <xsl:template match="node">
        <xsl:variable name="nodeId" select="@id"/>
        <xsl:variable name="childIds" select="//edge[@source=$nodeId]/@target"/>
        <treeNode id="{@id}">
          <xsl:apply-templates select="//node[@id=$childIds]"/>
        </treeNode>
      </xsl:template>
    
      <xsl:template match="svg:treeNode">
        <xsl:variable name="level" select="count(ancestor::*)"/>
        <xsl:variable name="leafChildren" select="count(descendant::*[not(descendant::*)])"/>
        <xsl:variable name="earlierChildren" select="count(preceding::*[not(descendant::*)])"/>
        <xsl:variable name="x" select="100 * $level"/>
        <xsl:variable name="y" select="50 * $earlierChildren + 25 * max((0, $leafChildren - 1))"/>
        <g class="node">
          <circle cx="{$x}" cy="{$y}" r="10"/>
          <text x="{$x - 10}" y="{$y + 25}"><xsl:value-of select="@id"/></text>
    
          <!-- Draw line to parent -->
          <xsl:if test="$level != 0">
            <xsl:variable name="parentLevel" select="$level - 1"/>
            <xsl:variable name="parentLeafChildren" select="count(../descendant::*[not(descendant::*)])"/>
            <xsl:variable name="parentEarlierChildren" select="count(../preceding::*[not(descendant::*)])"/>
            <xsl:variable name="parentX" select="100 * $parentLevel"/>
            <xsl:variable name="parentY" select="50 * $parentEarlierChildren + 25 * max((0, $parentLeafChildren - 1))"/>
            <path d="M {$parentX} {$parentY} C {$parentX + 50} {$parentY}, {$x - 50} {$y}, {$x} {$y}" stroke="black" fill="transparent" stroke-width="2"/>
          </xsl:if>
        </g>
        <xsl:apply-templates select="child::*"/>
      </xsl:template>
    
    </xsl:stylesheet>
    

    Result ist this:

    screenshot of generated graph