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));
});
};
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>