2025-06-25 18:59:39 +08:00

239 lines
10 KiB
JavaScript

document.addEventListener('DOMContentLoaded', function() {
const width = document.getElementById('chart-container').clientWidth;
const height = document.getElementById('chart-container').clientHeight;
const svg = d3.select("#chart-container")
.append("svg")
.attr("width", width)
.attr("height", height);
// 添加箭头标记定义
svg.append("defs").append("marker")
.attr("id", "arrowhead")
.attr("viewBox", "0 -5 10 10")
.attr("refX", 8)
.attr("refY", 0)
.attr("markerWidth", 6)
.attr("markerHeight", 6)
.attr("orient", "auto")
.append("path")
.attr("d", "M0,-5L10,0L0,5")
.attr("fill", "#666");
// 定义节点数据
const nodes = [
// 目标节点
{id: "goal", label: "OVERALL GOAL\nUS critical minerals security", x: width/2, y: 70, width: 300, height: 80, type: "goal"},
// 三大核心策略节点
{id: "strategy1", label: "1", x: 160, y: 180, width: 240, height: 100, type: "primary"},
{id: "strategy1_text", label: "Rebuild US critical minerals\nindustry competitiveness", x: 160, y: 175, width: 200, height: 30, type: "label"},
{id: "kpi1", label: "KPIs: Average time from discovery to construction; no.\nof workers in CM industry; no. of projects in pipeline", x: 160, y: 220, width: 240, height: 40, type: "kpi"},
{id: "strategy2", label: "2", x: 480, y: 180, width: 240, height: 100, type: "primary"},
{id: "strategy2_text", label: "Reduce import reliance from\nnon-allied countries", x: 480, y: 175, width: 200, height: 30, type: "label"},
{id: "kpi2", label: "KPIs: Percentage of US critical minerals imported from\nor processed by non-allied countries", x: 480, y: 220, width: 240, height: 40, type: "kpi"},
{id: "strategy3", label: "3", x: 800, y: 180, width: 240, height: 100, type: "primary"},
{id: "strategy3_text", label: "Reduce the risk of critical minerals\nmarket disruptions", x: 800, y: 175, width: 200, height: 30, type: "label"},
{id: "kpi3", label: "KPIs: No. of months of critical mineral reserves;\npercentage of value chain data collected", x: 800, y: 220, width: 240, height: 40, type: "kpi"},
// 二级节点
{id: "1e", label: "(1e) Develop\ncritical minerals\nworkforce", x: 60, y: 320, width: 100, height: 80, type: "grey"},
{id: "1d", label: "(1d) Increase\ninvestment in\nR&D for new\ntech and AI\nusage", x: 60, y: 440, width: 100, height: 80, type: "grey"},
{id: "1c", label: "(1c) Improve\npermitting and\nregulations", x: 60, y: 560, width: 100, height: 80, type: "grey"},
{id: "1b", label: "(1b)/(2d)\nSupport\nsecondary\nprocessing\nexpansion", x: 260, y: 440, width: 100, height: 80, type: "grey"},
{id: "domestic_prod", label: "Increase\ndomestic\nproduction", x: 260, y: 320, width: 100, height: 80, type: "secondary"},
{id: "allied_prod", label: "Increase allied\nproduction", x: 480, y: 320, width: 100, height: 80, type: "secondary"},
{id: "2a", label: "(2a) Expand the\nrole and use of\nexisting\nmultilateral\narrangements", x: 380, y: 440, width: 100, height: 80, type: "grey"},
{id: "2b", label: "(2b) Expand US\ngovernment\nfinancing tools", x: 580, y: 440, width: 100, height: 80, type: "grey"},
{id: "1a", label: "(1a) Increase\ndomestic\nfinancing and\nincentives", x: 260, y: 560, width: 100, height: 80, type: "grey"},
{id: "allied_pipeline", label: "Expand allied\ncritical minerals\nproject pipeline", x: 480, y: 560, width: 100, height: 80, type: "secondary"},
{id: "2c", label: "(2c) Support\nexploration and\ndata initiatives", x: 480, y: 680, width: 100, height: 80, type: "grey"},
{id: "3a", label: "(3a) Develop a\nreal-time value\nchain mapping\nand monitoring\nsystem", x: 690, y: 440, width: 100, height: 80, type: "grey"},
{id: "3b", label: "(3b) Expand\npurpose and\nuse of National\nDefense\nStockpile", x: 790, y: 440, width: 100, height: 80, type: "grey"},
{id: "3c", label: "(3c) Identify and\ndeploy targeted\nmarket\nincentives", x: 890, y: 440, width: 100, height: 80, type: "grey"},
{id: "value_chain", label: "Monitor critical\nminerals value\nchain\nvulnerabilities", x: 690, y: 320, width: 100, height: 80, type: "secondary"},
{id: "supply_risk", label: "Reduce supply\ndisruption risks", x: 790, y: 320, width: 100, height: 80, type: "secondary"},
{id: "price_risk", label: "Reduce price\ndisruption risks", x: 890, y: 320, width: 100, height: 80, type: "secondary"},
{id: "1d_pipeline", label: "Expand US\ncritical minerals\nproject pipeline", x: 160, y: 680, width: 100, height: 80, type: "secondary"}
];
// 定义连接数据
const links = [
{source: "goal", target: "strategy1"},
{source: "goal", target: "strategy2"},
{source: "goal", target: "strategy3"},
{source: "1e", target: "domestic_prod"},
{source: "domestic_prod", target: "strategy1"},
{source: "1d", target: "1e"},
{source: "1c", target: "1b"},
{source: "1b", target: "domestic_prod"},
{source: "domestic_prod", target: "allied_prod"},
{source: "allied_prod", target: "strategy2"},
{source: "2a", target: "allied_prod"},
{source: "2a", target: "2b"},
{source: "2b", target: "2a"},
{source: "1a", target: "1b"},
{source: "1a", target: "allied_pipeline"},
{source: "allied_pipeline", target: "allied_prod"},
{source: "2c", target: "allied_pipeline"},
{source: "1d", target: "1d_pipeline"},
{source: "1d_pipeline", target: "1c"},
{source: "3a", target: "value_chain"},
{source: "value_chain", target: "strategy3"},
{source: "3b", target: "supply_risk"},
{source: "supply_risk", target: "strategy3"},
{source: "3c", target: "price_risk"},
{source: "price_risk", target: "strategy3"}
];
// 绘制连接线
svg.selectAll("path.link")
.data(links)
.enter()
.append("path")
.attr("class", "link")
.attr("d", function(d) {
const source = nodes.find(n => n.id === d.source);
const target = nodes.find(n => n.id === d.target);
// 确定连接点位置
let sourceX, sourceY, targetX, targetY;
if (source.y < target.y) {
sourceX = source.x;
sourceY = source.y + source.height/2;
targetX = target.x;
targetY = target.y - target.height/2;
} else if (source.y > target.y) {
sourceX = source.x;
sourceY = source.y - source.height/2;
targetX = target.x;
targetY = target.y + target.height/2;
} else if (source.x < target.x) {
sourceX = source.x + source.width/2;
sourceY = source.y;
targetX = target.x - target.width/2;
targetY = target.y;
} else {
sourceX = source.x - source.width/2;
sourceY = source.y;
targetX = target.x + target.width/2;
targetY = target.y;
}
// 创建路径
return `M${sourceX},${sourceY}L${targetX},${targetY}`;
})
.attr("marker-end", "url(#arrowhead)");
// 绘制节点
const nodeGroups = svg.selectAll("g.node")
.data(nodes.filter(n => !n.id.includes("_text") && !n.id.includes("kpi")))
.enter()
.append("g")
.attr("class", function(d) { return "node " + d.type; });
// 添加矩形
nodeGroups.append("rect")
.attr("x", d => d.x - d.width/2)
.attr("y", d => d.y - d.height/2)
.attr("width", d => d.width)
.attr("height", d => d.height)
.attr("rx", 4)
.attr("ry", 4);
// 添加标签文本
nodeGroups.append("text")
.attr("x", d => d.x)
.attr("y", d => d.y)
.attr("dy", ".35em")
.attr("text-anchor", "middle")
.text(d => d.label)
.each(function(d) {
// 处理多行文本
const text = d3.select(this);
const words = d.label.split('\n');
text.text(null);
words.forEach((word, i) => {
text.append("tspan")
.attr("x", d.x)
.attr("dy", i ? "1.2em" : "0")
.text(word);
});
});
// 添加策略标题文本
svg.selectAll("text.strategy-text")
.data(nodes.filter(n => n.id.includes("_text")))
.enter()
.append("text")
.attr("class", "strategy-text")
.attr("x", d => d.x)
.attr("y", d => d.y)
.attr("text-anchor", "middle")
.attr("fill", "white")
.each(function(d) {
// 处理多行文本
const text = d3.select(this);
const words = d.label.split('\n');
text.text(null);
words.forEach((word, i) => {
text.append("tspan")
.attr("x", d.x)
.attr("dy", i ? "1.2em" : "0")
.text(word);
});
});
// 添加KPI文本
svg.selectAll("text.kpi")
.data(nodes.filter(n => n.id.includes("kpi")))
.enter()
.append("text")
.attr("class", "kpi")
.attr("x", d => d.x)
.attr("y", d => d.y)
.attr("text-anchor", "middle")
.attr("fill", "white")
.attr("font-size", "10px")
.each(function(d) {
// 处理多行文本
const text = d3.select(this);
const words = d.label.split('\n');
text.text(null);
words.forEach((word, i) => {
text.append("tspan")
.attr("x", d.x)
.attr("dy", i ? "1.2em" : "0")
.text(word);
});
});
});