开发者问题收集

创建将来自两个具有公共列的文件的数据组合在一起的地图 - 但数据并不一一匹配

2021-06-29
285

我正在使用两个数据文件创建地图。一个数据文件包含位置的经纬度。另一个数据文件包含这些位置的人口。两者都有一列位置代码。 只要两个文件包含相同的代码,就可以。但是,我的人口数据集仅包含 部分 位置代码的信息。所以我收到一个错误:

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)

  }
3个回答

解决方案 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>
maiermic
2021-07-01

目标是将数据重塑为以下形式...

{ 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)
danh
2021-07-01

有一种更简单、结构更好的方法,可以将两个不同的对象数组与一个公共键组合在一起,而不是像以前那样通过多次操作来达到所需的最终对象数组。

首先要修复现有代码中的错误,这里是修复方法,我添加了一个检查,如果某个州的人口数据不存在,则假定它为 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

Neel Shah
2021-07-06