如何使用 d3.js 拖动和旋转正交地图(地球仪)
How to drag and rotate orthographic map (globe) using d3.js
设置: 我正在构建一个 globe 应用程序以更好地直观地表示世界各地区的数据。它是 d3.js 使用 topojson 构建几何图形的。
我目前正在实施 ivyywang here 实现的拖拽。
(不要迷失在数学函数中,除非你是状态:"math nerd guru")
我的项目目前是here。
问题: 我得到了正交投影的地球,并成功实现了拖动功能...除了。只要我的光标在某个国家/地区的边界内,我就只能单击并拖动地球。我如何投影我的 SVG 以便整个 canvas 响应我的拖动事件?
相关代码:
首先,我从 MySQL 请求中获取一些数据并将其存储在 countryStatistics 中。我运行它通过下面的函数来更好地索引它。
var countryStatistics = (returned from mySQL query)
//this function build dataById[] setting data keyed to idTopo
function keyIdToData(d){
countryStatistics.forEach(function(d) {
dataById[d.idTopo] = d;
});
}
function visualize(statisticalData, mapType){
//pass arguments each function call to decide what data to viasually display, and what map type to use
var margin = {top: 100, left: 100, right: 100, bottom:100},
height = 800 - margin.top - margin.bottom,
width = 1200 - margin.left - margin.right;
//a simple color scale to correlate to data
var colorScale = d3.scaleLinear()
.domain([0, 100])
.range(["#646464", "#ffff00"])
//create svg
var svg = d3.select("#map")
.append("svg")
.attr("height", height + margin.top + margin.bottom)
.attr("width", width + margin.left + margin.right)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
//here I attmpt to fill the svg with a different color. but it is unresponsive
.attr("fill", "blue")
正如您在此代码块末尾看到的那样,我在 SVG 元素上调用了 .attr("fill"... 但无法获得要渲染的颜色。也许这与我的原因有关光标在此 space.
中没有响应
继续...
//set projection type to 2D map or 3d globe dependinding on argument passed see function below
var projection = setMapType(mapType, width, height);
//a function to call on visualize() to set projection type for map style.
function setMapType(mapType, width, height) {
if(mapType === "mercator") {
let projection = d3.geoMercator()
.translate([ width / 2, height / 2 ])
.scale(180)
return projection;
}else if (mapType === "orthographic"){
let projection = d3.geoOrthographic()
.clipAngle(90)
.scale(240);
return projection;
}
//pass path lines to projections
var path = d3.geoPath()
.projection(projection);
//here I create and call the drag function only when globe projection is displayed elected
if(mapType == "orthographic"){
var drag = d3.drag()
.on("start", dragstarted)
.on("drag", dragged);
svg.call(drag);
}
//coordinate variables
var gpos0,
o0;
function dragstarted(){
gpos0 = projection.invert(d3.mouse(this));
o0 = projection.rotate();
}
function dragged(){
var gpos1 = projection.invert(d3.mouse(this));
o0 = projection.rotate();
var o1 = eulerAngles(gpos0, gpos1, o0);
projection.rotate(o1);
svg.selectAll("path").attr("d", path);
}
//load in the topojson file
d3.queue()
.defer(d3.json, "world110m.json")
.await(ready)
以上函数是指计算正交旋转所需的数学函数。您可以在第一个 link 中 ivyywang 的代码块中看到它们,在顶部。
function ready (error, data){
if (error) throw error;
//output data to see what is happening
console.log("topojson data: ")
console.log(data);
//I suspect there may be an issue with this code.
countries = topojson.feature(data, data.objects.countries)
//bind dataById data into countries topojson variable
.features.map(function(d) {
d.properties = dataById[d.id];
return d
});
console.log("countries:")
console.log(countries)
我怀疑上面的 countries 变量可能是罪魁祸首,主要是因为我不完全理解这段代码。在此变量中,我将 keyIdToData()
处理的 countryStatistics 数据作为嵌套对象 "properties" 与我的 topojson 数据绑定。我控制台记录它以查看数据。
svg.selectAll(".country")
.data(countries)
.enter().append("path")
.attr("class", "country")
.attr("d", path)
//make fill gradient depend on data
.attr("fill", function(countries){
//if no data, country is grey
if(countries.properties == undefined){
return "rgb(100 100 100)";
}
//else pass data to colorScale()
return colorScale(countries.properties.literacy)
})
.on('mouseover', function(d) {
//on hover set class hovered which simply changes color with a transition time
d3.select(this).classed("hovered", true)
})
.on('mouseout', function(d) {
d3.select(this).classed("hovered", false)
})
}
};
终于有了这个小功能
//this function build dataById[] setting data keyed to idTopo
function keyIdToData(d){
countryStatistics.forEach(function(d) {
dataById[d.idTopo] = d;
});
}
可能性: 看来我的 SVG 渲染不包括我的空(非国家)区域。我的 SVG 结构可能有问题吗?或者当我更改数据并将我的 dataById 附加到我的 topojson 时,我可能会干扰 SVG 构造?
感谢键盘忍者走到这一步。有什么想法吗?
问题
您的鼠标交互仅在父级 g
中绘制路径的位置,而不是两者之间的 space。您应用于 g
的填充被您应用于子路径的样式覆盖。
查看你所拥有的,你的变量 svg
持有 g
:
//create svg
var svg = d3.select("#map")
.append("svg")
...
.append("g") // return a newly created and selected g
...
.attr("fill", "blue") // returns same g
对于鼠标交互,g
只能与其中存在的元素进行交互。对于 g
,fill
属性不会直接执行任何操作,它仅应用于表示元素(和动画):
As a presentation attribute, it [fill] can be applied to any element but it
has effect only on the following eleven elements: <altGlyph>
,
<circle>
, <ellipse>
, <path>
, <polygon>
, <polyline>
, <rect>
, <text>
,
<textPath>
, <tref>
, and <tspan>
(MDN)
在 g
上使用 fill
是为子元素(您的路径)着色。虽然直接上色,但是蓝色没有视觉效果:
var g = d3.select("body")
.append("svg")
.append("g")
.attr("fill","orange");
// Inherit fill:
g.append("rect")
.attr("width",50)
.attr("height",50)
// Override inheritable fill:
g.append("rect")
.attr("x", 100)
.attr("width",50)
.attr("height",50)
.attr("fill","steelblue");
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
解决方案
您需要创建一个元素来与之交互,以便在当前没有路径的地方拖动。
现在,我认为您不想将整个 svg 背景设为蓝色,只需将地球上不属于某个国家/地区的部分设为蓝色。您可以使用 geojson 球体执行此操作。从技术上讲,d3 不是 geojson 规范的一部分,d3 将 shere 类型的 geojson 识别为覆盖整个星球(因此它不需要坐标)。在将国家/地区添加到地球之前,添加一个球体,这为拖动事件提供了一个与之交互的元素:
svg.append("path")
.attr("d", path({type:"Sphere"})
.attr("fill","blue");
这填满了海洋(和陆地),我们可以在上面附加国家。现在由于球体和国家都是同一个 g
的一部分,我们可以像现在一样在整个地球上进行拖动,但是现在没有鼠标交互不起作用的漏洞。
这是一个快速演示,其中包含正交投影和最基本的拖动功能:
var svg = d3.select("svg").append("g");
var projection = d3.geoOrthographic()
.translate([250,250])
var path = d3.geoPath().projection(projection);
d3.json("https://unpkg.com/world-atlas@1/world/110m.json").then( function(data) {
var world = {type:"Sphere"}
svg.append("path")
.datum(world)
.attr("d", path)
.attr("fill","lightblue");
svg.selectAll(null)
.data(topojson.feature(data,data.objects.land).features)
.enter()
.append("path")
.attr("fill","lightgreen")
.attr("d",path);
svg.call(d3.drag()
.on("drag", function() {
var xy = d3.mouse(this);
projection.rotate(xy)
svg.selectAll("path")
.attr("d",path);
}))
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<script src="https://unpkg.com/topojson-client@3"></script>
<svg width="500" height="500"></svg>
设置: 我正在构建一个 globe 应用程序以更好地直观地表示世界各地区的数据。它是 d3.js 使用 topojson 构建几何图形的。
我目前正在实施 ivyywang here 实现的拖拽。 (不要迷失在数学函数中,除非你是状态:"math nerd guru")
我的项目目前是here。
问题: 我得到了正交投影的地球,并成功实现了拖动功能...除了。只要我的光标在某个国家/地区的边界内,我就只能单击并拖动地球。我如何投影我的 SVG 以便整个 canvas 响应我的拖动事件?
相关代码:
首先,我从 MySQL 请求中获取一些数据并将其存储在 countryStatistics 中。我运行它通过下面的函数来更好地索引它。
var countryStatistics = (returned from mySQL query)
//this function build dataById[] setting data keyed to idTopo
function keyIdToData(d){
countryStatistics.forEach(function(d) {
dataById[d.idTopo] = d;
});
}
function visualize(statisticalData, mapType){
//pass arguments each function call to decide what data to viasually display, and what map type to use
var margin = {top: 100, left: 100, right: 100, bottom:100},
height = 800 - margin.top - margin.bottom,
width = 1200 - margin.left - margin.right;
//a simple color scale to correlate to data
var colorScale = d3.scaleLinear()
.domain([0, 100])
.range(["#646464", "#ffff00"])
//create svg
var svg = d3.select("#map")
.append("svg")
.attr("height", height + margin.top + margin.bottom)
.attr("width", width + margin.left + margin.right)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
//here I attmpt to fill the svg with a different color. but it is unresponsive
.attr("fill", "blue")
正如您在此代码块末尾看到的那样,我在 SVG 元素上调用了 .attr("fill"... 但无法获得要渲染的颜色。也许这与我的原因有关光标在此 space.
中没有响应继续...
//set projection type to 2D map or 3d globe dependinding on argument passed see function below
var projection = setMapType(mapType, width, height);
//a function to call on visualize() to set projection type for map style.
function setMapType(mapType, width, height) {
if(mapType === "mercator") {
let projection = d3.geoMercator()
.translate([ width / 2, height / 2 ])
.scale(180)
return projection;
}else if (mapType === "orthographic"){
let projection = d3.geoOrthographic()
.clipAngle(90)
.scale(240);
return projection;
}
//pass path lines to projections
var path = d3.geoPath()
.projection(projection);
//here I create and call the drag function only when globe projection is displayed elected
if(mapType == "orthographic"){
var drag = d3.drag()
.on("start", dragstarted)
.on("drag", dragged);
svg.call(drag);
}
//coordinate variables
var gpos0,
o0;
function dragstarted(){
gpos0 = projection.invert(d3.mouse(this));
o0 = projection.rotate();
}
function dragged(){
var gpos1 = projection.invert(d3.mouse(this));
o0 = projection.rotate();
var o1 = eulerAngles(gpos0, gpos1, o0);
projection.rotate(o1);
svg.selectAll("path").attr("d", path);
}
//load in the topojson file
d3.queue()
.defer(d3.json, "world110m.json")
.await(ready)
以上函数是指计算正交旋转所需的数学函数。您可以在第一个 link 中 ivyywang 的代码块中看到它们,在顶部。
function ready (error, data){
if (error) throw error;
//output data to see what is happening
console.log("topojson data: ")
console.log(data);
//I suspect there may be an issue with this code.
countries = topojson.feature(data, data.objects.countries)
//bind dataById data into countries topojson variable
.features.map(function(d) {
d.properties = dataById[d.id];
return d
});
console.log("countries:")
console.log(countries)
我怀疑上面的 countries 变量可能是罪魁祸首,主要是因为我不完全理解这段代码。在此变量中,我将 keyIdToData()
处理的 countryStatistics 数据作为嵌套对象 "properties" 与我的 topojson 数据绑定。我控制台记录它以查看数据。
svg.selectAll(".country")
.data(countries)
.enter().append("path")
.attr("class", "country")
.attr("d", path)
//make fill gradient depend on data
.attr("fill", function(countries){
//if no data, country is grey
if(countries.properties == undefined){
return "rgb(100 100 100)";
}
//else pass data to colorScale()
return colorScale(countries.properties.literacy)
})
.on('mouseover', function(d) {
//on hover set class hovered which simply changes color with a transition time
d3.select(this).classed("hovered", true)
})
.on('mouseout', function(d) {
d3.select(this).classed("hovered", false)
})
}
};
终于有了这个小功能
//this function build dataById[] setting data keyed to idTopo
function keyIdToData(d){
countryStatistics.forEach(function(d) {
dataById[d.idTopo] = d;
});
}
可能性: 看来我的 SVG 渲染不包括我的空(非国家)区域。我的 SVG 结构可能有问题吗?或者当我更改数据并将我的 dataById 附加到我的 topojson 时,我可能会干扰 SVG 构造?
感谢键盘忍者走到这一步。有什么想法吗?
问题
您的鼠标交互仅在父级 g
中绘制路径的位置,而不是两者之间的 space。您应用于 g
的填充被您应用于子路径的样式覆盖。
查看你所拥有的,你的变量 svg
持有 g
:
//create svg
var svg = d3.select("#map")
.append("svg")
...
.append("g") // return a newly created and selected g
...
.attr("fill", "blue") // returns same g
对于鼠标交互,g
只能与其中存在的元素进行交互。对于 g
,fill
属性不会直接执行任何操作,它仅应用于表示元素(和动画):
As a presentation attribute, it [fill] can be applied to any element but it has effect only on the following eleven elements:
<altGlyph>
,<circle>
,<ellipse>
,<path>
,<polygon>
,<polyline>
,<rect>
,<text>
,<textPath>
,<tref>
, and<tspan>
(MDN)
在 g
上使用 fill
是为子元素(您的路径)着色。虽然直接上色,但是蓝色没有视觉效果:
var g = d3.select("body")
.append("svg")
.append("g")
.attr("fill","orange");
// Inherit fill:
g.append("rect")
.attr("width",50)
.attr("height",50)
// Override inheritable fill:
g.append("rect")
.attr("x", 100)
.attr("width",50)
.attr("height",50)
.attr("fill","steelblue");
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
解决方案
您需要创建一个元素来与之交互,以便在当前没有路径的地方拖动。
现在,我认为您不想将整个 svg 背景设为蓝色,只需将地球上不属于某个国家/地区的部分设为蓝色。您可以使用 geojson 球体执行此操作。从技术上讲,d3 不是 geojson 规范的一部分,d3 将 shere 类型的 geojson 识别为覆盖整个星球(因此它不需要坐标)。在将国家/地区添加到地球之前,添加一个球体,这为拖动事件提供了一个与之交互的元素:
svg.append("path")
.attr("d", path({type:"Sphere"})
.attr("fill","blue");
这填满了海洋(和陆地),我们可以在上面附加国家。现在由于球体和国家都是同一个 g
的一部分,我们可以像现在一样在整个地球上进行拖动,但是现在没有鼠标交互不起作用的漏洞。
这是一个快速演示,其中包含正交投影和最基本的拖动功能:
var svg = d3.select("svg").append("g");
var projection = d3.geoOrthographic()
.translate([250,250])
var path = d3.geoPath().projection(projection);
d3.json("https://unpkg.com/world-atlas@1/world/110m.json").then( function(data) {
var world = {type:"Sphere"}
svg.append("path")
.datum(world)
.attr("d", path)
.attr("fill","lightblue");
svg.selectAll(null)
.data(topojson.feature(data,data.objects.land).features)
.enter()
.append("path")
.attr("fill","lightgreen")
.attr("d",path);
svg.call(d3.drag()
.on("drag", function() {
var xy = d3.mouse(this);
projection.rotate(xy)
svg.selectAll("path")
.attr("d",path);
}))
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<script src="https://unpkg.com/topojson-client@3"></script>
<svg width="500" height="500"></svg>