Home > Software design >  d3.js proportional symbol map: setting radius of a circle based on data value
d3.js proportional symbol map: setting radius of a circle based on data value

Time:12-07

I'm following this great example of how to make a map using d3.js with mapbox:

enter image description here

But it still suffers as the value is not proportionate to the values area but the radius. Well, the radius is almost proportionate to value, but since our the lower limit of our range is 2, this isn't true. I've set a minimum value for the radius with the lower bounds of the scale's range to ensure that values close to zero are still visible (otherwise the smallest circle would be invisible essentially as it would have a radius of 1/25 pixels).

Instead, given that circles' areas are proportionate to the radius squared, we could use a square root scale to make the circles area (more) proportionate to their value:

let scale = d3.scaleSqrt().domain([0,500]).range([2,20]);
let data = [1, 10, 100, 250, 500];

let g = d3.select("body").append("svg")
  .selectAll("g")
  .data(data)
  .enter()
  .append("g")
  .attr("transform", (d,i)=>"translate(" (i*50 50) ", 50)")
  
g.append("circle")
  .attr("r", scale)
  .attr("fill", "#ccc")
  
g.append("text")
  .text(d=>d)
  .attr("fill","black")
  .attr("text-anchor","middle")
  .attr("dy", -20);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>

enter image description here

Here we have areas that are much closer to being proportionate to the underlying values (again, since our range doesn't start at 0 this isn't quite true). We could set the lower limit of the range to zero, which would result in much more honest representations of the data in terms of area, however, depending on what you are trying to convey it might be appropriate to leave a minimum radius a bit above zero, if we change it to zero, we essentially lose the smallest value (1):

let scale = d3.scaleSqrt().domain([0,500]).range([0,20]);
let data = [1, 10, 100, 250, 500];

let g = d3.select("body").append("svg")
  .selectAll("g")
  .data(data)
  .enter()
  .append("g")
  .attr("transform", (d,i)=>"translate(" (i*50 50) ", 50)")
  
g.append("circle")
  .attr("r", scale)
  .attr("fill", "#ccc")
  
g.append("text")
  .text(d=>d)
  .attr("fill","black")
  .attr("text-anchor","middle")
  .attr("dy", -20);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>

enter image description here

We can go one step further and use a scale that finds the third or higher root, which will lose proportionality, but might be useful in showing differences in value for the lower portion of the domain while homogenizing values in the upper portion of the domain:

let scale = d3.scalePow().exponent(.25).domain([0,500]).range([0,20]);
let data = [1, 10, 100, 250, 500];

let g = d3.select("body").append("svg")
  .selectAll("g")
  .data(data)
  .enter()
  .append("g")
  .attr("transform", (d,i)=>"translate(" (i*50 50) ", 50)")
  
g.append("circle")
  .attr("r", scale)
  .attr("fill", "#ccc")
  
g.append("text")
  .text(d=>d)
  .attr("fill","black")
  .attr("text-anchor","middle")
  .attr("dy", -20);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>

enter image description here

There are more scales of course, but for circles, generally a square root scale is appropriate, especially with zero as the lower bounds for domain and range as then circles are proportionate to value. However, there are cases of course for using a different scale, non-zero bounds for domain and/or range.

CodePudding user response:

First off, I'd point out that you can certainly use the Mapbox API directly to lay down points and size them according to data. To do so requires understanding the Mapbox API writing Mapbox expressions. An advantage (particularly, for large data sets) is that your map will be much more responsive, since Mapbox will use the GPU to handle interaction.

Of course, if you're familiar with D3 and your data is reasonably sized, then it might make perfect sense to lay down the SVG on top of the map and manipulate the points in the SVG with D3. If you're code is set up correctly, you should be able to do something like:

svg
  .selectAll("circle")
  .data(data)
  .join("circle")
  .attr("r", (d) => d.size)

Here's an Observable notebook where both techniques are illustrated: https://observablehq.com/d/fd3634ff22dc033a

Finally, I think it's worth pointing out that, while very cool, the webpage you refer to is a bit old and uses outdated versions of both Mapbox and D3. Mapbox, in particular, has changed quite a bit since then with the move to V2.

  • Related