leaflet-d3 icon indicating copy to clipboard operation
leaflet-d3 copied to clipboard

Fixed hex size

Open Cadrach opened this issue 9 years ago • 10 comments

Hi,

I am trying to implement the hexbin to a project of mine, and was wondering if it was possible to have a fixed hexagon size? At the moment, the size is in pixel, and the hexagons resize with zoom/dezoom.

How should I proceed if I wanted to have the hexagon at a fixed 200 meters radius?

Thanks a lot,

Cadrach avatar Sep 24 '15 13:09 Cadrach

This is something I need as well. FWIW, I may be able to code up a solution to this if you're not already working on it.

chriswilley avatar Apr 29 '17 01:04 chriswilley

I hadn't prioritized this cause we never needed anything like it for our projects. If you want to put in a PR go for it. Even if it just illustrates how to do it, I can use it as an basis for integrating it into the plugin.

reblace avatar Apr 29 '17 01:04 reblace

I will be out of the country for most of May, but can dig back into this when I return. I thought I would give you some more background and a possible direction if you should want to tinker with it:

My use case is plotting sensor data at a site using various means (handheld, vehicle mounted and drone mounted). Because of what we're doing, the binning of data is only relevant for the radius of the sensor (say for example: 10 meters). d3-hexbin, as you know, draws hexagons at a fixed radius in pixels on a map, but we need to show hexagons at a fixed radius in meters on the Earth. Otherwise at most zoom levels the visualization is hard to interpret/misleading.

I was thinking that your recent update provides a possible solution, using the radiusValue() method. Check out this post from SO that discusses how to determine the number of meters per pixel at a particular zoom level in Leaflet. The equation posted by synkyo could possibly be used in radiusValue(), which I assume fires whenever the hexbins are redrawn (such as when the zoom level is changed).

I won't have time before I leave to try this out, but I thought I would run it past you to see what you thought.

Thanks.

chriswilley avatar Apr 30 '17 18:04 chriswilley

Ok, I'll think about it a bit and see what I come up with.

One issue is that the hexbins are consistently sized in pixels, but not in km or miles due to the deformation from the map's projection. It would probably work fine with a WMS layer using a projection that maintains the pixel/km ratio. But, if we wanted it to be accurate with other projections that don't follow that rule, we may need to deform the hexbins themselves.

reblace avatar May 01 '17 19:05 reblace

Just got back on Friday and am digging into this again. Here's where I am so far.

I've added a radiusValue() function as follows (hexLayer is the hexbinLayer object):

hexLayer.radiusValue(function(d, i) {
    var mPP = 40075016.686 * Math.abs(Math.cos(this._map.getCenter().lat * 180/Math.PI)) / Math.pow(2, this._map.getZoom()+8);
    var radius = 10 / mPP;
    return radius;
});

This assumes that the sensor radius is 10 meters. By writing the radius value out to the console I can see that it's changing when I zoom in and out but I'm not seeing any change in the hexagons themselves. As a test I changed radius to equal d.length and the layer behaved as expected.

Through this experiment I think I better understand how radiusValue works: like colorValue, the radiusValue function provides a relative radius within a range. Since all the radius values are equal (a set number of meters per pixel), hexbinLayer draws them with the default radius of 12 (or whatever radius value is set in options). If instead I add a radiusRange option, the hexagons will still be drawn all the same size based on where the meters per pixel value happens to fall in that range.

So I guess this is where I'm stuck: how to draw hexagons using hexbinLayer that are of a fixed size relative to the zoom level? At this point I'm assuming I've exhausted what the API can deliver and would need to modify the JavaScript source code in some way. Let me know if you agree, and if you have any suggestions where to start.

Thanks.

chriswilley avatar May 22 '17 11:05 chriswilley

So, an important distinction here is you want to change the radius of the hex grid as you zoom. .radiusValue is used to change the size of the drawn hexagon inside the grid. The hex grid itself is regenerated every time you zoom, but it uses the configured pixel radius set by .radius(). This is why it always stays the same pixel size as you zoom.

I think what you want to do in this situation is change the hexbin grid radius whenever the map is zoomed so that the HexbinLayer regenerates the hexbin grid with the new pixel radius. I'm not sure how easy it will be to do this imperatively using the events, so we might need to change the API a little bit to support setting the radius getter/setter to allow either a number or a function that returns a number.

So, basically I'd suggest you try something like this:

map.on('zoomend', function() {
   var radiusInPixels = ...;
   hexLayer.radius(radiusInPixels);
});

and if that doesn't work, let's try changing the API to allow a function for radius and do this:

hexLayer.radius(function() {
   // Return radius in pixels
});

reblace avatar May 22 '17 15:05 reblace

Your first suggestion changes something during rendering, but not the hexagons themselves. The first image below shows how the layer looks normally, the second image shows the same layer using the radius() function. Each hexagon still has a radius of 12 pixels, but something about the way they're rendered (or binned?) has changed.

mpp1 mpp2

I did some hacking around the HexbinLayer.js script to see if I could get it to work; basically I set a temporary radius value in _createHexagons() and applied the value in join.enter():

var newRadius = ...;

join.enter().append('path')...
    .transition().duration...
        .attr('d'), function(d) {
            return that._hexLayout.hexagon(newRadius);
        }

Obviously this is not the right way to do it, but it does adjust the radius of the hexagons based on zoom level.

What I'm not sure about now is whether this is affecting the binning of data (i.e.: do the newly rendered hexagons still accurately reflect the underlying data?). I'm working with a team that's responsible for the data component; I'm waiting to hear back from them to see if the new output looks right (image below for same dataset as above). More to follow after I speak with them.

mpp3

Let me know if you want to move this conversation to another medium (email, Slack, etc.) while we hash this out. Thanks.

chriswilley avatar May 22 '17 15:05 chriswilley

Any progress on this? For my purposes I'd like to maintain the default behaviour up to a certain zoom level, then grow with the zoom level after that. Should be simpler to do perhaps?

Could I maybe respond to the map zoom level change and dynamically change the hex radius settings prior to rendering?

whittaker007 avatar Sep 19 '18 01:09 whittaker007

I did resolve this for a project I work on. The code is in my fork of this repo (https://github.com/chriswilley/leaflet-d3). I should note that I have not merged changes from this repo into mine in a while, and have not had a chance to see if I can merge them cleanly at this point. Let me know if you are able/not able to get it working.

chriswilley avatar Sep 19 '18 15:09 chriswilley

No worries, I actually kind of solved it myself, though it's not quite as clean as I'd like. Essentially I'm using map.on("zoomend") to catch the zoom, check the zoom level, compare it to my max level, and adjust the radius accordingly.

Unfortunately simply updating the radius variables in the hexlayer settings directly does not work as those values don't seem to be used when re-rendering the map. So instead every time the map is zoomed I have to erase the SVG from the layer, remove the layer from the map, and add a new one with the updated settings and feed it the data which I've stored from the Ajax callback. Like this:

map.on('zoomend', function(e) {
  var zoom = e.target._zoom;
  var maxZoom = 15;
  var zoomDiff = zoom - maxZoom;
  var newOptions = defaultOptions;

  d3.select("svg").remove();
  map.removeLayer(hexLayer);

  var newRadius = defaultRadius;
  if (zoomDiff >= 1) {
    newRadius = defaultRadius * (2**zoomDiff);
  }

  newOptions.radius = newRadius;
  newOptions.radiusRange = [newRadius, newRadius];
  hexLayer = createHexLayer(newOptions, map);
  hexLayer.data(loaded_data);
});

Fortunately destroying and re-creating the hex layer every time the map is zoomed doesn't seem to have a noticeable performance hit.

whittaker007 avatar Sep 19 '18 21:09 whittaker007