4
\$\begingroup\$

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>

\$\endgroup\$
5
\$\begingroup\$

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 the call, 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 and const (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>

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.