This tutorial shows how to:

Load and work with SVG files using the Snap.svg JavaScript library. Parse SVG drawings exported from Visio 2013 to read data and interact with the shapes.

To help with this I have exported a simple SVG from Visio that I will load into a web page and parse to get its data:

In a Hurry?

Background

Scalable Vector Graphics (SVG) is a W3C standard for displaying vector graphics in the web browser. Using JavaScript you can also interact with SVG files and their DOM, similar to the browser DOM. It is well supported by all browsers, although not that many developers actually know about the file format and its advantages.

Microsoft Visio can export its .vsd/.vsdx drawings and save as SVG images. This makes it easy to put Visio drawings on the web. For a work project I needed to load such images and display data extracted from the shapes in the drawings. I chose to use Snap.svg, which is a modern open source JavaScript library for working with SVG:s (think jQuery for SVG). I soon realized that there is lack of guidance though, both for working with Snap and for parsing Visio SVG:s. So after learning about it I decided to write a guide myself :-) This tutorial guides you in creating a basic framework to get started and perform typical tasks. I’ll begin by going through some of the more interesting points. Jump to the bottom to see the full code.

Note: The code snippets below may be slightly modified or out of context compared to the real code to make it easier to read.

Let’s get started!

Load and initialize drawings with Snap.svg

There are many ways to load SVG:s on a web page. I am using the <object> tag, which is usually the recommended way:

<object id="svg-object" data='VisioFlowchart.svg' type='image/svg+xml' width="100%"></object> 1 < object id = "svg-object" data = 'VisioFlowchart.svg' type = 'image/svg+xml' width = "100%" > < / object >

Using the jQuery load event (instead of the more common ready event) makes sure that the SVG file has been fully loaded before we begin to work with it:

$(window).on("load", function() { ... } 1 $ ( window ) . on ( "load" , function ( ) { . . . }

We initialize Snap.svg and tell it to use the SVG in the <object> tag using its id:

var rootPaper = Snap("#svg-object"); 1 var rootPaper = Snap ( "#svg-object" ) ;

You can have several SVG objects if you need to. Simply run run the initializer for each one in that case.

Tip: It is often useful to log Snap/SVG elements to the browser console during development, for example console.log(rootPaper). Since SVG is just XML the browser log will allow you to explore it, for example to figure out what selectors you need in order to locate the data you need.

Instead of using <object> tags, you can also load the SVG file using Snap.svg itself, and let Snap insert it into the page. Here is an example:

Snap.load(url, function (data) { var svgContainer = Snap("#some-empty-div-on-the-page"); svgContainer.append(data); svgPaper = Snap(svgContainer.node.firstElementChild); } 1 2 3 4 5 Snap . load ( url , function ( data ) { var svgContainer = Snap ( "#some-empty-div-on-the-page" ) ; svgContainer . append ( data ) ; svgPaper = Snap ( svgContainer . node . firstElementChild ) ; }

Note: This method does not produce an identical DOM as the first initialization code. Some minor changes would be required in order to use the latter one with this tutorial.

Different ways of loading SVG:s have their advantages and disadvantages. Often what you choose is just a matter of taste. But I have also found that server settings can be cause trouble. In my case, the SharePoint Online server used response header “X-Download-Options: noopen” which forces non-recognized files to be saved instead of displayed/embedded. Since I couldn’t edit the server settings myself, letting Snap.svg load the file instead solved the problem.

Verify the file format

We can verify that this file is actually a Visio SVG by checking that it contains the proper namespace attribute (xlmns):

$(rootPaper.node.attributes).each( function(){ if( this.value === "http://schemas.microsoft.com/visio/2003/SVGExtensions/" ) { isVisioSvg = true; return false; // breaks the jQuery each() } }); 1 2 3 4 5 6 $ ( rootPaper . node . attributes ) . each ( function ( ) { if ( this . value === "http://schemas.microsoft.com/visio/2003/SVGExtensions/" ) { isVisioSvg = true ; return false ; // breaks the jQuery each() } } ) ;

You’ll notice that above I’m using jQuery even though this is an SVG. You can actually do a lot of SVG parsing with jQuery, but because SVG:s use a separate namespace you may run into trouble. This is why a library such as Snap.svg is useful.

Reading Visio metadata

Microsoft Office files can contain a lot of metadata. Unfortunately it appears that only the “title” and “comment” is included when exporting SVG drawings. Other Visio metadata such as “tags” and “author” are nowhere to be found :-( Getting the title and comment (“desc”) is simple as they are stored at the root level in the xml:

$("#title").text("Title: " + rootPaper.select("title").node.textContent); $("#comment").text("Comment: " + rootPaper.select("desc").node.textContent); 1 2 $ ( "#title" ) . text ( "Title: " + rootPaper . select ( "title" ) . node . textContent ) ; $ ( "#comment" ) . text ( "Comment: " + rootPaper . select ( "desc" ) . node . textContent ) ;

Removing SVG tooltips

Browsers will pick up the <title> tag of SVG elements and display as a tooltip. This can interfere with your own application, so we can remove the node from the SVG. However, the title is the only way to know what kind of Visio shape an SVG element is. Since this is of interest to us, I’m keeping a copy of it in an attribute instead!

rootPaper.selectAll("g").forEach( function(elm, i) { if(elm.select("title") && elm.select("title").node) { var type = elm.select("title").node.textContent; // get the title elm.attr("shapeType", type); // save the title in our own attribute elm.select("title").remove(); // remove the original title } }); 1 2 3 4 5 6 7 rootPaper . selectAll ( "g" ) . forEach ( function ( elm , i ) { if ( elm . select ( "title" ) && elm . select ( "title" ) . node ) { var type = elm . select ( "title" ) . node . textContent ; // get the title elm . attr ( "shapeType" , type ) ; // save the title in our own attribute elm . select ( "title" ) . remove ( ) ; // remove the original title } } ) ;

Seting up event handlers on Visio shapes

Event handlers in Snap are very similar to events in jQuery. First we iterate though the SVG elements, using the selector “g[id^=shape]” to match only Visio shapes:

rootPaper.selectAll("g[id^=shape]").forEach( function(elm, i) { 1 rootPaper . selectAll ( "g[id^=shape]" ) . forEach ( function ( elm , i ) {

Then simply hook up event handlers on each element (elm) just as in jQuery:

elm.click(function(evt) {...}); elm.mouseover(function(evt) {...}); elm.mouseout(function(evt) {...}); 1 2 3 elm . click ( function ( evt ) { . . . } ) ; elm . mouseover ( function ( evt ) { . . . } ) ; elm . mouseout ( function ( evt ) { . . . } ) ;

Just like in HTML, events bubble upwards through the hierarchy. In this case, the Visio “page” is at the top (or bottom if you like). We can easily stop event from bubbling:

evt.preventDefault(); evt.stopPropagation(); 1 2 evt . preventDefault ( ) ; evt . stopPropagation ( ) ;

Drawing a box around clicked shapes

When clicking a shape we can draw a box around it to indicate that it is selected. First, use Snap.svg to create a new rectangle element:

selectionRect = shape.paper.rect(shape.x, shape.y, shape.width, shape.height); 1 selectionRect = shape . paper . rect ( shape . x , shape . y , shape . width , shape . height ) ;

The new rectangle will not be visible unless we set some SVG style attributes. These are similar to HTML/CSS attributes, sometimes even named the same:

selectionRect.attr({fill: "none", stroke: "red", strokeWidth: 1}); 1 selectionRect . attr ( { fill : "none" , stroke : "red" , strokeWidth : 1 } ) ;

“fill” should be evident. “stroke” refers to the border around an SVG element.

Setting “pointerEvents” to none means that the mouse will not be able to click or hover the box like normal elements:

selectionRect.attr({pointerEvents: "none"}); 1 selectionRect . attr ( { pointerEvents : "none" } ) ;

Now we can get boxes around the shapes! Unfortunately this is not as precise as you may be used to when working with CSS, so the box might look slightly off center. One reason for this is that getBBox() does not account for strokeWidth. I have yet to find a way to figure out the strokeWidth (especially in more complex shape-groups). You can try to set the “shapeRendering” attribute to get a better result:

selectionRect.attr({shapeRendering: "geometricPrecision"}); 1 selectionRect . attr ( { shapeRendering : "geometricPrecision" } ) ;

This is a hint to the browser how we want shapes to be rendered. In this example, “geometricPrecision” appears to give best result. Also try “optimizeSpeed” and “crispEdges”.

Tip: Another way to “fix” the rendering problems would be to skip SVG altogether and use an HTML/CSS div instead. This gives a crisp pixel perfect box. Simply add a div and style it as needed, then position it on top of the SVG element:

<div id="selection" style="border: 1px solid black; display: inline-block; position: absolute; z-index: 100; pointer-events: none;"></div> ... $("#selection").css("top", top + this.node.getBoundingClientRect().top); $("#selection").css("left", left + this.node.getBoundingClientRect().left); $("#selection").css("width", this.node.getBoundingClientRect().width); $("#selection").css("height", this.node.getBoundingClientRect().height); 1 2 3 4 5 6 < div id = "selection" style = "border: 1px solid black; display: inline-block; position: absolute; z-index: 100; pointer-events: none;" > < / div > . . . $ ( "#selection" ) . css ( "top" , top + this . node . getBoundingClientRect ( ) . top ) ; $ ( "#selection" ) . css ( "left" , left + this . node . getBoundingClientRect ( ) . left ) ; $ ( "#selection" ) . css ( "width" , this . node . getBoundingClientRect ( ) . width ) ; $ ( "#selection" ) . css ( "height" , this . node . getBoundingClientRect ( ) . height ) ;

Highlighting hovered shapes

When hovering a shape with the mouse we want some kind of indication. One way is to set the shape’s opacity to 50%. We need to set both the fill and stroke opacity attributes:

hoveredShape.attr({fillOpacity: "0.5", strokeOpacity: "0.5"}); 1 hoveredShape . attr ( { fillOpacity : "0.5" , strokeOpacity : "0.5" } ) ;

Don’t forget to reset the opacity to 1 when leaving the shape!

This works fine in our example, where the background underneath the shape is plain. If there had been a pattern though, it would not look as good. A better way would be to clone the shape and display the clone semi-transparent above the original. Unfortunately I have not gotten this to work properly, because Visio shapes are complex elements put together of multiple parts.

Parsing Visio shapes

When clicking a Visio shape we want to extract the data contained within the shape. For this, I made a function that parses an xml element and returns an object with the data.

First, make sure that this is a shape element:

var elementType = elm.node.attributes["v:groupContext"].value; if( elementType === "shape" ) { ... } 1 2 var elementType = elm . node . attributes [ "v:groupContext" ] . value ; if ( elementType === "shape" ) { . . . }

Note: You may be wondering how I knew to look for “v:groupContext” and similar? I have not found any official documentation from Microsoft, but since SVG is an XML based format I simply inspected them in an ordinary text editor (in my case Notepad2) to figure out how they are constructed.

The shape type tells us what kind of Visio shape we are dealing with:

console.log( elm.node.attributes["shapeType"].value ); 1 console . log ( elm . node . attributes [ "shapeType" ] . value ) ;

Note: “shapeType” is the attribute we previously created when we removed the tooltip.

Each shape has a id:

console.log( elm.node.attributes["id"].value ); 1 console . log ( elm . node . attributes [ "id" ] . value ) ;

Note: This id is unique in the drawing, but if you edit your original Visio drawing and export it to SVG again, the id might change. There is also an attribute called “v:mID” that you can use to identify shapes. In my experience, this appears to be more reliable to use.

To get the shape’s position we query the getBBox() function:

console.log( elm.getBBox().x ); console.log( elm.getBBox().y ); console.log( elm.getBBox().width ); console.log( elm.getBBox().height ); 1 2 3 4 console . log ( elm . getBBox ( ) . x ) ; console . log ( elm . getBBox ( ) . y ) ; console . log ( elm . getBBox ( ) . width ) ; console . log ( elm . getBBox ( ) . height ) ;

Note: Shape position is relative to the SVG coordinates, not the screen/browser!

Get the text inside the shape:

if(elm.select("desc") && elm.select("desc").node) { console.log( elm.select("desc").node.textContent ); } 1 2 3 if ( elm . select ( "desc" ) && elm . select ( "desc" ) . node ) { console . log ( elm . select ( "desc" ) . node . textContent ) ; }

In Visio, you can right click on a shape and select properties to view and edit the “Custom Properties” of the shape. This is how to extract them:

if( elm.select("custProps") ) { var visioProps = elm.select("custProps").selectAll("cp"); for(var i=0; i<visioProps.length; i++) { if( visioProps[i].node.attributes["v:nameU"] && visioProps[i].node.attributes["v:val"] ) { console.log( "Key: " + visioProps[i].node.attributes["v:nameU"].value ); console.log( "Value: " + visioProps[i].node.attributes["v:val"].value ); } } } 1 2 3 4 5 6 7 8 9 if ( elm . select ( "custProps" ) ) { var visioProps = elm . select ( "custProps" ) . selectAll ( "cp" ) ; for ( var i = 0 ; i < visioProps . length ; i ++ ) { if ( visioProps [ i ] . node . attributes [ "v:nameU" ] && visioProps [ i ] . node . attributes [ "v:val" ] ) { console . log ( "Key: " + visioProps [ i ] . node . attributes [ "v:nameU" ] . value ) ; console . log ( "Value: " + visioProps [ i ] . node . attributes [ "v:val" ] . value ) ; } } }

There are also “User Defs” which are almost identical to work with as custom properties.

Resizing the SVG <object> container

We don’t know the aspect ratio of the drawing in advance. Assuming that we want to utilize the full width of the page, we can’t set the container height until we have the aspect ratio.

A Visio drawing can contain several pages. Only one page can be exported to SVG, but the concept of a page remains as the root element. We can get the bounding box of this Visio page:

rootPaper.selectAll("g").forEach(function(elm){ if(elm.node.attributes["v:groupContext"] && elm.node.attributes["v:groupContext"].value === "foregroundPage") { visioPage = elm.node; x = visioPage.getBBox().x; y = visioPage.getBBox().y; w = visioPage.getBBox().width; h = visioPage.getBBox().height; } } 1 2 3 4 5 6 7 8 9 rootPaper . selectAll ( "g" ) . forEach ( function ( elm ) { if ( elm . node . attributes [ "v:groupContext" ] && elm . node . attributes [ "v:groupContext" ] . value === "foregroundPage" ) { visioPage = elm . node ; x = visioPage . getBBox ( ) . x ; y = visioPage . getBBox ( ) . y ; w = visioPage . getBBox ( ) . width ; h = visioPage . getBBox ( ) . height ; } }

Using this we can calculate the aspect ratio and change the height of the <object> container to show as much as possible of the drawing taking into account this aspect ratio:

$("#svg-object").height( $("#svg-object").width() / (w/h) ); 1 $ ( "#svg-object" ) . height ( $ ( "#svg-object" ) . width ( ) / ( w / h ) ) ;

Zooming in on the drawing

Visio adds a unnecessary empty border around the page. Let zoom in the drawing to better utilize the available space!

We can make a new viewBox to shows as much as possible of the drawing. I’m adding a small margin here, but it is quite inaccurate because getBBox() does not account for strokeWidth. Using a relative marginY appears to give descent result for my needs, but you may need to try something different yourself.

var marginX = 1; var marginY = (w/h); var newViewBox = (x-marginX) + " " + (y-marginY) + " " + (w+marginX*2) + " " + (h+marginY*2); 1 2 3 var marginX = 1 ; var marginY = ( w / h ) ; var newViewBox = ( x - marginX ) + " " + ( y - marginY ) + " " + ( w + marginX * 2 ) + " " + ( h + marginY * 2 ) ;

Note: The viewbox is a property of SVG that specifies what part of the drawing to display. The actual drawing can extend beyong this viewbox so you would have to pan the drawing to view it all.

Now we can use a new viewBox to resize the SVG to make it fill the entire object canvas:

rootPaper.animate({viewBox: newViewBox}, 300, mina.easeinout); 1 rootPaper . animate ( { viewBox : newViewBox } , 300 , mina . easeinout ) ;

Here I am also using Snap’s animation feature for a nice effect when loading the page. 300 is the animation duration, “mina.easeinout” is the animation easing. Other easings are: easeinout, linear, easein, easeout, backin, backout, elastic & bounce.

The complete code

I hope you enjoyed this tutorial and learned something new. Here is the complete, fully commented code, including the html/css page. Again, you can get this together with the sample SVG on GitHub.