D3.js嵌套数据可视化问题:如何基于map_data绘制地图圆点
Got it, let's break down what's going wrong here and fix it step by step.
The Core Problem
Right now, you're binding your top-level groups (like ISIL, Taliban) directly to <circle> elements. Each group has an array of map_data entries—but D3 expects each data item you bind to correspond to one DOM element. When you use map() inside the attr() functions, you're returning an array of values, but cx/cy/r only accept single numbers. That's why your circles aren't rendering correctly.
Solution 1: Flatten Your Data (Simplest Approach)
The easiest fix is to flatten your nested data into a single array where each entry is one map_data item (plus the group key if you need it for styling/links). This way, each circle gets exactly one data point.
First, flatten the data:
// Combine all map_data entries from every group into one array const flattenedAttacks = topgroups.flatMap(group => group.values.map_data.map(attack => ({ ...attack, // Copy all attack properties groupName: group.key // Keep track of which group the attack belongs to })) );
Then render the circles with this flattened array:
g.selectAll("circle") .data(flattenedAttacks) .enter() .append("a") .attr("xlink:href", d => `https://www.google.com/search?q=${d.groupName}`) .append("circle") .attr("cx", d => { // Skip entries with empty coordinates to avoid broken projections if (!d.longitude || !d.latitude) return null; // Convert string coordinates to numbers and project return projection([+d.longitude, +d.latitude])[0]; }) .attr("cy", d => { if (!d.longitude || !d.latitude) return null; return projection([+d.longitude, +d.latitude])[1]; }) .attr("r", d => { // Handle empty/zero kill counts with a minimum radius const killCount = +d.nkill || 0; return killCount > 0 ? killCount / 500 : 1; }) .style("fill", d => { // Optional: Color-code circles by group switch(d.groupName) { case "Islamic State of Iraq and the Levant (ISIL)": return "crimson"; case "Taliban": return "steelblue"; default: return "gray"; } }) .style("opacity", 0.7); // Add opacity to handle overlapping circles
Solution 2: Nested Selections (If You Need Grouped DOM Elements)
If you want to keep your groups organized in the DOM (e.g., each group's circles in a separate <g>), use nested selections:
// First, bind groups to <g> elements const groupGroups = g.selectAll(".attack-group") .data(topgroups) .enter() .append("g") .attr("class", "attack-group"); // Then bind each group's map_data to circles inside the group <g> groupGroups.selectAll("circle") .data(d => d.values.map_data) .enter() .append("a") .attr("xlink:href", (d, i, nodes) => { // Get the parent group's key from the parent <g> element const groupKey = d3.select(nodes[i].parentNode).datum().key; return `https://www.google.com/search?q=${groupKey}`; }) .append("circle") .attr("cx", d => { if (!d.longitude || !d.latitude) return null; return projection([+d.longitude, +d.latitude])[0]; }) .attr("cy", d => { if (!d.longitude || !d.latitude) return null; return projection([+d.longitude, +d.latitude])[1]; }) .attr("r", d => { const killCount = +d.nkill || 0; return killCount > 0 ? killCount / 500 : 1; }) .style("fill", (d, i, nodes) => { const groupKey = d3.select(nodes[i].parentNode).datum().key; return groupKey === "Islamic State of Iraq and the Levant (ISIL)" ? "crimson" : "steelblue"; }) .style("opacity", 0.7);
Key Notes to Avoid Headaches
- Convert Strings to Numbers: Your CSV data has coordinates and kill counts as strings—use
+d.longitudeto convert them to numbers, otherwise D3 will try to project strings (which breaks everything). - Handle Empty Data: Some entries (like the Taliban's "Unknown" city) have empty coordinates. Skipping these prevents invisible circles from appearing at the top-left corner of your map.
- Minimum Radius: For attacks with 0 kills, set a small minimum radius so they're still visible.
内容的提问来源于stack exchange,提问作者R.hagens

