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.
Related
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.
I am creating a sunburst for big data. To make it more readable, I need to assign different color for each node (ideally different shades of the same color for every subtree).
I've already tried with :
d3.scaleSequential()
d3.scale.ordinal()
d3.scale.category20c()
I think it can work but I am not sure where to put it exactly. For the moment it works only with one color for every subtree.
var width = 500;
var height = 500;
var radius = Math.min(width, height) / 2;
var color = d3.scaleSequential().domain([1,10]).interpolator(d3.interpolateViridis);
var g = d3.select('svg')
.attr('width', width)
.attr('height', height)
.append('g')
.attr('transform', 'translate(' + width / 2 + ',' + height / 2 + ')');
var partition = d3.partition() //.layout
.size([2 * Math.PI, radius]);
d3.json("file:///c:\\Users\\c1972519\\Desktop\\Stage\\tests_diagrams\\figure_4.8_ex3\\data2.json", function(error, nodeData){
if (error) throw error;
var root = d3.hierarchy(nodeData)
.sum(function(d){
return d.size;
});
partition(root);
var arc = d3.arc()
.startAngle(function(d) { return d.x0; })
.endAngle(function(d) { return d.x1; })
.innerRadius(function(d) { return d.y0; })
.outerRadius(function(d) { return d.y1; });
var arcs = g.selectAll('g')
.data(root.descendants())
.enter()
.append('g')
.attr("class", "node")
.append('path')
.attr("display", function (d) { return d.depth ? null : "none"; })
.attr("d", arc)
.style('stroke', '#fff')
.style("fill", function(d){return color(d)});
}
So I would like to have different shade on every subtree to make it more readable.
Anyone have an idea?
can you try with scaleLinear.
var x = d3.scaleLinear([10, 130], [0, 960]);
or
var color = d3.scaleLinear([10, 100], ["brown", "steelblue"]);
Example:
https://bl.ocks.org/starcalibre/6cccfa843ed254aa0a0d
Documentation:
https://github.com/d3/d3-scale/blob/master/README.md#scaleLinear
Linear Scales
d3.scaleLinear([[domain, ]range]) <>
Constructs a new continuous scale with the specified domain and range, the default interpolator and clamping disabled. If either domain or range are not specified, each defaults to [0, 1]. Linear scales are a good default choice for continuous quantitative data because they preserve proportional differences. Each range value y can be expressed as a function of the domain value x: y = mx + b.
I'm new to d3 and fairly new to javascript in general and have encountered an error I can't explain with my current knowledge...
I've generated 6 circles using a 2d array (success) and have created a function to call in a timer to increment the x and y position by 1 each call. Currently the code I have generates the 6 circles but the timer infinitely creates more circles of "NaN" instead of updating the positions of the initial 6. My code is below;
<body>
<div id="svgDiv"></div>
<script src="~/scripts/d3/d3.min.js"></script>
<script src="~/scripts/App/test.js"></script>
</body>
and js;
var windowWidth = window.innerWidth;
var windowLength = window.innerHeight;
var pos =
[[50, 40],
[100, 80],
[150, 120],
[200, 160],
[250, 200],
[300, 240]];
var base = d3.select("#svgDiv").append("svg")
.attr("width", windowWidth)
.attr("height", windowLength);
function initSetup() {
windowWidth = window.innerWidth;;
windowLength = window.innerHeight;
base.attr("width", windowWidth)
.attr("height", windowLength);
document.body.style.overflow = 'hidden';
}
window.onload = initSetup;
function windowResize() {
windowWidth = window.innerWidth;;
windowLength = window.innerHeight;
base.attr("width", windowWidth)
.attr("height", windowLength);
};
window.addEventListener("resize", windowResize);
function svgDivClick() {
base.selectAll("circle")
.data(pos) // .data(pos, function (d) { return d; })
.enter()
.append("circle")
.attr("cx", function (d, i) {
return pos[i][0];
})
.attr("cy", function (d, i) {
return pos[i][1];
})
.attr("r", 0)
.style("fill", "00ACCD")
.transition()
.attr("r", 20)
.style("fill", "00ACCD")
.duration(500);
base.exit().transition()
.attr("r", 0)
.remove();
console.log("click works");
d3.timer(animate);
};
document.getElementById("svgDiv").addEventListener("click", svgDivClick);
function animate() {
base.selectAll("circle")
.data("circle", function (d) { return d; })
.enter()
.append("circle")
.attr("cx", function (d, i) {
return d.cx + 1;
})
.attr("cy", function (d, i) {
return d.cy + 1;
});
base.exit().transition()
.attr("r", 0)
.remove();
};
The end goal is to have the circles float about randomly but at this point I'm just trying to control the positions. EDIT: The error occurs in the animate function.
Any insights would be great, thanks in advance!
You have some problems here. The main one is that you are binding the same data to the DOM elements 60 times a second, which makes no sense. The second one is that you are not increasing the positions, but simply resetting them. A third one is that there is no cx or cy properties in the data. A last one is that you are not declaring your circles' selection.
I made a simple demo to show you how to use a d3.timer. Check how the position is retrieved and changed. Also, have in mind that in this example I removed all transitions: mixing transitions and timers is complicated and, often, unnecessary.
Here is the demo:
var windowWidth = 500;
var windowLength = 300;
var pos = [
[50, 40],
[100, 80],
[150, 120],
[200, 160],
[250, 200],
[300, 240]
];
var base = d3.select("body").append("svg")
.attr("width", windowWidth)
.attr("height", windowLength);
var circles = base.selectAll("charlesDarwin")
.data(pos)
.enter()
.append("circle")
.attr("cx", function(d, i) {
return pos[i][0];
})
.attr("cy", function(d, i) {
return pos[i][1];
})
.attr("r", 20)
.style("fill", "00ACCD");
var timer = d3.timer(animate);
function animate() {
circles.attr("cx", function() {
if (+d3.select(this).attr("cx") > 500) {
timer.stop()
}
return +d3.select(this).attr("cx") + 1
});
};
<script src="https://d3js.org/d3.v4.min.js"></script>
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.
I'm trying to implement drill-down capability in zoom function, i.e., I want that my initial plot shows, for example, 50 points, and when the user makes zoom the number of points increases to 500.
My attempt consists in redraw inside the zoom function all the points and remove part of them when the zoom scale is under a threshold. As you can see in this JSFIDDLE, the implementation reproduces the drill-down capability.
However, I suspect that there is a more efficient way to implement the drill-down. Therefore, the question is if I'm in the correct way or there is a standard (more efficient and elegant) way for doing this effect.
My example code:
var width = 300,
height = 300;
var randomX = d3.random.normal(width / 2, 40),
randomY = d3.random.normal(height / 2, 40);
var data = d3.range(500).map(function() {
return [randomX(), randomY()];
});
var svg = d3.select("body").append("svg");
var zoomBehav = d3.behavior.zoom();
svg.attr("height", height)
.attr("width", width)
.call(zoomBehav
.scaleExtent([1, 10])
.on("zoom", zoom));
// Initial plot
d3.select("svg").selectAll("circle")
.data(data, function(d,i) {return i;})
.enter()
.append("circle")
.attr("r", 3)
.attr("cx", function(d) {return d[0]; })
.attr("cy", function(d) {return d[1]; })
.style("fill", "red");
d3.selectAll("circle")
.filter(function(d, i) {
if (zoomBehav.scale() < 2) { return i > 50; }
})
.remove();
function zoom(){
var selection = d3.select("svg")
.selectAll("circle")
.data(data, function(d,i) { return i; });
selection
.attr("cx", function(d) { return d3.event.translate[0] + d3.event.scale * d[0]; })
.attr("cy", function(d) { return d3.event.translate[1] + d3.event.scale * d[1]; });
selection.enter()
.append("circle")
.attr("r", 3)
.attr("cx", function(d) { return d3.event.translate[0] + d3.event.scale * d[0]; })
.attr("cy", function(d) { return d3.event.translate[1] + d3.event.scale * d[1]; })
.style("fill", "red");
d3.selectAll("circle")
.filter(function(d, i) {
if (zoomBehav.scale() < 2) { return i > 50; }
})
.remove();
}
If you're interested in dealing with semantic zoom of elements on an XY canvas, then you'll want to look into d3.geom.quadtree:
https://github.com/mbostock/d3/wiki/Quadtree-Geom
You can pass your points to a quadtree and they'll be spatially nested. Then, you can tie the nesting level to the zoom level and have automatic grid clustering. It's rather more involved than would fit into this answer, since you have to come up with mechanisms for representing the clustered points, and you'll also need to get into recursive functions to deal with the hierarchical level of points.
Here's an example using quadtrees for semantic zoom and clustering for mapping:
http://bl.ocks.org/emeeks/066e20c1ce5008f884eb