【问题标题】:Hexbins are not showing on geo tile map D3.JSHexbin 未在地理图块地图 D3.JS 上显示
【发布时间】:2019-07-05 01:52:35
【问题描述】:

我有一张使用 d3-tile 创建的地图。我添加了代码来显示根据经度和纬度从数据集中收集的 hexbin。但是hexbins没有显示。我只找到了在 hexbins 中绘制普通笛卡尔数据的示例,而不是在 d3-tile 制作的地图上放置 hexbins 的纬度经度。为什么我的 hexbin 没有显示?

这是我如何定义投影、图块和 hexbins:

var projection = d3.geo.mercator()
   .scale((1 << 22) / 2 / Math.PI) 
   .translate([width / 2, height / 2]);

var tile = d3.geo.tile()
  .size([width, height]);

// define hexbins
var hexbin = d3.hexbin()
   .size([width, height])
   .radius(4);

以下是我处理数据并将 hexbins 添加到地图的方式:

data.forEach(function (d) {
    d.lat = +d.lat;
    d.lon = +d.lon;
});

points = [];

// x,y maps to lng,lat - ref[2]
data.forEach(function (d) {
   d.lat = +d.lat;
   d.lon = +d.lon;
   var x = projection([d.lon, d.lat])[0];
   var y = projection([d.lon, d.lat])[1];
   points.push([x, y]);
});

// bin coords
var bins = hexbin(points);
var bins_n = []; // points per hexbin
bins.forEach(function (d) {
    bins_n.push(d.length);
});

// second of two scales for linear hexagon fill - ref[1]
var extent = d3.extent(bins_n);
var fill_scale2 = d3.scale.linear()
    .domain([extent[0], extent[1]])
    .range([0, 1]);

hex.selectAll(".hexagon")
    .data(hexbin(points))
    .enter()
    .append("path")
    .attr("class", function (d) { return "hexagon bin_" + d.length; })
    .attr("d", hexbin.hexagon())
    .attr("transform", function (d) {
         return "translate(" + d.x + "," + d.y + ")";
     })
     .style("fill", function (d) {
        return fill_scale1(fill_scale2(d.length));
     });

【问题讨论】:

  • 你是如何定义你的投影的?
  • var projection = d3.geo.mercator() .scale((1
  • 这是一个非常大的比例值 - 如果您记录投影的 x,y 坐标(或 points 在您的情况下),您可能会看到不在范围内的坐标你的svg。 d3-tile 的比例通常为1/Math.PI/2:它将世界投影到一个 1 像素的正方形,然后使用缩放在屏幕上平移和缩放。这不是最直观的方法。投影会导致您的问题,但是通过缩放,六边形比平时要复杂一些。我现在有点忙,过会再仔细看看。

标签: d3.js hexagonal-tiles


【解决方案1】:

我将使用这个example 来构建我的答案。这个可缩放的例子,与这个example 不同,是可缩放的。

投影比例

首先,我将使用上面的示例来解释比例。它使用起始比例为1/tau 的投影:世界的 2 π 弧度被拉伸超过一个像素。平移是 [0,0],因此 0°N,0°E 位于 SVG 的 [0,0]。地图的缩放和平移由 d3.zoom 管理:

projection.scale(transform.k / Math.PI / 2)
  .translate([transform.x, transform.y]);

因为k代表缩放系数,我们的起始地图宽度为1,k代表地图宽度和高度。除以 tau,我们得到地图的每个像素对应于地球的每个弧度。翻译使地图居中。

在示例中看不到任何 hexbin 的原因是因为您使用的比例尺将地球延伸到 4194304 像素宽 (1

也供参考:地图比例和地图宽度的关系在所有地图投影中并不一致

添加 hexbins(固定)

如果我们希望使用无论缩放如何都保持相同地理大小的 bin 进行六边形 binning,我们可以设置半径和范围以反映我们的初始投影(在应用缩放之前):

var hexbin = d3.hexbin()
    .radius(0.01)
    .extent([[-0.5, -0.5], [0.5, 0.5]]); 

然后我们可以使用初始投影传递投影点,并根据缩放变换六边形,同时根据缩放比例缩放六边形的笔画宽度:

var width = Math.max(960, window.innerWidth),
    height = Math.max(500, window.innerHeight);
	
var svg = d3.select("svg")
    .attr("width", width)
    .attr("height", height);
	
// Projection details:
var projection = d3.geoMercator()
    .scale(1 / Math.PI / 2)
    .translate([0, 0]);

var center = projection([0,0]);	

var tile = d3.tile()
    .size([width, height]);

// Zoom details:
var zoom = d3.zoom()
    .scaleExtent([1 << 11, 1 << 14])
    .on("zoom", zoomed);

// Layers for map
var raster = svg.append("g");  // holds tiles
var vector = svg.append("g");  // holds hexagons
var hexes;					   // to hold hexagons

// Hexbin:
var hexbin = d3.hexbin()
    .radius(0.01)
    .extent([[-0.5, -0.5], [0.5, 0.5]]); // extent of the one pixel projection.

var color = d3.scaleLinear()
	.range(["rgba(255,255,255,0.1)","orange"])
    .domain([0, 5]);

d3.json("https://unpkg.com/world-atlas@1/world/110m.json", function(error, world) {
  
 // Create some hexbin data:
 var land = topojson.feature(world, world.objects.land);
 var data = d3.range(500).map(function(d) {
	while(true) {
		var lat = Math.random() * 170 - 70;
		var lon = Math.random() * 360 - 180;
		if(d3.geoContains(land,[lon,lat])) return projection([lon,lat]);
	}
 })
 
 // Create hex bins:
 hexes = vector.selectAll()
  .data(hexbin(data))
  .enter()
  .append("path")
	.attr("d", hexbin.hexagon(0.0085))
  .attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; })
  .attr("fill", function(d) { return color(d.length); })
	.attr("stroke","black")

  svg
   .call(zoom)
   .call(zoom.transform, d3.zoomIdentity
   .translate(width / 2, height / 2)
   .scale(1 << 11)
   .translate(-center[0], -center[1]));
 
});		  
		  
function zoomed() {
  var transform = d3.event.transform;
  
  var tiles = tile
      .scale(transform.k)
      .translate([transform.x, transform.y])
      ();

  // Update projection
  projection
      .scale(transform.k / Math.PI / 2)
      .translate([transform.x, transform.y]);

  // Update vector holding hexes:
  vector.attr("transform","translate("+[transform.x,transform.y]+")scale("+transform.k+")" )
    .attr("stroke-width", 1/transform.k);

  // Update tiles:
  var image = raster
      .attr("transform", stringify(tiles.scale, tiles.translate))
    .selectAll("image")
    .data(tiles, function(d) { return d; });

  image.exit().remove();

  image.enter().append("image")
      .attr("xlink:href", function(d) { return "http://" + "abc"[d[1] % 3] + ".tile.openstreetmap.org/" + d[2] + "/" + d[0] + "/" + d[1] + ".png"; })
      .attr("x", function(d) { return d[0] * 256; })
      .attr("y", function(d) { return d[1] * 256; })
      .attr("width", 256)
      .attr("height", 256);
}

function stringify(scale, translate) {
  var k = scale / 256, r = scale % 1 ? Number : Math.round;
  return "translate(" + r(translate[0] * scale) + "," + r(translate[1] * scale) + ") scale(" + k + ")";
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/d3-tile.v0.0.min.js"></script>
<script src="https://d3js.org/d3-hexbin.v0.2.min.js"></script>
<script src="https://unpkg.com/topojson-client@3"></script>
<svg></svg>

添加 hexbins(使用缩放更新)

但是,如果我们想在放大代码时更改 bin 比例,可能会更容易一些,但计算上会更复杂。为此,我们在应用当前投影后,根据点的投影坐标重新计算每次缩放的 hexbin:

var hexbin = d3.hexbin()
 .radius(30)
 .extent([[0,0], [width,height]]) // extent of projected data (displayed)
 .x(function(d) { return projection(d)[0]; })
 .y(function(d) { return projection(d)[1]; })

范围和半径反映了整个 SVG 范围 - 应用缩放后我们投影数据的可见范围 - 我们要添加六边形的范围。下面我重新计算每个缩放/平移的十六进制:

var width = Math.max(960, window.innerWidth),
    height = Math.max(500, window.innerHeight);
	
var svg = d3.select("svg")
    .attr("width", width)
    .attr("height", height);
	
// Projection details:
var projection = d3.geoMercator()
    .scale(1 / Math.PI / 2)
    .translate([0, 0]);

var center = projection([0,0]);	

var tile = d3.tile()
    .size([width, height]);

// Zoom details:
var zoom = d3.zoom()
    .scaleExtent([1 << 11, 1 << 14])
    .on("zoom", zoomed);

// Layers for map
var raster = svg.append("g");  // holds tiles
var vector = svg.append("g");  // holds hexagons
var hexes;					   // to hold hexagons

// Hexbin:
var hexbin = d3.hexbin()
    .radius(30)
    .extent([[0,0], [width,height]]) // extent of projected data (displayed)
	.x(function(d) { return projection(d)[0]; })
	.y(function(d) { return projection(d)[1]; })

var color = d3.scaleLinear()
	.range(["rgba(255,255,255,0.1)","orange"])
    .domain([0, 5]);
	
var data;

d3.json("https://unpkg.com/world-atlas@1/world/110m.json", function(error, world) {
  
 // Create some hexbin data:
 var land = topojson.feature(world, world.objects.land);
 data = d3.range(500).map(function(d) {
	while(true) {
		var lat = Math.random() * 170 - 70;
		var lon = Math.random() * 360 - 180;
		if(d3.geoContains(land,[lon,lat])) return [lon,lat];
	}
 })
 
  svg
   .call(zoom)
   .call(zoom.transform, d3.zoomIdentity
   .translate(width / 2, height / 2)
   .scale(1 << 11)
   .translate(-center[0], -center[1]));
 
});		  
		  
function zoomed() {
  var transform = d3.event.transform;
  
  var tiles = tile
      .scale(transform.k)
      .translate([transform.x, transform.y])
      ();

  // Update projection
  projection
      .scale(transform.k / Math.PI / 2)
      .translate([transform.x, transform.y]);

  hexes = vector.selectAll("path")
   .data(hexbin(data)) ;
   
   hexes.exit().remove();
   
   hexes.enter()
   .append("path")
   .merge(hexes)
   .attr("d", hexbin.hexagon(29))
   .attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; })
   .attr("fill", function(d) { return color(d.length); })
   .attr("stroke","black")

  // Update tiles:
  var image = raster
      .attr("transform", stringify(tiles.scale, tiles.translate))
    .selectAll("image")
    .data(tiles, function(d) { return d; });

  image.exit().remove();

  image.enter().append("image")
      .attr("xlink:href", function(d) { return "http://" + "abc"[d[1] % 3] + ".tile.openstreetmap.org/" + d[2] + "/" + d[0] + "/" + d[1] + ".png"; })
      .attr("x", function(d) { return d[0] * 256; })
      .attr("y", function(d) { return d[1] * 256; })
      .attr("width", 256)
      .attr("height", 256);
}

function stringify(scale, translate) {
  var k = scale / 256, r = scale % 1 ? Number : Math.round;
  return "translate(" + r(translate[0] * scale) + "," + r(translate[1] * scale) + ") scale(" + k + ")";
}
<svg></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/d3-tile.v0.0.min.js"></script>
<script src="https://d3js.org/d3-hexbin.v0.2.min.js"></script>
<script src="https://unpkg.com/topojson-client@3"></script>

这两个例子都是在陆地上随机创建一些数据,这是加载缓慢的主要原因

最后的想法

这两个例子都有很多不足之处,用 d3-tile 和六边形组织坐标空间的方法比可能的不太直观 - 可能需要一点时间来适应。但是,目前没有很多替代方案。

【讨论】:

  • 我能够实施您的解决方案,谢谢!但现在我坚持用我自己的数据来实现它。这就是我收集数据的方式。
  • d3.json("all.json", function (data) { console.log(data); // 将 lat/lng 转换为数值 data.forEach(function (d) { d.lat = +d.lat; d.lon = +d.lon; }); points = []; // x,y 映射到 lng,lat - ref[2] data.forEach(function (d) { d.lat = +d.lat; d.lon = +d.lon; var x = projection([d.lon, d.lat])[0]; var y = projection([d.lon, d.lat])[ 1]; points.push([x, y]); });
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-05-19
  • 2017-07-24
  • 1970-01-01
  • 1970-01-01
  • 2016-04-10
  • 1970-01-01
相关资源
最近更新 更多