Problem
I’m fairly new to d3 and am wanting to create a visualization that will allow a user to check whether or not a device will be in range of a bluetooth speaker.
I was able to hack together most of what I want, but am not sure if I’m following d3 best practices. This will be eventually integrated with React. Any feedback on style/efficiency is appreciated.
(Note: you’ll want to view in the full page)
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height"),
radius = 8,
unitRadius = 64;
var points = d3.range(10).map(function() {
return {
x: Math.round(Math.random() * (width - radius * 2) + radius),
y: Math.round(Math.random() * (height - radius * 2) + radius)
};
});
var circle = d3.range(1).map(function() {
return {
x: width / 2,
y: height / 2
};
});
var points = svg
.selectAll("circle.point")
.data(points)
.enter()
.append("circle")
.attr("class", "point")
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
})
.attr("r", radius)
.style("fill", "red");
var circle = svg
.selectAll("circle.unit")
.data(circle)
.enter()
.append("circle")
.attr("class", "overlay")
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
})
.attr("r", unitRadius)
.style("fill", "blue")
.call(
d3
.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended)
);
function dragstarted(d) {
d3.select(this)
.raise()
.classed("active", true);
}
function dragged(d, i) {
d3.select(this)
.attr("cx", (d.x = d3.event.x))
.attr("cy", (d.y = d3.event.y));
var inside = d3.selectAll("circle.point").style("fill", function(p) {
var x = d.x - p.x;
var y = d.y - p.y;
var dis = Math.sqrt(x * x + y * y);
if (dis <= Math.abs(unitRadius - radius)) {
return "green";
} else {
return "red";
}
});
}
function dragended(d) {
d3.select(this).classed("active", false);
}
.overlay {
fill-opacity: 0.1;
}
<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.js"></script>
<svg width="960" height="500"></svg>
Solution
Overall you have a pretty nice code for someone who is “fairly new to D3”, good on you.
The proposed changes here are few:
The big circle
You have just one big circle (the blue one). Therefore, you don’t need that mix of d3.range
and map
for its datum. Also, you don’t need an enter selection, since that’s just one circle.
So, you can do:
var circleDatum = {
x: width / 2,
y: height / 2
};
And:
var circle = svg
.append("circle")
.datum(circleDatum)
//etc...
Mind the fact that you’re using circle
as the name for two different things!
Don’t re-select the same thing again…
Inside the dragged function you have this:
var inside = d3.selectAll("circle.point").style("fill", function(d){
//etc...
Which basically re-selects all .point
circles again. I wrote re-select because you already have a selection for them: points
.
Therefore, just do:
points.style("fill", function(d){
Use selectAll(null) in your enter selections
If you don’t plan to update the chart, that is, if you don’t want to have an update and exit selections, you don’t need to select anything in your enter selections.
That being said, you can do:
var points = svg.selectAll(null)
//etc...
By using selectAll(null)
you’ll have a cleaner and faster code (not noticeable since you have soo few elements here).
For reading more about selectAll(null)
have a look my explanation here: Selecting null: what is the reason behind ‘selectAll(null)’ in D3.js?.
Minor changes
-
Instead of putting all the
d3.drag
inside thecall
, you can do:var drag = d3.drag()
And then:
.call(drag)
-
If you don’t use the second argument (the index), don’t write the parameter. So, instead of:
function dragged(d, i) {
Just do:
function dragged(d) {
-
If you like ternary operators, the whole
if
can be just:return dis <= Math.abs(unitRadius - radius) ? "green" : "red";
-
You can use
Math.hypot
for the hypothenuse (note: this doesn’t work on IE)var dis = Math.hypot(x, y);
Also, if you don’t mind old IE versions, you can use
let
andconst
(accordingly) instead of var. -
Sometimes users don’t know that they can interact with the visualisation, and a lot of people don’t read the instructions/description. So, adding a visual clue may be useful. For instance, changing the cursor when you hover over the big circle helps to indicate that it is draggable:
.style("cursor", "pointer")
Demo
Here is your code with those changes:
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height"),
radius = 8,
unitRadius = 64,
drag = d3.drag();
var points = d3.range(10).map(function() {
return {
x: Math.round(Math.random() * (width - radius * 2) + radius),
y: Math.round(Math.random() * (height - radius * 2) + radius)
};
});
var circleDatum = {
x: width / 2,
y: height / 2
};
var points = svg.selectAll(null)
.data(points)
.enter()
.append("circle")
.attr("class", "point")
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
})
.attr("r", radius)
.style("fill", "red");
var circle = svg.append("circle")
.datum(circleDatum)
.attr("class", "overlay")
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
})
.attr("r", unitRadius)
.style("fill", "blue")
.style("cursor", "pointer")
.call(drag.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended)
);
function dragstarted(d) {
d3.select(this)
.raise()
.classed("active", true);
}
function dragged(d) {
d3.select(this)
.attr("cx", (d.x = d3.event.x))
.attr("cy", (d.y = d3.event.y));
points.style("fill", function(p) {
var x = d.x - p.x;
var y = d.y - p.y;
var dis = Math.hypot(x, y);
return dis <= Math.abs(unitRadius - radius) ? "green" : "red";
});
}
function dragended(d) {
d3.select(this).classed("active", false);
}
.overlay {
fill-opacity: 0.1;
}
<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.js"></script>
<svg width="960" height="500"></svg>