如何使用来自另一个 json 文件的数据将 attr fill 与 scalesqrt 一起使用?

How to use attr fill with scalesqrt using data from another json file?

我是 d3 的新手,我正在尝试根据总能耗值为美国每个州着色。当我使用 :

d3.selectAll("path")
  .attr("fill", "blue");

我能成功把颜色改成蓝色,所以我觉得我的底子是对的。但是当我尝试应用我自己定义的颜色渐变函数时,它不再起作用了。我在网上看到的大多数示例都使用相同的 json 文件来创建地图和分配颜色值。但是我有两个正在使用的 json 文件——一个包含美国州路径形状,另一个包含每个州的元数据。这是我的:

const data = await d3.json("states.json");
const energy_data = await d3.csv("Energy Census and Economic Data US 2010-2014.csv");

console.log(energy_data);

const states = topojson.feature(data, data.objects.usStates).features

// draw the states
svg.selectAll(".state")
    .data(states)
    .enter().append("path")
    .attr("class", "state")
    .attr("d", path);

const min = d3.min(energy_data, d => parseInt(d.TotalC2010));
const max = d3.max(energy_data, d => parseInt(d.TotalC2010));

const colorgradient = d3.scaleSqrt()
    .domain([min, max])
    .range(["green", "blue"]);

d3.selectAll("path")
    .data(energy_data)
    .enter()
    .attr("fill", d => colorgradient(d.TotalC2010));

有什么建议吗?

提前致谢!

编辑:感谢Andrew Reid

,我终于让它工作了
const state_data = await d3.json("states.json");
const energy_data_raw = await d3.csv("Energy Census and Economic Data US 2010-2014.csv");

const energy_data = new Map(energy_data_raw.map(d => [d.StateCodes, d]))

const states = topojson.feature(state_data, state_data.objects.usStates).features

const min = d3.min(energy_data_raw, d => parseInt(d.TotalC2010));
const max = d3.max(energy_data_raw, d => parseInt(d.TotalC2010));

let colorgradient = d3.scaleSqrt()
    .domain([min, max])
    .range(["green", "blue"]);

svg.selectAll(".state")
    .data(states)
    .enter()
    .append("path")
    .attr("class", "state")
    .attr("d", path)
    .attr("fill", d => colorgradient(energy_data.get(d.properties.STATE_ABBR).TotalC2010))

问题

此处代码:

d3.selectAll("path")
   .data(energy_data)
   .enter()
   .attr("fill", d => colorgradient(d.TotalC2010));

根本不应该做任何事情。

首先,假设您的地理数据和能源数据具有相同数量的项目,您正在选择现有路径 (d3.selectAll("path"))。

然后您将按照索引的顺序为这些现有路径分配新数据 (.data(energy_data)) 以匹配每个现有路径。

接下来您将创建一个可能为空的输入选择。输入选择为数据数组中没有对应元素(此处通过索引匹配)的每个项目创建一个元素:如果数据数组中的项目多于元素,您将输入新元素。否则,您的选择将是空的,因为您不需要输入任何新元素来表示 energy_data.

中的数据

最后你设置了一个空的输入选择(通常你会使用 .append() 来指定你想要追加的元素类型,否则,输入选择只是一个占位符。因为这是一个空的无论如何选择,什么都没做。

可能的解决方案

我正在研究这个解决方案,因为它看起来像你正在尝试做的,尽管这不是我推荐的。

您似乎在尝试将新数据分配给现有选择 - 这是绝对可能的。在这种方法中,您将使用地理数据绘制要素,然后为每个要素分配一个新数据集并根据此新数据修改要素。

数据数组顺序相同

如果我们的数据在 geojson 和 csv 中的顺序相同,那么我们可以简单地使用:

selection.data(states)
  .enter()
  .append("path")
  .attr("d", path)
  .data(energy_data)
  .attr("fill", ...)

因为 .data() 默认情况下通过匹配它们的索引将数据数组中的项目绑定到选择中的元素,所以只有当两个数据数组被排序时,每个特征才会用 energy_data 中的正确数据更新相同。这是一个明显的限制,但可以克服。

数据数组排序不同

如果数组的顺序不同,我们需要有一种方法将现有特征与新数据集相匹配。默认情况下, .data() 方法通过索引将数据分配给现有元素。但是我们可以使用 .data() 的第二个参数来使用键函数分配唯一标识符。

对于这种情况,我假设 statesenergy_data 的标识符都位于 d.properties.id

当我们输入我们的路径时,我们不需要 key 函数,没有数据可以连接到现有元素。

当我们用 energy_data 数据更新我们的路径时,我们想使用一个关键函数来确保我们用正确的新数据更新每个元素。 key 函数首先对每个现有元素的数据进行评估,然后对新数据数组中的每个项目进行评估。在找到匹配项的地方,匹配的新数据将替换 old.eg:

 svg.selectAll("path") 
    .data(energy_data, function(d) { return d.properties.id; })
    .attr("fill",...

这是一个包含人为数据的简单示例:

let data = [
  { value: 4, properties: {id: "A" }},
  { value: 6, properties: {id: "B" }},
  { value: 2, properties: {id: "C" }}

]

let color = d3.scaleLinear()
  .domain([1,6]).range(["red","yellow"]);

let geojson = {
  "type":"FeatureCollection",
  "features": [
       { 
         "type": "Feature",
         "properties": { id: "C"},
         "geometry": {
            "type": "Polygon",
            "coordinates": [
              [
                [ 0, 0 ],
                [ 100, 0 ],
                [ 100,100 ],
                [ 0, 100 ],
                [ 0, 0 ]
              ]
            ]
          }
       },
       { 
         "type": "Feature",
         "properties": { id: "B"},
         "geometry": {
            "type": "Polygon",
            "coordinates": [
              [
                [ 100, 0 ],
                [ 200, 0 ],
                [ 200, 100 ],
                [ 100, 100 ],
                [ 100, 0 ]
              ]
            ]
          }
        },
        { 
         "type": "Feature",
         "properties": { id: "A"},
         "geometry": {
            "type": "Polygon",
            "coordinates": [
              [
                [ 200, 0 ],
                [ 300, 0 ],
                [ 300,100 ],
                [ 200, 100 ],
                [ 200, 0 ]
              ]
            ]
          } 
        }
      ]
    }
    
let svg = d3.select("body")
  .append("svg")
  .attr("width", 300);
  
svg.selectAll("path")
  .data(geojson.features)
  .enter()
  .append("path")
  .attr("d", d3.geoPath(null));
 
svg.selectAll("path")   // Note: You can just chain .data() to .attr() omitting this line.
  .data(data, d=>d.properties.id)
  .attr("fill", d=>color(d.value));
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.1.0/d3.min.js"></script>

不同键访问器的数据数组排序不同

但是,如果标识符的位置或名称与以前不同,我们需要更改关键功能。

对于这种情况,我假设 states 标识符位于 d.properties.id。对于 energy_data,我假设标识符位于 d.id,一个更常见的解析 DSV 位置。

如前所述,键函数先针对现有元素的数据进行评估,然后针对新数据进行评估。这意味着我们需要一个适用于两个数据集的关键函数,这意味着我们需要一个稍微复杂一点的关键函数来比较两个数据集中的项目,例如:

 .data(energy_data, function(d) {
     if(d.properties) 
        return d.properties.id; // get the key from items in `states` 
     else  
        return d.id;            // get the key from items in `energy_data`
 })
 .attr("fill",...

关键功能现在可以用新数据替换旧数据,确保正确的特征具有正确的数据。

假设您的所有标识符都正确匹配(并且是字符串),您将为现有特征分配新数据。

这种方法的缺点是你丢失了原始数据——如果你想做语义缩放,检查地理数据的不同属性,或者重新访问geojson中的数据,你需要重新绑定原始数据.选择路径也需要时间,它假设没有其他可能被错误选择的路径。

这是一个简单的例子:

let csv = d3.csvParse(d3.select("pre").remove().text());

let color = d3.scaleLinear()
  .domain([1,6]).range(["red","yellow"]);

let geojson = {
  "type":"FeatureCollection",
  "features": [
       { 
         "type": "Feature",
         "properties": { id: "C"},
         "geometry": {
            "type": "Polygon",
            "coordinates": [
              [
                [ 0, 0 ],
                [ 100, 0 ],
                [ 100,100 ],
                [ 0, 100 ],
                [ 0, 0 ]
              ]
            ]
          }
       },
       { 
         "type": "Feature",
         "properties": { id: "B"},
         "geometry": {
            "type": "Polygon",
            "coordinates": [
              [
                [ 100, 0 ],
                [ 200, 0 ],
                [ 200, 100 ],
                [ 100, 100 ],
                [ 100, 0 ]
              ]
            ]
          }
        },
        { 
         "type": "Feature",
         "properties": { id: "A"},
         "geometry": {
            "type": "Polygon",
            "coordinates": [
              [
                [ 200, 0 ],
                [ 300, 0 ],
                [ 300,100 ],
                [ 200, 100 ],
                [ 200, 0 ]
              ]
            ]
          } 
        }
      ]
    }
    
let svg = d3.select("body")
  .append("svg")
  .attr("width", 300);
  
svg.selectAll("path")
  .data(geojson.features)
  .enter()
  .append("path")
  .attr("d", d3.geoPath(null));
  
svg.selectAll("path") // Note: You can just chain .data() to the .attr() omitting this line.
  .data(csv, d=>d.properties?d.properties.id:d.id)
  .attr("fill", d=>color(d.value));
  
  
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.1.0/d3.min.js"></script>
<pre>id,value
A,4
B,6
C,2</pre>

推荐方法

要连接空间数据和非空间数据,我建议使用 javascript 地图。这允许您使用共享标识符在非空间数据中查找值:

let map = new Map(energy_data.map(function(d) { return [d.id, d] }))

我们现在可以使用 map.get("someIdentifier")

查找 energy_data 中的任何项目

我们可以这样使用:

.attr("fill", d=> colorgradient(map.get(d.properties.id).TotalC2010))

这样我们的空间特征保留了它们的空间数据,但我们可以使用公共标识符和 javascript 地图轻松访问非空间数据。

这是一个使用与上面相同的人为设计的 geojson 和 DSV 数据的简单示例:

let csv = d3.csvParse(d3.select("pre").remove().text());
let map = new Map(csv.map(function(d) { return [d.id, d] }))

let color = d3.scaleLinear()
  .domain([1,6]).range(["red","yellow"]);

let geojson = {
  "type":"FeatureCollection",
  "features": [
       { 
         "type": "Feature",
         "properties": { id: "C"},
         "geometry": {
            "type": "Polygon",
            "coordinates": [
              [
                [ 0, 0 ],
                [ 100, 0 ],
                [ 100,100 ],
                [ 0, 100 ],
                [ 0, 0 ]
              ]
            ]
          }
       },
       { 
         "type": "Feature",
         "properties": { id: "B"},
         "geometry": {
            "type": "Polygon",
            "coordinates": [
              [
                [ 100, 0 ],
                [ 200, 0 ],
                [ 200, 100 ],
                [ 100, 100 ],
                [ 100, 0 ]
              ]
            ]
          }
        },
        { 
         "type": "Feature",
         "properties": { id: "A"},
         "geometry": {
            "type": "Polygon",
            "coordinates": [
              [
                [ 200, 0 ],
                [ 300, 0 ],
                [ 300,100 ],
                [ 200, 100 ],
                [ 200, 0 ]
              ]
            ]
          } 
        }
      ]
    }
    
let svg = d3.select("body")
  .append("svg")
  .attr("width", 300);
  
svg.selectAll("path")
  .data(geojson.features)
  .enter()
  .append("path")
  .attr("d", d3.geoPath(null))
  .attr("fill", d=> color(map.get(d.properties.id).value));
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.1.0/d3.min.js"></script>
<pre>id,value
A,4
B,6
C,2</pre>

其他方法

第三种选择是组合数据数组 - 遍历 geojson 并手动将 energy_data 中包含的值添加到每个特征,这样您只有一个数据数组包含您需要绘制和设置样式的所有内容可视化。