Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

D3: hide voronoi strokes that fall 'in the sea'

What would be the least 'expensive' way to hide all voronoi strokes that fall in the sea?

The strokes (and polygon fill) that run on land should be visible, whilst those that are on sea should be hidden from view. I think my objective should be clear from the picture below:

enter image description here

I can think of two options:

  1. Somehow 'reclip' the voronoi polygons to the underlying country ('land') polygons. That sounds super CPU intensive. This is not a good option, so let's not go there.
  2. Overlay a 'sea' polygon on top of the voronoi tesselation. That would be super effective visually and exactly what I need. How would I go about calculating the new sea polygon from a base map of countries? (e.g. this jsfiddle with a geoJSON D3 map) I have multiple maps with various levels of polygon complexity, so I need a fool-proof way to build that map.

Any ideas?

like image 357
Noobster Avatar asked Feb 22 '18 07:02

Noobster


1 Answers

A simple option, which requires none of:

  • an ocean polygon
  • redrawing land polygons / merging land polygons
  • clip paths

is to use an svg pattern. This may sound a bit odd, and I'm not positive as to what the performance implications are, but a pattern of your voronoi diagram can be used to fill your countries/features (preserving boundaries and allowing features to be drawn once and only once).

This requires that the polygon fill is not geographic feature dependent but rather voronoi dependent - your image uses the same fill for each polygon, but your question text might be suggesting this is not the case

To use a pattern like this, create a pattern that is the same width and height as your map. In the pattern, place the voronoi diagram paths. Lastly, set the fill of each feature to the pattern:

var svg = d3.select("svg"),
    width = +svg.attr("width"),
    height = +svg.attr("height");

var projection = d3.geoMercator()
  .scale((width - 3) / (2 * Math.PI))
  .translate([width / 2, height / 2]);

var path = d3.geoPath()
  .projection(projection);

var graticule = d3.geoGraticule();

d3.json("https://unpkg.com/world-atlas@1/world/50m.json", function(error, world) {
  if (error) throw error;
  
  // random points:
  var circles = d3.range(20).map(function() {
    return {
      x: Math.round(Math.random() * width),
      y: Math.round(Math.random() * height)
    };
  });
	
  // voronoi:
  var voronoi = d3.voronoi()
    .x(function(d) { return d.x; })
    .y(function(d) { return d.y; })
    .extent([[-1, -1], [width + 1, height + 1]]);	
      
      
  // pattern:
  var pattern = svg.append("defs")
    .append("pattern")
    .attr("id","voronoi")
    .attr("patternUnits","userSpaceOnUse")
    .attr("width", width)
    .attr("height",height)
    .attr("x",0)
    .attr("y",0)
	  	
  pattern.selectAll("path")
    .data(voronoi.polygons(circles))
    .enter()
    .append("path")
    .attr("d", renderCell)
    .attr("fill",function(d,i) { return d3.schemeCategory20[i]; })
		
  // append paths as normal:
  var features = svg.selectAll(null)
    .data(topojson.feature(world,world.objects.countries).features)
    .enter()
    .append("path")
    .attr("class", "boundary")
    .attr("d", path)
    .attr("fill","url(#voronoi)");  // fill with pattern
	
  function renderCell(d) {
    return d == null ? null : "M" + d.join("L") + "Z";
  }	
	
	 
});
.boundary {
  stroke: black;
  stroke-width: 1px;
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://unpkg.com/topojson-client@3"></script>
<svg width="600" height="400"></svg>
like image 131
Andrew Reid Avatar answered Oct 25 '22 17:10

Andrew Reid