19

I have an .svg file and want to embed it in the svg strucure of my d3-graphic.

I also need to reference all paths/polygons attached to g-elements via an id of certain g elements.

I tried different ways to to embed and reference the svg (g's), but it didn't work for some reasons:

(1) first attempt

// Firefox displays my svg but when i open it with Chrome the svg     
//is not displayed (just default placeholder icon)
// I can't reference the svg-g id's with d3.select functions. 
main_chart_svg
        .append("svg:image")
        .attr("xlink:href", "mySVGpicture.svg")
        .attr("x", "50")
        .attr("y", "50")
        .attr("width", "500")
        .attr("height", "500");  

main_chart_svg.select("#anIdWhichIsInTheSvgFile").remove(); //// This doesn't work

(2) second attempt

// This displays the svg but -not- inside my main-chart-svg. I want to keep the graphic   
//things seperate to html-stuff.
d3.xml("mySVGpicture.svg", "image/svg+xml", function(xml) {
        document.body.appendChild(xml.documentElement);
    });
//----------or----------//
    d3.select("body")
        .append("object")
        .attr("data", "mySVGpicture.svg")
        .attr("width", 500)
        .attr("height", 500)
        .attr("type", "image/svg+xml");

d3.select("#anIdThatIsInTheSvgFile").remove(); //does not work.

(3) The svg-file looks something like that:

<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 15.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="400px"
     height="400px" viewBox="0 0 400 400" enable-background="new 0 0 400 400" xml:space="preserve">
    <g id="anIdWhichIsInTheSvgFile">
        <g id="DE">
            <path fill="#FEDCBD" d="M215.958,160.554c0,0-0.082, ... ,1.145
                l0.865,0.656L215.958,160.554z"/>
            <path fill="#FEDCBD" d="M208.682,155.88l1.246,1.031c0,0,0.191,0.283, ... ,0.572L208.682,155.88z"/>
            <polygon fill="#FEDCBD" points="190.76,153.007 190.678, ... ,153.938 
                191.427,152.906"/>
            <polygon fill="#FEDCBD" points="170.088,151.015 169.888,150.067 169.125,150.075 168.846,150.836 169.521,151.588"/>
            <polygon fill="#FEDCBD" points="168.953,152.067 168.188,151.505 168.674,152.639"/>
            <polygon fill="#FEDCBD" points="170.105,153.099 170.666,152.052 170.002,152.248"/>
    </g>
    <g id="anIdThatIsInTheSvgFile">
    ...
    </g>
</svg>
Seb Waynee
  • 325
  • 1
  • 2
  • 12

2 Answers2

39

There's a better solution.

I hadn't realized that the d3.xml() function returns a read-in xml file as a document fragment (as opposed to converting it to JSON). In other words, it returns a ready-made DOM tree that can be inserted within you main document's DOM wherever you need it.

Knowing that (and knowing that stand-alone SVG files are also XML files), this is likely the most reliable approach for adding external SVG content to your webpage:

d3.xml("http://upload.wikimedia.org/wikipedia/commons/a/a0/Circle_-_black_simple.svg", 
        function(error, documentFragment) {

    if (error) {console.log(error); return;}

    var svgNode = documentFragment
                .getElementsByTagName("svg")[0];
    //use plain Javascript to extract the node

    main_chart_svg.node().appendChild(svgNode);
    //d3's selection.node() returns the DOM node, so we
    //can use plain Javascript to append content

    var innerSVG = main_chart_svg.select("svg");

    innerSVG.transition().duration(1000).delay(1000)
          .select("circle")
          .attr("r", 100);

});

Fiddle here: http://jsfiddle.net/J8sp3/4/

Notice in particular that (a) I am adding the content to an existing SVG, and (b) I am not deleting any of that SVG's current content.

Of course, you can also add the SVG to a <div> or any other element that can contain it: http://jsfiddle.net/J8sp3/3/

This method should work in any browser that supports SVG. I even tried it in IE8, and the DOM structure is correct even if IE8 doesn't know how to draw circles and rectangles!

@Seb, please unselect the previous answer at pick this one as the accepted answer.

AmeliaBR
  • 27,344
  • 6
  • 86
  • 119
  • Is there a way to loop through this code if I want to use several different standalone ```.svg``` files (for example, reading in this black circle, then a red one, etc.)? – Amyunimus Oct 24 '14 at 00:00
  • @Amyunimus: You can call multiple different file-reading commands, and pass each one a callback to extract the relevant document fragment and add it to your main DOM. If you want to set a global callback function to run after all the files have been downloaded and parsed, however, you should probably use [queue.js](https://github.com/mbostock/queue) to handle your callbacks. – AmeliaBR Oct 24 '14 at 04:18
  • @AmeliaBR do you know if this trick also works in nodejs...? I'm trying to manipulate an svg using nodejs but even loading the dom using d3 is difficult. – JPCF Aug 02 '17 at 09:11
  • @JPCF That really depends what other libraries you have running in Node, and whether they fully replace the DOM methods that D3 uses under the hood. – AmeliaBR Aug 03 '17 at 17:16
  • 1
    This does not work anymore with the introduction of d3 v5, since d3 switched to using promises: d3.xml("file.xml").then(function(xml) { console.log(xml); }); – luQ Apr 04 '18 at 08:17
4

The proper format for embedding SVG content from another file is to use a <use> element. However, you cannot then access the DOM structure (i.e., individual elements) of the nested SVG -- it's all treated as a single image.

If you want to be able to modify the graphic described by the external file, you would be better off reading the external file as a text/XML file (using d3.text ), and then using that XML text to create the SVG graphic in the DOM by writing the content as the inner HTML of a container <div>

d3.text("mySVGpicture.svg", function(error, externalSVGText) {
         if (error) {console.log(error); return;}

         chart_div.html(externalSVGText);

         svg = chart_div.select("svg");

         do_stuff();
});

Your DOM should now look like

body
  div.chart
     svg
       g#anIDWhichIsInTheSVGFile
         g#DE
           path
           /*etc*/
       g#anotherIDWhichIsInTheSVGFile

You can now select elements as normal, and set the position or size of the svg you created or remove g elements or whatever -- they are all just normal elements in your DOM. Make sure that your "id" attributes don't conflict -- they have to be unique for the entire page.

Example here, showing additional content added to the SVG with d3, and a d3 transition selecting and modifying an element from the external SVG:
http://jsfiddle.net/J8sp3/2/

WARNING

The original version of this answer suggested adding the external SVG content as a nested SVG inside the SVG that had already been created with D3.

As Seb discovered, this method does not work on the latest Safari and Opera browsers, or on Chrome 31 and under.

The problem is in the way the webkit browsers implement the innerHTML function, which is called by d3's selection.html() function. Specifically, the innerHTML function isn't implemented at all on SVG elements!

What's most confusing is that the method doesn't return any errors, and some versions of Chrome actually display the SVG content without adding it to the DOM (as if you used <use xlink:href="external.svg"/>).

AmeliaBR
  • 27,344
  • 6
  • 86
  • 119
  • thank you so far. Maybe I don't get it, but i cant find the svg in the DOM. Here is a fiddle: http://jsfiddle.net/J8sp3/ – Seb Waynee Jan 19 '14 at 09:00
  • @SebWaynee The read-in SVG is right there in the DOM. However, it is also over-writing the rectangle that you are adding to the main SVG; remember that the callback function in `d3.text` gets called *asynchronously*, when the file is ready, not in the order it's written in the script. Everything that needs to happen after that SVG is added must wait until that function calls it. Updated fiddle: http://jsfiddle.net/J8sp3/1/ – AmeliaBR Jan 19 '14 at 15:45
  • Did you open it in firefox? In firefox it works. When I open it with Chrome or Safari it doesn't seem to work. – Seb Waynee Jan 20 '14 at 14:37
  • Does someone have an idea how to fix that ? (my version of) Chrome/Safari can display the external svg when I load it directly, why not in the read-in-version ? – Seb Waynee Jan 20 '14 at 14:44
  • The fiddle I created works in Chrome. If you're testing your code on your personal computer, it could be that security restrictions are preventing a script from reading a file on your hard drive. If that's the problem, you'll need to run a local-host web server. Search for info on how to set that up. – AmeliaBR Jan 20 '14 at 15:58
  • P.S. Are you getting any errors? If you open up the SVG in the DOM inspector is there any text content? (Text within an SVG isn't displayed on screen -- if your read in file is being interpretted as text instead of XML you might not see anything.) – AmeliaBR Jan 20 '14 at 16:12
  • I wonder that it works in your Chrome. I have a local-host server and know what you mean. That should not be the problem. I cant see the circle in fiddle-chrome (opposed to firefox) and through my local-host webserver. I dont get any errors. The svg appears in the loaded files window but not anywhere in the DOM. Not as text element or anything. In the main_chart_svg there is just the test-rect. – Seb Waynee Jan 20 '14 at 17:39
  • I accepted the answer because it works (at least partly/in firefox) and I think you deserve the points for you effort! – Seb Waynee Jan 20 '14 at 17:45
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/45661/discussion-between-seb-waynee-and-ameliabr) – Seb Waynee Jan 20 '14 at 18:14