创建将来自两个具有公共列的文件的数据组合在一起的地图 - 但数据并不一一匹配
我正在使用两个数据文件创建地图。一个数据文件包含位置的经纬度。另一个数据文件包含这些位置的人口。两者都有一列位置代码。 只要两个文件包含相同的代码,就可以。但是,我的人口数据集仅包含 部分 位置代码的信息。所以我收到一个错误:
test.html:75 Uncaught TypeError: 无法读取未定义的属性“pop”
我该如何避免此错误?
我的完整代码如下。
这里还有一个包含所有 CSV/文件的 Plunker: https://plnkr.co/edit/pvPv2To72OiSOPaz
<!DOCTYPE html>
<meta charset="utf-8">
<style>
.states {
fill: #eee;
stroke: #999;
stroke-linejoin: round;
}
</style>
<body>
<div class="g-chart"></div>
</body>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://unpkg.com/[email protected]/dist/geo-albers-usa-territories.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/queue-async/1.0.7/queue.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/topojson/1.6.19/topojson.min.js"></script>
<script>
var margin = { top: 10, left: 10, bottom: 10, right: 10 },
width = window.outerWidth,
width = width - margin.left - margin.right,
mapRatio = .5,
height = width * mapRatio;
var projection = geoAlbersUsaTerritories.geoAlbersUsaTerritories()
.scale(width)
.translate([width / 2, height / 2]);
var path = d3.geoPath()
.projection(projection);
var map = d3.select(".g-chart").append("svg")
.style('height', height + 'px')
.style('width', width + 'px')
.call(d3.zoom().on("zoom", function () {
map.attr("transform", d3.event.transform)
d3.selectAll()
}))
.append("g");
queue()
.defer(d3.json, "us.json")
.defer(d3.csv, "locations.csv")
.defer(d3.csv, "populations.csv")
.await(ready);
function ready(error, us, loc, pop) {
if (error) throw error;
const data = pop.sort((a, b) => +b.population - +a.population)
console.log('populations', data)
const locations = loc.filter(d => d.latitude !== '' && d.longitude !== '')
const maxPop = d3.max(data, d => +d.population)
console.log('maxPop', maxPop)
const newDict = {};
data.forEach(function (d) {
d.population = +d.population;
newDict[d.location_code] = { pop: d.population };
})
map.append("g")
.attr("class", "states")
.selectAll("path")
.data(topojson.feature(us, us.objects.states).features)
.enter().append("path")
.attr("d", path);
const location_data = locations.sort((a, b) => newDict[b.code].pop - newDict[a.code].pop)
location_data.forEach(function (d) {
d.pop = newDict[d.code].pop
})
console.log('location_data', location_data)
map.append('g')
.attr('class', 'location')
.selectAll("circle")
.data(location_data)
.enter()
.append("circle")
.attr("cx", function (d) {
return projection([d.longitude, d.latitude])[0];
})
.attr("cy", function (d) {
return projection([d.longitude, d.latitude])[1];
})
.attr('r', 10)
.style("fill", "navy")
.style("opacity", 0.5)
}
解决方案 1:删除未指定人口的地点
将
const location_data = locations.sort((a, b) => newDict[b.code].pop - newDict[a.code].pop)
替换为
const location_data =
locations
.filter(l => Boolean(newDict[l.code]))
.sort((a, b) => newDict[b.code].pop - newDict[a.code].pop)
以避免错误
test.html:75 Uncaught TypeError: Cannot read property 'pop' of undefined
,方法是删除未指定人口的地点。
解决方案 2:使用默认值
如果您希望即使未指定人口也保留该地点,您可以假设/使用默认值,例如
0
。因此,将
const location_data = locations.sort((a, b) => newDict[b.code].pop - newDict[a.code].pop)
location_data.forEach(function (d) {
d.pop = newDict[d.code].pop
})
替换为
const location_data =
locations
.map(l => ({
...l,
pop: newDict[l.code] ? newDict[l.code].pop : 0,
}))
.sort((a, b) => b.pop - a.pop);
以下是可运行的代码片段。使用“全页”按钮打开它以查看地图和日志:
var margin = { top: 10, left: 10, bottom: 10, right: 10 },
width = window.outerWidth,
width = width - margin.left - margin.right,
mapRatio = .5,
height = width * mapRatio;
var projection = geoAlbersUsaTerritories.geoAlbersUsaTerritories()
.scale(width)
.translate([width / 2, height / 2]);
var path = d3.geoPath()
.projection(projection);
var map = d3.select(".g-chart").append("svg")
.style('height', height + 'px')
.style('width', width + 'px')
.call(d3.zoom().on("zoom", function () {
map.attr("transform", d3.event.transform)
d3.selectAll()
}))
.append("g");
queue()
.defer(d3.json, "https://gist.githubusercontent.com/maiermic/44da7021b7a3c89374cd8fe54e1a68a2/raw/aaa4b1f676d14571875e005a0b59c6a5165e7e17/us.json")
.defer(d3.csv, "https://gist.githubusercontent.com/maiermic/44da7021b7a3c89374cd8fe54e1a68a2/raw/aaa4b1f676d14571875e005a0b59c6a5165e7e17/locations.csv")
.defer(d3.csv, "https://gist.githubusercontent.com/maiermic/44da7021b7a3c89374cd8fe54e1a68a2/raw/aaa4b1f676d14571875e005a0b59c6a5165e7e17/populations.csv")
.await(ready);
function ready(error, us, loc, pop) {
if (error) throw error;
const data = pop.sort((a, b) => +b.population - +a.population)
console.log('populations', data)
const locations = loc.filter(d => d.latitude !== '' && d.longitude !== '')
const maxPop = d3.max(data, d => +d.population)
console.log('maxPop', maxPop)
const newDict = {};
data.forEach(function (d) {
d.population = +d.population;
newDict[d.location_code] = { pop: d.population };
})
map.append("g")
.attr("class", "states")
.selectAll("path")
.data(topojson.feature(us, us.objects.states).features)
.enter().append("path")
.attr("d", path);
const location_data =
locations
.map(l => ({
...l,
pop: newDict[l.code] ? newDict[l.code].pop : 0,
}))
.sort((a, b) => b.pop - a.pop);
console.log('location_data', location_data)
map.append('g')
.attr('class', 'location')
.selectAll("circle")
.data(location_data)
.enter()
.append("circle")
.attr("cx", function (d) {
return projection([d.longitude, d.latitude])[0];
})
.attr("cy", function (d) {
return projection([d.longitude, d.latitude])[1];
})
.attr('r', 10)
.style("fill", "navy")
.style("opacity", 0.5)
}
.states {
fill: #eee;
stroke: #999;
stroke-linejoin: round;
}
<!DOCTYPE html>
<meta charset="utf-8">
<body>
<div class="g-chart"></div>
</body>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://unpkg.com/[email protected]/dist/geo-albers-usa-territories.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/queue-async/1.0.7/queue.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/topojson/1.6.19/topojson.min.js"></script>
目标是将数据重塑为以下形式...
{ location_code, population, latitude, longitude }
...考虑到并非所有地点都有人口数据这一事实。根据与 d3 配合良好的功能为未知人口选择默认值,例如 0。
从更完整的数据集(定义地点)构建索引,设置默认人口,然后将人口数据添加到您拥有的位置。
let locationObjs = [
{ location_code: 'AB', latitude: 39.19969535, longitude: -76.59393033 },
{ location_code: 'BC', latitude: 35.0526543, longitude: -106.6292985 },
{ location_code: 'CD', latitude: 40.19177, longitude: -75.91592 },
{ location_code: 'DE', latitude: 32.56212771, longitude: -99.63538265 },
{ location_code: 'EF', latitude: 43.60675502, longitude: -116.269811 },
{ location_code: 'FG', latitude: 37.103848, longitude: -85.3066339 },
{ location_code: 'GH', latitude: 39.986877, longitude: -104.799156 },
{ location_code: 'HI', latitude: 31.56917545, longitude: -91.23046875 },
{ location_code: 'IJ', latitude: 47.12747, longitude: -118.38267 },
{ location_code: 'JK', latitude: 44.0115189, longitude: -73.16354529 },
{ location_code: 'KL', latitude: 34.55772948, longitude: -117.4418648 },
{ location_code: 'LM', latitude: 32.8571484, longitude: -104.4153316 },
{ location_code: 'MN', latitude: 13.476607, longitude: 144.747679 },
{ location_code: 'NO', latitude: 18.49428, longitude: -67.14213 },
{ location_code: 'OP', latitude: 33.8205041, longitude: -117.9096302 },
{ location_code: 'PQ', latitude: 18.439917, longitude: -66.004817 },
{ location_code: 'QR', latitude: 42.505131, longitude: -96.404939 },
{ location_code: 'RS', latitude: 30.471947, longitude: -84.354361 },
{ location_code: 'ST', latitude: 34.015686, longitude: -90.39034 },
{ location_code: 'TU', latitude: 27.959412, longitude: -82.372276 }
];
let populationObjs = [
{ location_code: 'AB', population: 20 },
{ location_code: 'CD', population: 10 },
{ location_code: 'DE', population: 30 },
{ location_code: 'FG', population: 50 },
{ location_code: 'GH', population: 30 },
{ location_code: 'IJ', population: 20 },
{ location_code: 'JK', population: 15 },
{ location_code: 'KL', population: 40 },
{ location_code: 'LM', population: 30 },
{ location_code: 'OP', population: 20 },
{ location_code: 'RS', population: 10 },
{ location_code: 'TU', population: 5 }
];
// presuming the arrays are large, it will be worthwhile to build an index
// what the OP calls "newDict" might be better called locationIndex...
let locationIndex = locationObjs.reduce((acc, el) => {
acc[el.location_code] = { ...el, population: 0 }; // note we supply a default population
return acc;
}, {});
// change the default popluations to have values where we know them
populationObjs.forEach(popObj => {
locationIndex[popObj.location_code].population = popObj.population
});
// if we no longer need O(1) access to objects by location code, discard the index keeping just its values
let location_data = Object.values(locationIndex);
// these can be sorted. the defaulted 0 values will sort first
location_data.sort((a,b) => a.population - b.population)
console.log(location_data)
// from here:
// map.append('g')
// .attr('class', 'location')
// .selectAll("circle")
// .data(location_data)
有一种更简单、结构更好的方法,可以将两个不同的对象数组与一个公共键组合在一起,而不是像以前那样通过多次操作来达到所需的最终对象数组。
首先要修复现有代码中的错误,这里是修复方法,我添加了一个检查,如果某个州的人口数据不存在,则假定它为 0,就是这样。这是已解决的 Plunker 。
修复:
// if the population data doesn't exist for a state, will assume it as 0
const location_data = locations
.sort((a, b) => (!!newDict[b.code] ? newDict[b.code].pop : 0) - (newDict[a.code] ? newDict[a.code].pop : 0));
location_data.forEach(function (d) {
d.pop = !!newDict[d.code] ? newDict[d.code].pop : 0
});
现在,更简单的解决方案是达到所需的最终对象数组,或者说基于一个公共键将两个不同的对象数组合并为一个。
我们有如下所示的人口数据: (假设我们已经获取了 CSV 文件并保存为 JSON)
// populations
var pop = [
{ location_code: "FG", population: 50 },
{ location_code: "KL", population: 40 },
{ location_code: "DE", population: 30 },
{ location_code: "GH", population: 30 },
{ location_code: "LM", population: 30 },
{ location_code: "AB", population: 20 },
{ location_code: "IJ", population: 20 },
{ location_code: "OP", population: 20 },
{ location_code: "JK", population: 15 },
{ location_code: "CD", population: 10 },
{ location_code: "RS", population: 10 },
{ location_code: "TU", population: 5 }
];
我们有如下所示的位置坐标数据: (假设我们已经获取了 CSV 文件并保存为 JSON)
// location coordinates
var loc = [
{ code: "AB", latitude: "39.19969535", longitude: "-76.59393033" },
{ code: "BC", latitude: "35.0526543", longitude: "-106.6292985" },
{ code: "CD", latitude: "40.19177", longitude: "-75.91592" },
{ code: "DE", latitude: "32.56212771", longitude: "-99.63538265" },
{ code: "EF", latitude: "43.60675502", longitude: "-116.269811" },
{ code: "FG", latitude: "37.103848", longitude: "-85.3066339" },
{ code: "GH", latitude: "39.986877", longitude: "-104.799156" },
{ code: "HI", latitude: "31.56917545", longitude: "-91.23046875" },
{ code: "IJ", latitude: "47.12747", longitude: "-118.38267" },
{ code: "JK", latitude: "44.0115189", longitude: "-73.16354529" },
{ code: "KL", latitude: "34.55772948", longitude: "-117.4418648" },
{ code: "LM", latitude: "32.8571484", longitude: "-104.4153316" },
{ code: "MN", latitude: "13.476607", longitude: "144.747679" },
{ code: "NO", latitude: "18.49428", longitude: "-67.14213" },
{ code: "OP", latitude: "33.8205041", longitude: "-117.9096302" },
{ code: "PQ", latitude: "18.439917", longitude: "-66.004817" },
{ code: "QR", latitude: "42.505131", longitude: "-96.404939" },
{ code: "RS", latitude: "30.471947", longitude: "-84.354361" },
{ code: "ST", latitude: "34.015686", longitude: "-90.39034" },
{ code: "TU", latitude: "27.959412", longitude: "-82.372276" }
];
现在,我们可以使用以下步骤并按照代码:
// concatenate both arrays and reduce this concatenated array into simpler one
let locPopObject = pop.concat(...loc).reduce((finalObj, curr) => {
// curr object is a POPULATION OBJECT
if (curr.hasOwnProperty('location_code')) {
// current state code
let currStateCode = curr['location_code'];
// if state code exist in finalObj,
// then use same or create empty obj for current state code
finalObj[currStateCode] = finalObj[currStateCode] || {};
// add population data of curr object in finalObj against pop key
finalObj[currStateCode]['pop'] = +curr['population'];
return finalObj;
}
// -------------------------------------------------------------------
// curr object is a LOCATION OBJECT
// current state code
let currStateCode = curr['code'];
// if state code exist in finalObj,
// then use same or create empty obj for current state code
finalObj[currStateCode] = finalObj[currStateCode] || {};
// add all key-value pairs of curr object in finalObj
Array.from(Object.keys(curr)).forEach(key => {
finalObj[currStateCode][key] = curr[key];
});
// since Location array is concatenated last,
// therefore till now population array data is processed
// and now its processing location array data
// so if our finalObj (reduced object) doesn't have pop property in it,
// then assign it as zero
finalObj[currStateCode]['pop'] = finalObj[currStateCode]['pop'] || +0;
return finalObj;
}, {});
// get values of locPopObject, which is a array we want to have
// also sort the array before storing its value to a variable
let locPopArray = Array.from(Object.values(locPopObject))
.sort((a, b) => b.pop - a.pop);
就是这样,这是一个优化、更简单、更容易的解决方案,可以以任何我们想要的方式操作数据并获得合并的输出。
这是使用此解决方案的工作地图示例 Plunker 。