Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixed hex size #14

Open
Cadrach opened this issue Sep 24, 2015 · 10 comments
Open

Fixed hex size #14

Cadrach opened this issue Sep 24, 2015 · 10 comments

Comments

@Cadrach
Copy link

Cadrach commented Sep 24, 2015

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,

@chriswilley
Copy link

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.

@reblace
Copy link
Contributor

reblace commented Apr 29, 2017

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.

@chriswilley
Copy link

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.

@reblace
Copy link
Contributor

reblace commented May 1, 2017

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.

@chriswilley
Copy link

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.

@reblace
Copy link
Contributor

reblace commented May 22, 2017

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
});

@chriswilley
Copy link

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.

@whittaker007
Copy link

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?

@chriswilley
Copy link

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.

@whittaker007
Copy link

whittaker007 commented Sep 19, 2018

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants