Extending d3.selection in v5—is this the correct w

2020-04-08 11:30发布

问题:

Background: I was trying to solve the problem of appending existing local SVG files to a d3 SVG container in an Electron desktop app. I discovered that I can't use d3.svg() on local files because fetch does not work with the file protocol (so said the error msg).

I came across this gist referenced by this question for extending d3.selection and it appears to do exactly what I need, adding appendHTML and appendSVG functions.

When I tested the code though (at bottom below), it threw an error: "Cannot read property 'prototype of undefined" – choking on this line:

d3.selection.enter.prototype.appendHTML 

I dug around a bit through logging to the console and came up with this change and it seems to work:

d3.selection.prototype.enter.prototype.appendHTML   

My Question: Am I doing this right? Did something change in d3 which necessitates the additional prototype reference? I'm no Javascript or d3 hero and would like to understand what the difference is here.

d3.selection.prototype.appendHTML =
    d3.selection.prototype.enter.prototype.appendHTML = function (HTMLString) {
        return this.select(function () {
            return this.appendChild(document.importNode(new DOMParser().parseFromString(HTMLString, 'text/html').body.childNodes[0], true));
        });
    };

Original code

 d3.selection.prototype.appendHTML =
    d3.selection.enter.prototype.appendHTML = function(HTMLString) {
        return this.select(function() {
            return this.appendChild(document.importNode(new DOMParser().parseFromString(HTMLString, 'text/html').body.childNodes[0], true));
        });
    };


 d3.selection.prototype.appendSVG =
    d3.selection.enter.prototype.appendSVG = function(SVGString) {
        return this.select(function() {
            return this.appendChild(document.importNode(new DOMParser()
            .parseFromString('<svg xmlns="http://www.w3.org/2000/svg">' + SVGString + '</svg>', 'application/xml').documentElement.firstChild, true));
        });
    };

回答1:

The answers you mentioned are using D3 v3, but things have changed considerably from v3 to v4/v5. The main difference when it comes to selections is covered by just one sentence in the changelog:

Selections no longer subclass Array using prototype chain injection; they are now plain objects, improving performance.

Although this sounds quite simple it nonetheless required vast changes under the hood. All selection objects are now instances of the Selection function which is not directly exposed. d3.selection is a function returning a new instance of a Selection:

function selection() {
  return new Selection([[document.documentElement]], root);
}

Although both Selection and d3.selection share the same prototype, which contains the .enter property, there is no property .enter unless an instance is created, hence the error in your code.

The correct way of extending D3's selection objects in v4/v5 would be along the following lines:

d3.selection    
  .prototype    // This prototype is shared across all types of selections.   
  .appendHTML = // Apply changes to the selection's prototype.

Since the prototype property of Selection and d3.selection points to the same object, these changes will affect both normal as well as enter selections because they are both instances of the Selection function.

As you can see, this is just the first line of your own code, which is perfectly fine. Your extension using d3.selection.prototype.enter.prototype.appendHTML only kind of works: it does neither harm nor good! Setting the property on the .enter function is pointless as there is never an instance created from this function.

Have a look at the following working demo which I adopted from the gist you linked to in your question:

d3.selection.prototype.appendHTML =
  function(HTMLString) {
    return this.select(function() {
      return this.appendChild(
        document.importNode(
          new DOMParser().parseFromString(HTMLString, 'text/html').body.childNodes[0], true)
        );
    });
  };

d3.selection.prototype.appendSVG =
  function(SVGString) {
    return this.select(function() {
      return this.appendChild(
        document.importNode(
          new DOMParser()
            .parseFromString('<svg xmlns="http://www.w3.org/2000/svg">' + SVGString + '</svg>', 'application/xml').documentElement.firstChild, true));
    });
  };

d3.select('.container').appendHTML('<svg><g><rect width="50" height="50" /></g></svg>');

var svg = d3.select('.container')
  .appendHTML('<svg xmlns="http://www.w3.org/2000/svg"><g><circle class="circle1" cx="50" cy="50" r="50"></circle></g></svg>')
  .select('g');

svg.appendSVG('<circle class="circle2" cx="20" cy="20" r="20"></circle>');
svg.appendSVG('<rect width="30" height="30"></rect>');
div,
svg {
  border: 1px solid silver;
  margin: 10px;
}

rect {
  fill: skyblue;
}

.circle1 {
  fill: orange;
}

.circle2 {
  fill: lime;
}
<script src="https://d3js.org/d3.v5.js"></script>
<div class="container"></div>