score:1

Solution: d3.xml plus d3.queue

I needed to set up a d3.queue, where one external call was bringing in the data for the visualization, and the other was bringing in the SVG image mentioned above.

The entire visualization became fairly large and complex; the code below captures the relevant parts as I'd used them.

// First create helper function with callback return, to provide structure `d3.queue` needs.
readSvg = function(svg_path, callback) {
  d3.xml(svg_path).mimeType("image/svg+xml")
    .get(function(err, xml) {
       if (err) {
         throw err;
       }
       callback(null, xml.documentElement.outerHTML);
     });
};

// Next make the `d3.queue` call.
// (In reality, the `makeViz` function would need to be defined first, but
//  logically, I felt it easier to understand if I show this first.)
d3.queue()
  .defer(readSvg, my_svg_image_path)
  .defer(d3.json, my_data_path)
  .await(makeViz);


// "Finally" display the visualizations using `makeViz`.
var makeViz = function(error, svg_image, data) {
  if (error) {
    console.log(error);
  } else {
    var points = svg.selectAll('.point')
                    .data(data).enter()
                    .append('g')
                    .classed('point', true)
                    .style('fill', my_fill)
                    .style('stroke-opacity', my_opacity)
                    .html(svg_image); // Note the `html` call!!!
    // These circles are added to make the icons more easily "clickable".
    // NOTE:  THE VARIABLE "points" IS STILL ASSOCIATED WITH THE IMAGE, NOT
    //        THE CIRCLES BELOW!!!  THIS IS ON PURPOSE, FOR EASIER
    //        ASSOCIATION AND MAPPING LATER!!
    points.append('circle').attr('r', icon_size);
    // Now call a function that associates all of the x & y coordinates according
    // to the bound data and what the users click on.  (`clicked` is a variable
    // defined out of scope, and is part of the interactivity.)
    updatePoints(data, points, clicked);

A few things to note:

  1. The image had a lot of "empty space". Imagine if the image were a smiley face; most of the smiley face is nothing. Therefore, most of it is not clickable. I therefore created a parent g DOM element and attached the circle to it as well as the image. Therefore, the entire "smiley face" was clickable, including the white space.

  2. By using this d3.xml.mimeType.get call, I was able to get the entire SVG structure of the file. Then sending on its .documentElement.outerHTML gave me just the parts I was looking for: all of the gs, svgs, paths, and all other elements of the image, and none of the other DOM stuff that comes along with an xml import.

  3. Because I needed to import 2 things, I needed to use d3.queue. But because d3.queue works via an expected callback, I needed to structure that skeleton call with readSvg as I did. That function does not do much, except call the function that is passed to it, which is a function to return the data.

    • There might be a better or cleaner way to handle this part, but simply making a d3.xml call directly within d3.queue I could not get to work properly.

If anyone has better suggestions, that's great, but I can say this works.

Also, I'm always looking for tips, pointers, and suggestions for how to indent all these chained calls. Is the way I've shown here the best? I find it challenging to read d3 code sometimes because of the combination of chained calls and callback functions. (I'm in the process of adopting an async/await structure for some of my d3 work.)


Related Query