d3 force layout as line (y coordinate stays the same) - javascript

I'm trying to draw what I think amounts to a force graph in d3, but in a single flat line. I would like around 4-5 points of varying size depending on their magnitude, spaced evenly between them (not by center, but the distance between the sides of the circles should be constant), and lines to join them. So in ASCII format, something like:
o---O---o---O
I was trying to avoid a complicated calculation to figure out the center coordinates and start and end of each line, so it seemed like the force layout might do the trick. Unfortunately, when I put it together, I can't seem to get it to work very well. Often times points end up behind other points, so for a 4 node graph like above, it comes out looking something more like:
O---O
Is there any way to get the force layout to work in 1 dimension instead of 2? Or am I stuck doing all of the spacing calculations myself? The code I'm working with is below:
var width = 500;
var height = 200;
var svg = d3.select($el[0])
.append('svg')
.attr('width', width)
.attr('height', height);
var data_nodes = [
{ x: width / 2, y: height / 2, count: 5 },
{ x: width / 2, y: height / 2, count: 0 },
{ x: width / 2, y: height / 2, count: 1 },
{ x: width / 2, y: height / 2, count: 10 },
];
var data_links = [
{ source: 0, target: 1 },
{ source: 1, target: 2 },
{ source: 2, target: 3 },
];
var force = d3.layout.force()
.nodes(data_nodes)
.links(data_links)
.linkDistance(150)
.linkStrength(0.5)
.gravity(0.7)
.friction(0.3)
.size([width, height])
.charge(-300);
var links = svg.selectAll('line')
.data(data_links)
.enter()
.append('line')
.attr('stroke', '#65759E')
.attr('stroke-width', 4)
.attr('x1', function (d) { return data_nodes[d.source].x; })
.attr('y1', function (d) { return data_nodes[d.source].y; })
.attr('x2', function (d) { return data_nodes[d.target].x; })
.attr('y2', function (d) { return data_nodes[d.target].y; });
var nodes = svg.selectAll('circle')
.data(data_nodes)
.enter()
.append('circle')
.attr('fill', '#65759E')
.attr('r', function (d) { return 10 + Math.sqrt(d.count) * 4; })
.attr('cx', function (d, i) { return d.x + i * 10; })
.attr('cy', function (d, i) { return d.y; });
force.on('tick', function () {
nodes.attr('cx', function (d) { return d.x; });
links.attr('x1', function (d) { return d.source.x; })
.attr('x2', function (d) { return d.target.x; });
});
force.start();

The difficulty you're having stems from the fact that there is no way for the nodes to get around each other without leaving the 1D line you're forcing them into. The repulsive forces prevent a node from passing over top of another node to get to the other side, so they become trapped in these sub-optimal arrangements.
By default, d3 force layout initializes nodes in a random position. However, you can initialize them yourself by setting the x and y properties of the data nodes objects before starting the layout. If you initialize the graph with the nodes ordered in a row, according to the order of their connections, then the force layout can handle the spacing for you.

Related

Why are the results of d3.simulate not always symmetrical?

I want to create a simulation of magnets floating on oil without any friction.
The magnets have different force based on their radius. They move freely until they find an ideal balance of forces. I would guess that this balance of forces will always cause the magnets to be symmetrically arranged.
I have tried to create such a simulation with d3, but the result is not always Symetrical. Even when I drag and drop the elements, they do not always move to the same position.
Is the symmetry theory fundamentally wrong? Shouldn't the elements always move to the same position?
Or are the forces constructed incorrectly?
// Source: https://bl.ocks.org/HarryStevens/f636199a46fc4b210fbca3b1dc4ef372
var radius = 160;
var positives = [27, 50, 20, 20];
var negatives = [10, 10, 10, 10, 10, 10, 10, 10, 10, 10];
/* Änderungen der Konfiguration nur oberhalb dieser Zeile */
var width = 400,
height = 400;
var myNodes = [];
for (let i = 0; i < positives.length; i++) {
myNodes.push({
charge: -positives[i]
});
}
for (let i = 0; i < negatives.length; i++) {
myNodes.push({
charge: negatives[i]
});
}
var nodePadding = 10;
var svg = d3.select("svg").attr("width", width).attr("height", height);
var simulation = d3
.forceSimulation()
.nodes(myNodes)
.force(
"forceX",
d3
.forceX()
.strength(0.1)
.x(width * 0.5)
)
.force(
"forceY",
d3
.forceY()
.strength(0.1)
.y(height * 0.5)
)
.force(
"center",
d3
.forceCenter()
.x(width * 0.5)
.y(height * 0.5)
)
.force(
"charge",
d3.forceManyBody().strength(function (d) {
return -d.charge * 10;
})
)
.force(
"radial",
d3.forceRadial(
function (d) {
return d.charge > 0 ? radius : 0;
},
width / 2,
height / 2
)
)
.force(
"collide",
d3.forceCollide().radius(function (d) {
return Math.abs(d.charge) + nodePadding;
})
)
.on("tick", function (d) {
node
.attr("cx", function (d) {
return d.x;
})
.attr("cy", function (d) {
return d.y;
});
});
svg
.append("circle")
.classed("radius", true)
.attr("cx", width / 2)
.attr("cy", height / 2)
.attr("r", radius)
.style("fill", "none")
.style("stroke", "#bbb")
.style("stroke-dasharray", 4);
var node = svg
.selectAll(".node")
.data(myNodes)
.join("circle")
.classed("node", true)
.attr("r", function (d) {
return Math.abs(d.charge);
})
.attr("fill", function (d) {
return d.charge > 0 ? "#0000ff" : "#ff0000";
})
.attr("cx", function (d) {
return d.x;
})
.attr("cy", function (d) {
return d.y;
});
d3.selectAll("circle").call(
d3.drag().on("start", dragstarted).on("drag", dragged).on("end", dragended)
);
distance = ([x1, y1], [x2, y2]) =>
Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2));
function dragstarted(event, d) {
if (!event.active) simulation.alphaTarget(0.03).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(event, d) {
d.fx = event.x;
d.fy = event.y;
}
function dragended(event, d) {
if (event.active) simulation.alphaTarget(0.03);
d.fx = null;
d.fy = null;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.2.0/d3.min.js"></script>
<div id="content">
<svg width="400" height="400">
</svg>
</div>
See here for a codepen:
https://codepen.io/ValeSauer/pen/bGKJawe
Generally, force directed placement of nodes tends to placement that will minimize the energy inherent in the system. While, it works pretty well and is fun to watch, it does have some disadvantages, as explained in this section of Wikipedia's article on graph drawing. In particular, the process is designed to find a local minimum of the energy function, rather than a global minimum. I too would expect the global minimum to have some sort of symmetry, if the number and sizes of the nodes allows it, but the local minimum need not.
As an example to see this in action, simply reduce the number of negative nodes in your simulation to two. You should see an image that looks like so:
Now, you have a lot of forces at work here that makes this a bit tricky to analyze in depth. Among those forces, though, is a charge force that's pulling the red and blue nodes together along with a collision force at length that's pushing them apart. Thus, it's not too hard to imagine that the two negatively charged blue nodes reside in local energy wells, though it's likely that there's a more symmetrical global configuration of smaller energy.

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

I am trying to update a force-directed graph written using d3js version 3 to d3js version 7.
The following code snippet is the working implementation using d3js v3:
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height");
graph = {
nodes: [],
links: [],
}
var simulation = d3.layout.force()
.size([width, height])
.nodes(graph.nodes)
.links(graph.links)
.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 + ")";
})
});
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
.linkDistance(100)
.charge(-200)
.start();
};
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.v3.min.js"></script>
</head>
<body>
<svg width="400" height="200"></svg>
</body>
</html>
The following code snippet my attempt of implementing the above code snippet using d3js v7:
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 + ")";
})
});
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>
The d3js v7 code snippet does not produce the same results as d3js v3 - why is this? The exact changes I have done are seen in this diff: https://www.diffchecker.com/wdq7AFbU.
Even without adding any connections, there is a difference between the two implementations. The v3 implementation makes the "you" node fly in from random directions, whilst with the v7 implementation the "you" node always flies in from the same direction.
There also seems to be some discrepancy on how the force is being applied since the new nodes in the v7 implementation get stuck in the top-left corner.
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>
Edit: What is alpha and alphaDecay?
doing simulation.restart().alpha(0.3) seem to give the same effect which was mentioned in an answer to my earlier post. Is there any difference between the two?
d3-force github readme says alpha stands for entropy. In easy words, alpha stands for the life of simulation; alpha=1 stands for start and alpha=0 stands for the end.
https://github.com/d3/d3-force#simulation_alpha
alpha is roughly analogous to temperature in simulated annealing. It decreases over time as the simulation “cools down”. When alpha reaches alphaMin, the simulation stops
here's a simple pseudocode that illustrates the idea.
alpha = 1
alphaDecay = 0.002
function tick() {
alpha = alpha - alphaDecay
}
loop {
tick()
if alpha equals to 0 then end simulation
}
the previous answer mentioned in the comment increased alpha when restart because he wanted to give simulation more time after a reset.
in my answer, I've set alphaDecay to a lower number so that the simulation can work for a longer period of time.
increasing alphaDecay/decreasing alpha = simulation ends quicker
decreasing alphaDecay/increasing alpha = simulation ends later
Edit: Changes in d3-force since D3 v4?
Also, there is still some difference between the v3 and v7 implementation; 1) the collisions in v3 is more elastic and 2) the new nodes being added come in from random directions. Do you know what could be fixed to get 1) and 2) in the v7 implementation?
please read this d3-force v1 github changelog; d3-force became a separate package since d3 v4 and this changelog explains the changes.
1. d3-force has become more accurate.
The changelog mentions many improvements:
The force simulation now uses velocity Verlet integration rather than position Verlet, tracking the nodes’ positions (node.x, node.y) and velocities (node.vx, node.vy) rather than their previous positions (node.px, node.py).
The new link force replaces force.linkStrength and employs better default heuristics to improve stability.
The physics integration of d3-force has improved for better accuracy. This is why it looks different from v3 implementation.
Although it is possible to tune the simulation look like in a specific way but what does more elastic mean? Does it mean stronger reaction force? or does it mean faster animation(but in same amount of time)? it surely can be tuned in, only if the request was more detailed. And every d3 package has surprisingly simple structure and formulas. It is possible to look inside and change its inner function.
2. manipulating positions of nodes
https://github.com/d3/d3-force#simulation_nodes
manipulate .x and .y of nodes during the simulation to change their positions.
addNode({ id: /* ... */, x: 0, y: 100}) // like this
edit: there were some typos in my answer about the increase-decrease relation of alpha and time.

Mimicking an audio waveform with d3

I'm making a visualisation showing the most common genres for concerts in a given city. Currently I'm displaying the data using a d3 area chart.
To make the visualisation more thematically appropriate I thought I'd try to make it look similar to an audio waveform, and this is where I hit a bit of a stumbling block.
Currently I'm still using the area chart mirrored over the x-axis, and this looks good for some cities when the data is spread out over a significant amount of time. It however doesn't look right when looking at a large city with lots of events.
Looks okay here:
Not so good here:
I think what I want to do is to draw smaller rectangles inside the bounds of the area chart, first I thought I could use the area SVG as a mask, but then the tops of the rectangles would end up slanted...
Also thought of just making a bar chart with several bars for each data point, but that I think would result in big shifts in between each point. I want to still have the slants between data points that I get from an area/line chart.
Anyone got any suggestions? My goal would be to get the visualisation something like below:
One approach is to use data not for creating the chart, but for creating a jagged linear scale x -> y. Next, generate desired amount of rectangles, and map their indexes to x position. Rest is easy -- go over indexes, map them to x, map the gotten x to y, and you're set.
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v4.min.js"></script>
<style>
body { margin:0;position:fixed;top:0;right:0;bottom:0;left:0; }
</style>
</head>
<body>
<script>
var data = [{
x: 1,
y: 2
}, {
x: 5,
y: 10
}, {
x: 15,
y: 5
}, {
x: 20,
y: 13
}, {
x: 25,
y: 4
}];
var rectCount = 8;
var yScale = d3.scaleLinear()
.domain(data.map(function(d){ return d.x; }))
.range(data.map(function(d){ return d.y; }));
var xScale = d3.scaleBand()
.domain(d3.range(rectCount))
.paddingInner(0.5)
.range([d3.min(data, function(d) {return d.x; }),
d3.max(data, function(d) {return d.x; })]);
var svg = d3.select("body").append("svg")
.attr("width", 100)
.attr("height", 100)
.attr('viewBox', '-5 -20 35 40');
var chartData = d3.range(rectCount).map(function(i){
var x = xScale(i);
var width = xScale.bandwidth();
var y = yScale(x);
var height = y*2;
return {
x: x,
y: y,
width: width,
height: height
};
});
svg.selectAll('rect')
.data(chartData)
.enter()
.append('rect')
.attr('x', function(d) { return d.x; })
.attr('y', function(d) { return -d.y; })
.attr('width', function(d) { return d.width; })
.attr('height', function(d) { return d.height; })
</script>
</body>

Force simulation is jittery when using svg transforms to update position

JSFiddle example
I've noticed that when updating positions of svg elements in a d3-force diagram, updating the positions of elements using (in the case of circles) the cx and cy attributes is much smoother than using the transform attribute.
In the example JSFiddle, there are two separate force simulations side-by-side. The one on the left updates positions using the transform attribute:
sim_transform.on('tick', function () {
circles_transform.attr('transform', function (d) {
return 'translate(' + d.x + ',' + d.y + ')';
});
});
The one on the right updates positions using the cx and cy attributes of a circle:
sim_position.on('tick', function () {
circles_position
.attr('cx', function (d) {
return d.x;
})
.attr('cy', function (d) {
return d.y;
})
});
The simulations appear identical until they're just about to become static, at which point the one using transforms starts to jitter quite a bit. Any ideas what is causing this? Can it be fixed so that the animation remains smooth using transforms?
It seems to me that the issue you're observing (only reproducible in FireFox, as #altocumulus noted) has something to do with the way FF uses floating numbers for the translate of the transform attribute.
We can see this if we set both simulations to use integers, doing ~~(d.x) and ~~(d.y). Have a look, both will jitter:
var svg = d3.select('svg');
var graph_transform = gen_data();
var graph_position = gen_data();
var force_left = d3.forceCenter(
parseInt(svg.style('width')) / 3,
parseInt(svg.style('height')) / 2
)
var force_right = d3.forceCenter(
2 * parseInt(svg.style('width')) / 3,
parseInt(svg.style('height')) / 2
)
var sim_transform = d3.forceSimulation()
.force('left', force_left)
.force('collide', d3.forceCollide(65))
.force('link', d3.forceLink().id(id));
var sim_position = d3.forceSimulation()
.force('right', force_right)
.force('collide', d3.forceCollide(65))
.force('link', d3.forceLink().id(id));
var g_transform = svg.append('g');
var g_position = svg.append('g');
var circles_transform = g_transform.selectAll('circle')
.data(graph_transform.nodes)
.enter()
.append('circle')
.attr('r', 40);
var circles_position = g_position.selectAll('circle')
.data(graph_position.nodes)
.enter()
.append('circle')
.attr('r', 40);
sim_transform
.nodes(graph_transform.nodes)
.force('link')
.links(graph_transform.links);
sim_position
.nodes(graph_position.nodes)
.force('link')
.links(graph_position.links);
sim_transform.on('tick', function() {
circles_transform.attr('transform', function(d) {
return 'translate(' + (~~(d.x)) + ',' + (~~(d.y)) + ')';
});
});
sim_position.on('tick', function() {
circles_position
.attr('cx', function(d) {
return ~~d.x;
})
.attr('cy', function(d) {
return ~~d.y;
})
});
function id(d) {
return d.id;
}
function gen_data() {
var nodes = [{
id: 'a'
},
{
id: 'b'
},
{
id: 'c'
},
{
id: 'd'
}
]
var links = [{
source: 'a',
target: 'b'
},
{
source: 'b',
target: 'c'
},
{
source: 'c',
target: 'd'
},
{
source: 'd',
target: 'a'
}
];
return {
nodes: nodes,
links: links
}
}
svg {
width: 100%;
height: 500px;
}
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg></svg>
So, in your original code, it seems like the circles move correctly when using cx and cy, but they jump from integer to integer when using translate (or maybe half pixel, see the last demo). If the hypothesis here is correct, the reason that you just see the effect when the simulation is cooling down is because, at that moment, the movements are smaller.
Demos
Now, if we get rid of the simulation, we can see that this strange behaviour also happens with a very basic transform. To check this, I created a transition for a big black circle, using a linear ease and a very long time (to facilitate seeing the issue). The circle will move 30px to the right. I also put a gridline to make the jumps more noticeable.
(Warning: the demos below are only reproducible in FireFox, you won't see any difference in Chrome/Safari)
If we use cx, the transition is smooth:
var svg = d3.select("svg");
var gridlines = svg.selectAll(null)
.data(d3.range(10))
.enter()
.append("line")
.attr("y1", 0)
.attr("y2", 200)
.attr("x1", function(d) {
return 300 + d * 3
})
.attr("x2", function(d) {
return 300 + d * 3
})
.style("stroke", "lightgray")
.style("stroke-width", "1px");
var circle = svg.append("circle")
.attr("cx", 200)
.attr("cy", 100)
.attr("r", 98)
.transition()
.duration(10000)
.ease(d3.easeLinear)
.attr("cx", "230")
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg width="500" height="200"></svg>
However, if we use translate, you can see the circle jumping 1px at every move:
var svg = d3.select("svg");
var gridlines = svg.selectAll(null)
.data(d3.range(10))
.enter()
.append("line")
.attr("y1", 0)
.attr("y2", 200)
.attr("x1", function(d) {
return 300 + d * 3
})
.attr("x2", function(d) {
return 300 + d * 3
})
.style("stroke", "lightgray")
.style("stroke-width", "1px");
var circle = svg.append("circle")
.attr("cx", 200)
.attr("cy", 100)
.attr("r", 98)
.transition()
.duration(10000)
.ease(d3.easeLinear)
.attr("transform", "translate(30,0)")
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg width="500" height="200"></svg>
For you people running this in Chrome/Safari, this is how the last snippet looks like in Firefox. It's like the circle is being moved half a pixel at every change... definitely not as smooth as changing cx:
var svg = d3.select("svg");
var gridlines = svg.selectAll(null)
.data(d3.range(10))
.enter()
.append("line")
.attr("y1", 0)
.attr("y2", 200)
.attr("x1", function(d) {
return 300 + d * 3
})
.attr("x2", function(d) {
return 300 + d * 3
})
.style("stroke", "lightgray")
.style("stroke-width", "1px");
var circle = svg.append("circle")
.attr("cx", 200)
.attr("cy", 100)
.attr("r", 98);
var timer = d3.timer(function(t){
if(t>10000) timer.stop();
circle.attr("cx", 200 + (~~(60/(10000/t))/2));
})
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg width="500" height="200"></svg>
As this is an implementation issue only visible in FF, it may be worth reporting a bug.

d3 selection confusion with exit() and remove()

So I'm working through some d3 tutorials and learning a bit about why things work the way they do, and I've run into a peculiar instance of selection not behaving as expected. So I'm wondering if anyone can explain this to me.
var sales = [
{ product: 'Hoodie', count: 7 },
{ product: 'Jacket', count: 6 },
{ product: 'Snuggie', count: 9 },
];
var rects = svg.selectAll('rect')
.data(sales).enter();
var maxCount = d3.max(sales, function(d, i) {
return d.count;
});
var x = d3.scaleLinear()
.range([0, 300])
.domain([0, maxCount]);
var y = d3.scaleBand()
.rangeRound([0, 75])
.domain(sales.map(function(d, i) {
return d.product;
}));
rects.append('rect')
.attr('x', x(0))
.attr('y', function(d, i) {
return y(d.product);
})
.attr('height', y.bandwidth())
.attr('width', function(d, i) {
return x(d.count);
});
This all works good and fine, generates 3 horizontal bars that correspond to the data in sales, but here's where I'm seeing the ambiguity:
sales.pop();
rects.data(sales).exit().remove();
The last line is supposed to remove the bar that was popped, from the visual but it doesn't work. I think that there must be something going on with the d3 selection that I'm missing, because this does work:
d3.selectAll('rect').data(sales).exit().remove();
Also when I break out the first one that doesn't work, it does appear to be selecting the correct element on exit, but just doesn't seem to be removing it. Anyway if anyone can explain what's going on here that would be very helpful, thanks!
Note: using d3 v4
This is your rects selection:
var rects = svg.selectAll('rect')
.data(sales).enter();
So, when you do this:
rects.data(sales).exit().remove();
You are effectively doing this:
svg.selectAll('rect')
.data(sales)
.enter()
.data(sales)
.exit()
.remove();
Thus, you are binding data, calling enter, binding data again and calling exit on top of all that! Wow!
Solution: just create a regular, old-fashioned "update" selection:
var rects = svg.selectAll('rect')
.data(sales);
Here is a basic demo with your code, calling exit after 2 seconds:
var svg = d3.select("svg")
var sales = [{
product: 'Hoodie',
count: 7
}, {
product: 'Jacket',
count: 6
}, {
product: 'Snuggie',
count: 9
}, ];
draw();
function draw() {
var rects = svg.selectAll('rect')
.data(sales);
var maxCount = d3.max(sales, function(d, i) {
return d.count;
});
var x = d3.scaleLinear()
.range([0, 300])
.domain([0, maxCount]);
var y = d3.scaleBand()
.rangeRound([0, 75])
.domain(sales.map(function(d, i) {
return d.product;
}))
.padding(.2);
var rectsEnter = rects.enter().append('rect')
.attr('x', x(0))
.attr('y', function(d, i) {
return y(d.product);
})
.attr('height', y.bandwidth())
.attr('width', function(d, i) {
return x(d.count);
});
rects.exit().remove();
}
d3.timeout(function() {
sales.pop();
draw()
}, 2000)
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg></svg>
rects is already a d3 selection, so you only need rects.exit().remove()

Categories