Rendering Hierarchies with XSLT and SVG – The Basics
When you want to display an XML document as a visual tree, the first question is how the data is mapped onto a two‑dimensional canvas. A common misconception is that all trees are balanced, so you could simply split the canvas width by the number of nodes at each depth level and stack the levels vertically. In practice, XML documents are rarely symmetric. Some branches may have dozens of descendants, while others might be shallow. A layout that ignores this variation will appear lopsided, with the root node drifting off‑center when the tree is skewed.
A good algorithm for tree rendering must therefore respect branch breadth. The most straightforward approach is to perform a single traversal of the document, assigning coordinates to each node as you go. This is efficient, but it needs a way to remember information about siblings that you have not yet processed. One solution is to embed bookkeeping data into the SVG output itself, using a dedicated namespace so that it does not clash with standard SVG attributes. Although this adds a small amount of clutter to the final SVG, it keeps the stylesheet stateless and simple.
An alternative strategy is to perform two passes. The first pass calculates the width of each subtree, while the second pass assigns absolute coordinates based on those widths. The two‑pass approach produces a more balanced result, but it requires the stylesheet to store temporary values in variables or in the result tree before the final layout. Because XSLT 1.0 does not allow arbitrary variable assignment after a value is set, the implementation relies on result‑tree fragments that are later converted into node sets. Modern processors offer convenient extensions for this conversion, but portability can become an issue if you target older processors or those that lack full XSLT 1.1 support.
Whatever the chosen method, the output is an SVG file that shows the tree’s skeleton: circles or rectangles for nodes, and lines for edges. In the examples below, the tree is drawn using rounded rectangles for nodes and thick, curved lines for edges. Each node’s position is computed by a combination of horizontal and vertical offsets that depend on the node’s depth and its sibling count.
The key takeaway from this overview is that a naive, one‑step division of the canvas rarely produces a readable diagram. Instead, you should either embed state in the SVG with a namespaced attribute or perform a preparatory pass to measure subtree widths. The next sections show how to implement each strategy in detail.
Single‑Pass Layout: One Pass, Embedded Metadata
In the single‑pass approach, the stylesheet walks the XML tree once, emitting an SVG node for each XML element. To calculate the x‑coordinate of a node, you need to know how many sibling nodes exist to its left and right. Instead of recomputing this count every time, you can pass the current depth as a parameter and calculate the horizontal position on the fly. For example, a simple recursive template might look like this:
The horizontal offset, $x, is calculated by multiplying the depth by a fixed width, such as 150 pixels. The vertical offset, $y, can be set by a stack counter that increments each time a new child is rendered. Because each node emits its own transformation matrix, the SVG elements stay in the same relative position regardless of the number of siblings.
However, the single‑pass method suffers from two drawbacks. First, the node’s x‑position is determined solely by its depth, not by the positions of its children. If the root has a child with many descendants, the root’s circle will appear shifted to the right, giving the diagram an unbalanced look. Second, since the algorithm does not store any subtree width information, you cannot easily size the node’s container to fit the text it contains. The node’s label might be clipped or overlap adjacent nodes if it’s too long.
To mitigate the first problem, you can embed a count attribute in each <g> element that records the number of descendants below that node. The attribute can be written in a dedicated namespace like xmlns:meta="http://example.org/meta". By adding meta:count="{count(descendant::*)}" to the <g>, later templates can read this value to adjust the parent’s position. This approach keeps the SVG self‑contained: anyone who opens the file can view the tree without needing the original stylesheet.
The resulting diagram looks like Figure 9‑15. The tree skeleton is visible, but the root node is noticeably off‑center. Despite its simplicity, the single‑pass method is useful when you want a quick visual test or when the data set is small and balanced enough that skewing is negligible.
Figure 9‑15 – A simple tree rendered with a single pass. The diagram uses circles for nodes and curved lines for edges, with the root node slightly off‑center due to uneven branch sizes. Each node is represented by an <g> element containing a <circle> and a <text> child.
The calculate-width() template would recursively sum child widths and return the result. In practice, many processors provide an exsl:node-set() function that turns a result‑tree fragment into a node set, making it possible to store the width values in a temporary tree that the second pass can read. This method does not pollute the SVG with custom attributes; all layout data stays within the stylesheet.
Figure 9‑16 shows the same XML input rendered with the balanced two‑pass algorithm. The diagram looks more symmetrical, and the root node sits at the center of the page. The nodes are still small circles, but the spacing now reflects the true breadth of each branch. Because the width calculation takes place before any output is written, the final SVG is cleaner and easier to maintain.
Figure 9‑16 – The balanced tree from the two‑pass algorithm. The diagram uses rectangles for nodes and straight lines for edges, with the root node centered and each level evenly spaced. The subtree widths are calculated ahead of time, ensuring a harmonious layout.
If you prefer to stay within XSLT 2.0, you can use the math:floor() and format-number() functions to approximate the width, though this may not be as accurate. Another option is to rely on CSS styles in the SVG: set the text-anchor to middle and wrap the <text> in a <tspan> with x="0" and dy="0.35em" to vertically center the text. The container’s size can be left at a fixed value if the labels are short, or you can use min-width properties in the SVG’s internal stylesheet.
Beyond labels, you can tweak the visual appearance of the tree with CSS. Changing the stroke color of edges to a muted gray, adding a subtle drop shadow to nodes, or applying a gradient fill can make the diagram more appealing. The stylesheet can output a <style> element inside the <svg> to keep all styling together:
When you need to present a large dataset, you may want to limit the depth of the tree or collapse branches that exceed a certain node count. You can add a parameter to the match="node()" template that checks the depth against a user‑supplied limit. If the limit is reached, the template emits a simplified node that indicates that more data exists but is hidden.
All these enhancements keep the core layout logic unchanged; they simply add visual polish and usability. By keeping the stylesheet modular - one section for geometry, another for styling - you can swap out the visual theme without touching the tree‑drawing logic.
Customizing Connections and Handling Portability Issues
The examples above use curved lines for edges, but straight lines often look cleaner, especially when the tree is tall and narrow. The stylesheet can be extended by overriding a drawConnections template. Instead of the default drawCurvedConnections, you replace it with a simple drawStraightConnections that emits <line> elements between parent and child coordinates.
Figure 9‑17 shows the tree rendered with straight connections. The diagram is easier to read when many nodes share a common ancestor, as the straight lines avoid the visual clutter of multiple curves.
Portability remains a concern. Some XSLT processors do not support the exsl:node-set() function, which is critical for converting result‑tree fragments into node sets. If you target a processor that lacks this function, you can replace it with a custom extension function or rewrite the stylesheet to use apply-templates for the second pass, passing the width values as parameters instead of storing them in a temporary tree.
Another common portability hurdle is the reliance on Java extensions for mathematical functions like max(). While XSLT 2.0 provides a math:max() function, XSLT 1.0 does not. If you need to run the stylesheet on a pure XSLT 1.0 processor, you can implement max() using a recursive template that compares two numbers and returns the larger one. This keeps the stylesheet free from language‑specific dependencies.
Finally, filtering irrelevant nodes is essential for realistic datasets. The generic match pattern match="node()" will traverse every element, including whitespace and comment nodes. Replace it with more selective patterns like match="bookstore/book" or match="*[contains(@class,'important')]" to focus the diagram on meaningful parts of the XML hierarchy. This reduces the number of nodes rendered and improves performance, especially for large documents.
By combining these techniques - custom connection styles, portable math functions, and selective matching - you can adapt the tree‑drawing algorithm to a wide range of XML data sets and XSLT processors.
Further Reading and Advanced Graph Drawing Resources
If you want to dig deeper into graph‑drawing theory, consider the book Graph Drawing: Algorithms for the Visualization of Graphs by Di Battista, Eades, Tamassia, and Tollis. The text covers a range of algorithms from force‑directed layouts to tree‑specific approaches like the Reingold–Tilford method. Although it is mathematically dense, it offers insight into why certain layout strategies work and how they can be optimized for different data characteristics.
For hands‑on XSLT solutions, the XSLT Cookbook by Sal Mangano is an excellent resource. The book contains hundreds of recipes covering string manipulation, mathematical operations, and SVG generation. The recipe for tree rendering you just explored is just one example. The cookbook also includes techniques for debugging stylesheets, testing with sample XML data, and extending XSLT with Java or other languages.
Web‑based resources like XML.com host a variety of XSLT tutorials and examples. The XML.com article on tree rendering provides a side‑by‑side comparison of the single‑pass and two‑pass algorithms, complete with full source code. While the examples are a bit dated, the core concepts remain relevant, and the source files are available for download and modification.
For developers looking to integrate tree diagrams into modern web applications, consider leveraging JavaScript libraries that can consume the SVG output from the XSLT process. Libraries such as D3.js or Cytoscape.js can further style the diagram, enable interactivity, and allow users to collapse or expand branches on the fly. By producing a clean, well‑structured SVG from XSLT, you can hand off the visual backbone to the JavaScript layer for enhanced user experience.
No comments yet. Be the first to comment!