【问题标题】:D3 - Loading data from a second CSVD3 - 从第二个 CSV 加载数据
【发布时间】:2026-02-14 22:00:01
【问题描述】:

我有两个单独的 CSV 文件,共享一个属性:

文件 1

name,total
Frank,13
Mary,24
Jim,46

文件 2

name,desc
Frank,yellow
Mary,blue
Jim,green

我如何映射属性 desc 到文件 1,比如说,当我将鼠标悬停在 Frank 上时,我会看到“13”和“黄色”?

目前,我的代码如下所示:

d3.csv("data/csv/bilancio-missioni-desc.csv", function(descrizione) {
  data = descrizione.map(function(data){
            div.html("<h3>" + d.name + "</h3>" + "<p>" + data.desc + "</p>")
})

问题在于 d.namedata.desc(来自文件 1 和文件 2)不匹配 - 我可以理解这是因为我没有将它们组合起来,因此它们可以共享通用属性name,但是不知道怎么写正确的代码。

更新 到目前为止我的代码:

d3.csv("data/csv/bilancio-tipologiedispesa-nest.csv", function(data1) {
d3.csv("data/csv/bilancio-missioni-desc.csv", function(data2) {
    //code that depends on both data1 and data2       
    data1.forEach(d => {
        data2.forEach(e => {
        if (d.name === e.name) {
        d.desc = e.desc;
    }
});
}); 
// Fade all the segments.
d3.selectAll("path")
.style("opacity", .3);
vis.selectAll("path")
.filter(function(node) {
    return (sequenceArray.indexOf(node) >= 0);
})
.style("opacity", 1);
div.transition()
.duration(200)
.style("opacity", .9);
div.html("<h3>" + d.name + "</h3>" + "<p>" + d.desc + "</p>");
});
});
}

如果我 console.log(data1),“desc”(来自 data2)被附加到 data1(耶!)。但是“d.desc”在 HTML 中返回“unidefined”。

【问题讨论】:

  • 你能提供一些你拥有的代码吗?
  • 你好@snolflake,我编辑了我的问题。谢谢。
  • @ValentinaRachiele 关于您的更新:d3.csv 是异步的。因此,任何依赖于data1 的代码都必须inside 内部d3.csv。也就是说,将所有代码移至forEach 下方。

标签: javascript d3.js


【解决方案1】:

正如another answer 中提到的,您可以嵌套 csv 加载函数来异步加载文件。然后您可以将它们组合在一起:

var file1 = "data/csv/bilancio-missioni-desc.csv";
var file2 = "data/csv/bilancio-missioni-info.csv"; // I named it info just as an example

var div = d3.select('#my-div');
var data = {};

d3.csv(file1, function(descrizione) {
    d3.csv(file2, function(info) {
        processData(descrizione, info);
    })
})

function processData(descrizione, info) {
    descrizione.forEach(d => data[d.name] = d);
    info.forEach(i => data[i.name] && data[i.name]['desc'] = i.desc);
    appendData();
}

function appendData() {
    div.data(data).append(d => '<h3>' + d.name + ' (' + d.total ')</h3> <p>' + d.desc + '</p>');
}

当然,如果不进行一些调整,此代码可能无法正常工作,但我希望您能理解。如果没有,请让我知道您是否有不清楚的地方。祝你好运!

【讨论】:

  • 你真好!明天我会尝试使用你的代码——交叉手指!如果我能让它工作,我会告诉你的。谢谢
【解决方案2】:

加载多个 CSV 有两种主要方法:使用 d3.queue() 或嵌套它们:

d3.csv("file1.csv", function(data1) {
    d3.csv("file2.csv", function(data2) {
        //code that depends on both data1 and data2
    })
});

其中data1 是“file1.csv”中的数组,data2 是“file2.csv”中的数组。

然后,我们将根据它们的属性合并两个数组。

您可以创建第三个数组或将值推送到两个原始数组之一中。在这里,我将执行第二个选项(实际上,第二个选项就是您所要求的:“我如何将属性 'desc' 映射到文件 1”)。

请记住,这是我针对您的问题专门制作的临时解决方案(它不适用于其他数据结构):

data1.forEach(d => {
    data2.forEach(e => {
        if (d.name === e.name) {
            d.desc = e.desc
        }
    })
})

现在,data1 拥有您想要的所有信息:姓名、总数和描述。

这是一个演示,用数据检查控制台(在这个演示中,我使用&lt;pre&gt; 元素来加载数据,因为我不能在 sn-p 中使用 CSV):

var data1 = d3.csvParse(d3.select("#file1").text());
var data2 = d3.csvParse(d3.select("#file2").text());

data1.forEach(d => {
    data2.forEach(e => {
        if (d.name === e.name) {
            d.desc = e.desc
        }
    })
})

console.log(data1);
pre {
    display: none;
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<pre id="file1">name,total
Frank,13
Jim,46
Mary,24</pre>
<pre id="file2">name,desc
Frank,yellow
Mary,blue
Jim,green</pre>

PS:如果文件太大,请查看下方 cmets 中的 @altocumulus 替代代码。

【讨论】:

  • 很好的解决方案,将那些&lt;pre&gt; 元素用于模拟外部文件内容。
  • 对于内部循环,我更喜欢data2.some(e =&gt; { return !!(d.name === e.name ? d.desc = e.desc : null) }) 这样的东西,一旦找到匹配项,您就可以从该循环中中断。另一方面,.forEach() 将始终循环遍历整个数组,这对于嵌套循环可能不是一个好主意。如果有大量数据,这可以显着提高性能。
  • 是的,你是对的。但是现代浏览器是如此之快,除非 OP 的数组长度非常大,否则这些微优化可能会产生微不足道的影响。但是,由于数组的长度未知,因此您的代码确实更好。
  • 谢谢!我更新了我的问题,你的回答对我帮助很大,但我又卡住了......
  • @ValentinaRachiele 您的问题现在很容易解决:任何 依赖于data1 的代码必须inside 第二个(内部) d3.csv 函数。
【解决方案3】:

正如 Gerardo Furtado 在他的 answer 中所述,您将不得不使用 d3.queued3.csv 的嵌套版本来保持同步加载数据。在使用 D3 的两个内置功能时,可以改进嵌套方法,使您无需在初始加载后进行显式嵌套循环。

  • 使用 D3 Map 通过name 属性提供对数据的快速访问。

  • 使用row conversion function,它可以作为第二个可选参数传递给d3.csv(url[[, row], callback])

    如果指定了 row 转换函数,则为每一行调用指定的函数,并传递一个表示当前行的对象 (d)...

    这特别方便,因为在解析文件内容时,行将在内部被迭代。您可以使用行转换函数作为挂钩,通过在该函数中操作映射,将第二个文件的数据同步到第一个文件的数据。

我设置了一个Block 来演示这种方法:

d3.csv("file1.csv", function(data1) {
  // 1. Build a D3 map from your first file's contents 
  var map = d3.map(data1, function(d) { return d.name; });

  d3.csv("file2.csv", function(row) {
    // 2. Use the row function of the second file to match your data.
    map.get(row.name).desc = row.desc;  // 3. Sync data
    return null;   // 4. Don't append any row to the array.
  }, function() {  // 5. There is no parameter present; all data has been synced into data1.
    // Just logging below
    console.log(data1);  // [{ name: "Frank", total: "13", desc: "yellow"}, ...]
  })
});

让我一步一步地告诉你:

  1. 我们根据您的第一个文件中的解析数据构建地图,使用 name 属性作为键。

  2. 在执行第二个文件的内部加载时,我们将行转换指定为第二个参数。通常,这将用于进行一些数据转换等,并返回一些表示实际行数据的转换对象。然而,在我们的场景中,我们对转换本身不感兴趣,而是对隐式迭代和对行数据的访问感兴趣。

  3. 我们现在可以通过getting 与地图中该行的name 对应的对象同步数据。

  4. 从行转换函数返回null 将避免为第二个文件构建数组,因为我们对内容的独立版本不感兴趣:

    如果返回值为 null 或未定义,则跳过该行并将从数组中省略...

  5. 在内部d3.csv() 的回调中,您的数据已准备好并已同步。请注意,我们没有将任何参数传递给此回调,因为我们将通过data1 访问同步数据。我们甚至不必再为地图烦恼了,这只是一种允许从第一个文件快速访问我们的数据的方法。因为地图存储了对我们数据对象的引用,所以我们更新了来自data1 的实际对象,这些对象用于构建地图。


作为改进,您可能需要添加一些 ES6 糖,使其更加简洁 (ES6 Block):

d3.csv("file1.csv", data1 => {
  let map = d3.map(data1, d => d.name);
  d3.csv("file2.csv",
         row => (map.get(row.name).desc = row.desc, null),
         () => {console.log(data1)}  // Merged data available at this point.
  );
}); 

【讨论】:

  • 使用行转换功能的好主意。
  • 这最终成为 XY 问题的另一个经典示例。 OP 真正想要的不能用这个来完成(请参阅我的答案中的 cmets 和她的 plunker)。