D3.Js - V3 and V4 - Enter and Update Differences

d3.js - v3 and v4 - Enter and Update differences

This is the expected behaviour, and I've explained this before in this answer (not a duplicate, though).

What happened is that Mike Bostock, D3 creator, introduced a magic behaviour in D3 v2, which he kept in D3 v3.x, but decided to abandon in D3 v4.x. To read more about that, have a look here: What Makes Software Good? This is what he says:

D3 2.0 introduced a change: appending to the enter selection would now copy entering elements into the update selection [...] D3 4.0 removes the magic of enter.append. (In fact, D3 4.0 removes the distinction between enter and normal selections entirely: there is now only one class of selection.)

Let's see it.

Here is your code with D3 v3:

var svg = d3.select('body').append('svg')  .attr('width', 250)  .attr('height', 250);
//render the datafunction render(data) { //Bind var circles = svg.selectAll('circle').data(data);
//Enter circles.enter().append('circle') .attr('r', 10); //Update circles .attr('cx', function(d) { return d.x; }) .attr('cy', function(d) { return d.y; });
//Exit circles.exit().remove();}
var myObjects = [{ x: 100, y: 100}, { x: 130, y: 120}, { x: 80, y: 180}, { x: 180, y: 80}, { x: 180, y: 40}];

render(myObjects);
<script src='https://d3js.org/d3.v3.min.js'></script>

selection.append / insert: d3 changes from v3 to v4 turns checkboxes into text fields

I believe there was a small change in regards to setting multiple attributes from d3 v3 to d3 v4. Addressing this change produced the intended results in your code.

While in d3 v3 you could use selection.attr() to set multiple attributes, that is no longer the case. The proper method for setting multiple attributes at once is now:

   .attrs({
type: "checkbox",
id: "CB_LABELS",
class: "checkbox",
name: function(d, i) {return i;},
value: function(d, i) {return i;}
})

But .attrs is not included in the default d3 v4 package, you can add:

<script src="https://d3js.org/d3-selection-multi.v1.min.js"></script>

for access to that helper method.

From d3 issues reporting on github:

in the new version to assign multiple attributes on the same line you need to add d3-selection-multi. Now this functionality is not included with the main d3 import. The command is now called attrs instead of attr.

I believe there is similar changes in method behavior for selection.property() and selection.style() as well.

How would this d3 code be updated from v3 to v4?

In d3v3 d3.geo.path used a default projection, the US Albers projection:

d3.geo.path()


Creates a new geographic path generator with the default settings: the
albersUsa projection and a point radius of 4.5 pixels. (link)

In d3v4, the default projection is a null projection:

path.projection([projection]) <>


If a projection is specified, sets the current projection to the
specified projection. If projection is not specified, returns the
current projection, which defaults to null. (link)

This is why your data is scaled and centered appropriately in your d3v3 map, though it wouldn't be if it were of anywhere else.

The d3v4 default projection for a geoPath simply converts the coordinates in the data to svg coordinates with no transformation or projection. Consequently, in d3v4, your data needs a projection to be properly rendered (it is drawn, but as all the x coordinates in the US are negative, it's off screen). To use the default projection from v3 (a US Albers composite projection) you can use:

var projection = d3.geoAlbersUsa();
var path = d3.geoPath().projection(projection);

And then go about everything else as you are:

        var width = 950, height = 500;        var svg = d3.select('body')            .append('svg')            .attr("width",width)            .attr("height",height);
var url = 'https://gist.githubusercontent.com/d3byex/65a128a9a499f7f0b37d/raw/176771c2f08dbd3431009ae27bef9b2f2fb56e36/us-states.json'; d3.json(url, function (error, data) { var projection = d3.geoAlbersUsa() var path = d3.geoPath().projection(projection); svg.selectAll('path') .data(data.features) .enter() .append('path') .attr('d', path); });
    <script src="http://d3js.org/d3.v4.min.js"></script>

d3 v4: merge enter and update selections to remove duplicate code

I'm the author of the solution for your past question, which you linked in this one. I provided that solution in a comment, not as a proper answer, because I was in a hurry and I wrote a lazy solution, full of duplication — as you say here. As I commented in the same question, the solution for reducing the duplication is using merge.

Right now, in your code, there is duplication regarding the setup of the "update" and "enter" selections:

var update = g.selectAll(".datapoints")
.data(filtered[0].values);

var enter = update.enter().append("g")
.attr("class", "datapoints");

update.each(function(d, i){
//code here
});

enter.each(function(d, i){
//same code here
});

To avoid the duplication, we merge the selections. This is how you can do it:

var enter = update.enter().append("g")
.attr("class", "datapoints")
.merge(update)
.each(function(d, i) {
//etc...

Here is the updated Plunker: http://plnkr.co/edit/MADPLmfiqpLSj9aGK8SC?p=preview

D3 (v3.4) edge bundle chart and update button to load a new dataset

I tried editing your code and was able to achieve this. To update the data, we need to make use of enter, update and exit methods. In v3 I remember, when we enter the new data and this will create any missing links or nodes and using exit(), we can remove nodes, links which are not present. After enter, we append the new nodes,links and later update its attributes. We can actually use the same function to create and update which you can try. I dont know why the newDataB is not coming as node in below function. Something wrong with my sample data it seems. Your new sample data is working fine with below code.

var diameter = 760,
radius = diameter / 2;
innerRadius = radius - 160;

var cluster = d3.cluster()
.size([360, innerRadius]);

var line = d3.radialLine()
.curve(d3.curveBundle.beta(0.85))
.radius(function(d) {
return d.y;
})
.angle(function(d) {
return d.x / 180 * Math.PI;
});

var svg = d3.select("body").append("svg")
.attr("width", diameter)
.attr("height", diameter)
.append("g")
.attr("transform", "translate(" + radius + "," + radius + ")");

var link = svg.append("g").selectAll(".link"),
node = svg.append("g").selectAll(".node");

switchData(1);

function mouseovered(d) {
node
.each(function(n) {
n.target = n.source = false;
});

link
.classed("link--target", function(l) {
if (l.target === d) return l.source.source = true;
})
.classed("link--source", function(l) {
if (l.source === d) return l.target.target = true;
})
.filter(function(l) {
return l.target === d || l.source === d;
})
.raise();
node
.classed("node--target", function(n) {
return n.target;
})
.classed("node--source", function(n) {
return n.source;
});
}

function mouseouted(d) {
link
.classed("link--target", false)
.classed("link--source", false);

node
.classed("node--target", false)
.classed("node--source", false);
}

// Lazily construct the package hierarchy from class names.
function packageHierarchy(classes) {
var map = {};

function find(name, data) {
var node = map[name],
i;
if (!node) {
node = map[name] = data || {
name: name,
children: []
};
if (name.length) {
node.parent = find(name.substring(0, i = name.lastIndexOf(".")));
node.parent.children.push(node);
node.key = name.substring(i + 1);
}
}
return node;
}

classes.forEach(function(d) {
find(d.name, d);
});

return d3.hierarchy(map[""]);
}

// Return a list of imports for the given array of nodes.
function packageImports(nodes) {
var map = {},
imports = [];

// Compute a map from name to node.
nodes.forEach(function(d) {
map[d.data.name] = d;
});

// For each import, construct a link from the source to target node.
nodes.forEach(function(d) {
if (d.data.imports) d.data.imports.forEach(function(i) {
imports.push(map[d.data.name].path(map[i]));
});
});

return imports;
}

// update data function
function updateData(classes) {

var root = packageHierarchy(classes);

cluster(root);

var nodes = root.leaves();
var links = packageImports(root.leaves());

let link = svg.selectAll('.link').data(links);

link.enter().append("g")
.attr("class", "link").append("path").each(function(d) {
d.source = d[0], d.target = d[d.length - 1];
});
svg.selectAll('path').attr("class", "link").transition()
.duration(1000).attr("d", line);
link.exit().remove();

let node = svg.selectAll('.node').data(nodes);
node.enter().append('g').append("text");
svg.selectAll('text').attr("transform", function(d) {
return "rotate(" + (d.x - 90) + ")translate(" + (d.y + 8) + ",0)" + (d.x < 180 ? "" : "rotate(180)");
})
.attr("text-anchor", function(d) {
return d.x < 180 ? "start" : "end";
})
.attr("class", "node")
.attr("dy", ".31em")
.text(function(d) {
return d.data.key;
});

node.exit().remove();

}

function switchData(dataset) {
let classes = [];
if (dataset == 0) {
classes = [{
"name": "flare.A.newdataA",
"size": 1000,
"imports": [
"flare.B.newdataB"

]
},
{
"name": "flare.B.newdataB",
"size": 1400,
"imports": [
"flare.A.newdataA"
]
},
{
"name": "flare.A.newdataC",
"size": 1200,
"imports": [
"flare.A.newdataA", "flare.B.newdataB"
]
}
];

} else

{
classes = [{
"name": "flare.A.dataA",
"size": 1000,
"imports": [
"flare.B.dataB"

]
},
{
"name": "flare.B.dataB",
"size": 1200,
"imports": [
"flare.A.dataA"
]
}
];

}

updateData(classes)

}
<!DOCTYPE html>
<html>

<head>

<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">

<script type='text/javascript' src='https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js'></script>

<script src="https://d3js.org/d3.v5.js"></script>

<style>
.node {
font: 300 11px "Helvetica Neue", Helvetica, Arial, sans-serif;
fill: rgb(0, 0, 0, .6);
}

.link {
stroke: steelblue;
fill: none;
pointer-events: none;
}
</style>

</head>

<body>

<div class="grid-container"></div>

<div class="grid-item" id="chart">

<div id="my_dataviz"></div>
</div>

</div>

</body>

<div id="option">
<input name="updateButton" type="button" value="Update" onclick="switchData(0)" />
</div>
<div id="option">
<input name="updateButton" type="button" value="SwitchBack" onclick="switchData(1)" />
</div>

</html>

Updating d3.layout.force v3 to d3.forceSimulation v7

I've noticed the attributes of DOMs are reflecting the status alright. It's just that the simulation just stopped prematurely.

In short, the default value of d3.force.alphaDecay is too short for the intended result; alphaDecay dictates the end of simulation. Try expand the value a little bit. The latest default value for alphaDecay is 0.001, according to d3-force github readme. In my testing session, setting the value to 1/5(0.0002) seems to be enough for the same result.

try run the code below. it works fine.

Tips

When working with DOMs and SVGs, try add matching data-ooo tag to see if the d3.selection is working properly. I've added properties of node data such as .index and .target, .source to attributes like data-index,data-id,data-target,data-source... and noticed that everything is in place.

var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height");

graph = {
nodes: [],
links: [],
}

var simulation = d3.forceSimulation()
.force("center", d3.forceCenter(width / 2, height / 2).strength(0.01))
.nodes(graph.nodes)
.force("link", d3.forceLink(graph.links).distance(100))
.on("tick", function() {
svg.selectAll('.link')
.attr("x1", function (d) { return d.source.x })
.attr("y1", function (d) { return d.source.y })
.attr("x2", function (d) { return d.target.x })
.attr("y2", function (d) { return d.target.y })

svg.selectAll('.node')
.attr("cx", function (d) { return d.x })
.attr("cy", function (d) { return d.y })
.attr("transform", function (d) {
return "translate(" + d.x + "," + d.y + ")";
})
}).alphaDecay(0.0002) // just added alpha decay to delay end of execution

function update() {
// update links
var link = svg.selectAll('.link').data(graph.links);
link.enter()
.insert('line', '.node')
.attr('class', 'link')
.style('stroke', '#d9d9d9');
link
.exit()
.remove()

// update nodes
var node = svg.selectAll('.node').data(graph.nodes);
var g = node.enter()
.append('g')
.attr('class', 'node');
g.append('circle')
.attr("r", 20)
.style("fill", "#d9d9d9");
g.append('text')
.attr("class", "text")
.text(function (d) { return d.name });
node
.exit()
.remove();

// update simulation
simulation
.nodes(graph.nodes)
.force("link", d3.forceLink(graph.links).distance(100))
.force("charge", d3.forceManyBody().strength(-200))
.restart()
};

function addNode(node) {
graph.nodes.push(node);
update();
};

function connectNodes(source, target) {
graph.links.push({
source: source,
target: target,
});
update();
};

addNode({
id: "you",
name: "you",
});

let index = 1;

// add a new node every three seconds and connect to 'you'
const interval = window.setInterval(() => {
let id = Math.random().toString(36).replace('0.','');
id = id.slice(0,4);
addNode({
id: id,
name: id
});

connectNodes(0, index);
index++;
}, 3000);

// no more than 8 nodes
setTimeout(() => {
clearInterval(interval)
}, 3000 * 8);
<html>
<head>
<script src="https://d3js.org/d3.v7.min.js"></script>
</head>
<body>
<svg width="400" height="200"></svg>
</body>
</html>

D3's data update not functioning as expected

Without the rest of your code to ensure that this works. You should use .merge(). For v3 the method is different, this is for v4+.

let rects = svg.selectAll('rect')
.data(data)

//On update Only
rects.attr('fill', 'black')

//On enter ...
rects.enter()
.append('rect')
.style('fill', 'maroon')
.merge(rects)
.attr('x', (d, i)=>i*(barWidth+padding))
.attr('y', d=>height - bar_height(d.value))
.attr('width', barWidth)
.attr('height', d=> bar_height(d['value']))

This will merge the newly added rects into the selection of the existing rects and will update all of them with everything that comes after the merge statement.



Related Topics



Leave a reply



Submit