272 lines
12 KiB
HTML
272 lines
12 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>US Critical Minerals Security Flowchart</title>
|
|
<style>
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
background-color: #f0f2f5;
|
|
margin: 0;
|
|
padding: 2rem;
|
|
}
|
|
|
|
/* --- Node Styles --- */
|
|
.node-group .node-rect {
|
|
stroke-width: 1.5;
|
|
stroke: #333;
|
|
}
|
|
|
|
.node-goal .node-rect {
|
|
fill: #8BC34A; /* A green similar to the image */
|
|
stroke: none;
|
|
}
|
|
|
|
.node-pillar .node-rect {
|
|
fill: #212121;
|
|
stroke: none;
|
|
}
|
|
|
|
.node-action .node-rect {
|
|
fill: #E0E0E0; /* A light grey */
|
|
stroke: #9E9E9E;
|
|
}
|
|
|
|
.node-intermediate .node-rect {
|
|
fill: #FFFFFF;
|
|
stroke: #424242;
|
|
}
|
|
|
|
/* --- Text Styles --- */
|
|
.node-text {
|
|
font-size: 11px;
|
|
text-anchor: middle;
|
|
fill: #000;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.node-pillar .node-text, .node-goal .node-text {
|
|
fill: #fff;
|
|
}
|
|
|
|
.node-text .kpi-text {
|
|
font-size: 9px;
|
|
font-style: italic;
|
|
}
|
|
|
|
/* --- Link Styles --- */
|
|
.link {
|
|
fill: none;
|
|
stroke: #616161;
|
|
stroke-width: 1.5px;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<div id="chart"></div>
|
|
|
|
<script src="https://d3js.org/d3.v7.min.js"></script>
|
|
<script>
|
|
// 1. Data Definition
|
|
const nodes = [
|
|
// Top Goal
|
|
{ id: 'goal', type: 'goal', width: 280, height: 50, text: ['OVERALL GOAL', 'US critical minerals security'] },
|
|
|
|
// 3 Pillars
|
|
{ id: 'p1', type: 'pillar', width: 280, height: 75, text: ['1', 'Rebuild US critical minerals', 'industry competitiveness'], kpis: ['KPIs: Average time from discovery to construction; no.', 'of workers in CM industry; no. of projects in pipeline'] },
|
|
{ id: 'p2', type: 'pillar', width: 280, height: 75, text: ['2', 'Reduce import reliance from', 'non-allied countries'], kpis: ['KPIs: Percentage of US critical minerals imported from', 'or processed by non-allied countries'] },
|
|
{ id: 'p3', type: 'pillar', width: 280, height: 75, text: ['3', 'Reduce the risk of critical minerals', 'market disruptions'], kpis: ['KPIs: No. of months of critical mineral reserves;', 'percentage of value chain data collected'] },
|
|
|
|
// Intermediate Nodes
|
|
{ id: 'int1', type: 'intermediate', width: 130, height: 40, text: ['Increase domestic', 'production'] },
|
|
{ id: 'int2', type: 'intermediate', width: 130, height: 40, text: ['Increase allied', 'production'] },
|
|
{ id: 'int3', type: 'intermediate', width: 130, height: 40, text: ['Expand US', 'critical minerals', 'project pipeline'] },
|
|
{ id: 'int4', type: 'intermediate', width: 130, height: 40, text: ['Expand allied', 'critical minerals', 'project pipeline'] },
|
|
{ id: 'int5', type: 'intermediate', width: 130, height: 50, text: ['Monitor critical', 'minerals value', 'chain', 'vulnerabilities'] },
|
|
{ id: 'int6', type: 'intermediate', width: 130, height: 40, text: ['Reduce supply', 'disruption risks'] },
|
|
{ id: 'int7', type: 'intermediate', width: 130, height: 40, text: ['Reduce price', 'disruption risks'] },
|
|
|
|
// Action Nodes (Grey)
|
|
{ id: 'a1e', type: 'action', width: 130, height: 45, text: ['(1e) Develop', 'critical minerals', 'workforce'] },
|
|
{ id: 'a1f', type: 'action', width: 130, height: 55, text: ['(1f) Increase', 'investment in', 'R&D for new', 'tech and AI', 'usage'] },
|
|
{ id: 'a1c', type: 'action', width: 130, height: 45, text: ['(1c) Improve', 'permitting and', 'regulations'] },
|
|
{ id: 'a1d', type: 'action', width: 130, height: 55, text: ['(1d) Support', 'domestic', 'exploration with', 'funding, data,', 'and AI tools'] },
|
|
{ id: 'a1a', type: 'action', width: 130, height: 45, text: ['(1a) Increase', 'domestic', 'financing and', 'incentives'] },
|
|
{ id: 'a1b/2d', type: 'action', width: 130, height: 55, text: ['(1b)/(2d)', 'Support', 'secondary', 'processing', 'expansion'] },
|
|
{ id: 'a2a', type: 'action', width: 130, height: 55, text: ['(2a) Expand the', 'role and use of', 'existing', 'multilateral', 'arrangements'] },
|
|
{ id: 'a2b', type: 'action', width: 130, height: 45, text: ['(2b) Expand US', 'government', 'financing tools'] },
|
|
{ id: 'a2c', type: 'action', width: 130, height: 45, text: ['(2c) Support', 'exploration and', 'data initiatives'] },
|
|
{ id: 'a3a', type: 'action', width: 130, height: 55, text: ['(3a) Develop a', 'real-time value', 'chain mapping', 'and monitoring', 'system'] },
|
|
{ id: 'a3b', type: 'action', width: 130, height: 55, text: ['(3b) Expand', 'purpose and', 'use of National', 'Defense', 'Stockpile'] },
|
|
{ id: 'a3c', type: 'action', width: 130, height: 55, text: ['(3c) Identify and', 'deploy targeted', 'market', 'incentives'] },
|
|
];
|
|
|
|
const links = [
|
|
{ source: 'goal', target: 'p1' }, { source: 'goal', target: 'p2' }, { source: 'goal', target: 'p3' },
|
|
{ source: 'p1', target: 'int1' }, { source: 'p2', target: 'int2' },
|
|
{ source: 'p3', target: 'int5' }, { source: 'p3', target: 'int6' }, { source: 'p3', target: 'int7' },
|
|
{ source: 'int1', target: 'a1e' }, { source: 'int1', target: 'a1f' }, { source: 'int1', target: 'a1c' },
|
|
{ source: 'int1', target: 'a1b/2d' },
|
|
{ source: 'int3', target: 'a1d' },
|
|
{ source: 'a1a', target: 'int1' },
|
|
{ source: 'a1b/2d', target: 'int2' },
|
|
{ source: 'a1c', target: 'int3' }, { source: 'a1f', target: 'int3' },
|
|
{ source: 'a2a', target: 'int2' }, { source: 'a2b', target: 'a2a' },
|
|
{ source: 'a2a', target: 'int4' },
|
|
{ source: 'a2b', target: 'int4' },
|
|
{ source: 'int4', target: 'a2c' },
|
|
{ source: 'a3a', target: 'int5' }, { source: 'a3b', target: 'int6' }, { source: 'a3c', target: 'int7' },
|
|
{ source: 'int6', target: 'a3b' }
|
|
];
|
|
|
|
// 2. SVG Setup and Layout
|
|
const width = 960;
|
|
const height = 850;
|
|
const nodePositions = {
|
|
'goal': { x: width / 2, y: 50 },
|
|
'p1': { x: 150, y: 170 },
|
|
'p2': { x: width / 2, y: 170 },
|
|
'p3': { x: width - 150, y: 170 },
|
|
'int1': { x: 150, y: 320 },
|
|
'int2': { x: width / 2, y: 320 },
|
|
'int3': { x: 150, y: 510 },
|
|
'int4': { x: width / 2, y: 510 },
|
|
'int5': { x: 570, y: 320 },
|
|
'int6': { x: 710, y: 320 },
|
|
'int7': { x: 850, y: 320 },
|
|
'a1e': { x: 50, y: 420 },
|
|
'a1f': { x: 50, y: 510 },
|
|
'a1c': { x: 50, y: 600 },
|
|
'a1d': { x: 150, y: 600 },
|
|
'a1a': { x: 250, y: 420 },
|
|
'a1b/2d': { x: 250, y: 510 },
|
|
'a2a': { x: width / 2, y: 420 },
|
|
'a2b': { x: 350, y: 510 },
|
|
'a2c': { x: width / 2, y: 600 },
|
|
'a3a': { x: 570, y: 420 },
|
|
'a3b': { x: 710, y: 420 },
|
|
'a3c': { x: 850, y: 420 },
|
|
};
|
|
|
|
const svg = d3.select("#chart").append("svg")
|
|
.attr("width", width)
|
|
.attr("height", height);
|
|
|
|
// Define arrow markers
|
|
svg.append("defs").append("marker")
|
|
.attr("id", "arrow")
|
|
.attr("viewBox", "0 -5 10 10")
|
|
.attr("refX", 8)
|
|
.attr("refY", 0)
|
|
.attr("markerWidth", 5)
|
|
.attr("markerHeight", 5)
|
|
.attr("orient", "auto")
|
|
.append("path")
|
|
.attr("d", "M0,-5L10,0L0,5")
|
|
.attr("fill", "#616161");
|
|
|
|
// 3. Render Links
|
|
const linkGroup = svg.append("g").attr("class", "links");
|
|
links.forEach(link => {
|
|
const sourceNode = nodes.find(n => n.id === link.source);
|
|
const targetNode = nodes.find(n => n.id === link.target);
|
|
const sourcePos = nodePositions[link.source];
|
|
const targetPos = nodePositions[link.target];
|
|
|
|
let pathData;
|
|
const sx = sourcePos.x, sy = sourcePos.y;
|
|
const tx = targetPos.x, ty = targetPos.y;
|
|
const sourceHalfHeight = sourceNode.height / 2;
|
|
const targetHalfHeight = targetNode.height / 2;
|
|
const sourceHalfWidth = sourceNode.width / 2;
|
|
const targetHalfWidth = targetNode.width / 2;
|
|
|
|
// Simple routing for orthogonal lines
|
|
if (sx === tx) { // Vertical
|
|
pathData = `M ${sx},${sy < ty ? sy + sourceHalfHeight : sy - sourceHalfHeight} L ${tx},${sy < ty ? ty - targetHalfHeight : ty + targetHalfHeight}`;
|
|
} else if (sy === ty) { // Horizontal
|
|
pathData = `M ${sx < tx ? sx + sourceHalfWidth : sx - sourceHalfWidth},${sy} L ${tx < sx ? tx + targetHalfWidth : tx - targetHalfWidth},${ty}`;
|
|
} else { // L-shaped connectors
|
|
const midY = sy + (ty-sy)/2;
|
|
pathData = `M ${sx},${sy < ty ? sy + sourceHalfHeight : sy - sourceHalfHeight} L ${sx},${midY} L ${tx},${midY} L ${tx},${sy < ty ? ty - targetHalfHeight : ty + targetHalfHeight}`;
|
|
}
|
|
|
|
// Custom paths based on the diagram
|
|
if( (link.source === 'a1c' && link.target === 'int3') || (link.source === 'a1f' && link.target === 'int3') ) {
|
|
pathData = `M ${sx + sourceHalfWidth},${sy} L ${tx - targetHalfWidth},${ty}`;
|
|
} else if (link.source === 'a1b/2d' && link.target === 'int2') {
|
|
pathData = `M ${sx + sourceHalfWidth},${sy} L ${tx - targetHalfWidth},${ty}`;
|
|
} else if (link.source === 'a2b' && link.target === 'a2a') {
|
|
pathData = `M ${sx},${sy - sourceHalfHeight} L ${tx},${ty + targetHalfHeight}`;
|
|
} else if (link.source === 'a2a' && link.target === 'int4') {
|
|
pathData = `M ${sx},${sy + sourceHalfHeight} L ${tx},${ty - targetHalfHeight}`;
|
|
} else if (link.source === 'a2b' && link.target === 'int4') {
|
|
const midX = sx + (tx-sx)/2;
|
|
pathData = `M ${sx-sourceHalfWidth},${sy} L ${midX},${sy} L ${midX},${ty+targetHalfHeight}`;
|
|
}
|
|
|
|
linkGroup.append("path")
|
|
.attr("class", "link")
|
|
.attr("d", pathData)
|
|
.attr("marker-end", "url(#arrow)");
|
|
});
|
|
|
|
|
|
// 4. Render Nodes
|
|
const nodeGroups = svg.selectAll("g.node-group")
|
|
.data(nodes)
|
|
.enter()
|
|
.append("g")
|
|
.attr("class", d => `node-group node-${d.type}`)
|
|
.attr("transform", d => `translate(${nodePositions[d.id].x}, ${nodePositions[d.id].y})`);
|
|
|
|
// Add rectangles
|
|
nodeGroups.append("rect")
|
|
.attr("class", "node-rect")
|
|
.attr("x", d => -d.width / 2)
|
|
.attr("y", d => -d.height / 2)
|
|
.attr("width", d => d.width)
|
|
.attr("height", d => d.height)
|
|
.attr("rx", 3) // Rounded corners
|
|
.attr("ry", 3);
|
|
|
|
// Add text labels
|
|
const text = nodeGroups.append("text")
|
|
.attr("class", "node-text")
|
|
.attr("dy", d => (d.kpis ? -5 : 0)); // Adjust vertical position for nodes with KPIs
|
|
|
|
text.each(function(d) {
|
|
const el = d3.select(this);
|
|
const lineHeight = 12; // Adjust this value for line spacing
|
|
const initialY = -( (d.text.length -1) * lineHeight) / 2; // Calculate starting Y to center the block
|
|
|
|
d.text.forEach((line, i) => {
|
|
el.append("tspan")
|
|
.attr("x", 0)
|
|
.attr("y", initialY + (i * lineHeight))
|
|
.text(line)
|
|
.style("font-weight", (i === 0 && (d.type === 'pillar' || d.text[0].startsWith('(')) ? "bold" : "normal"));
|
|
});
|
|
|
|
// Add KPI text for pillar nodes
|
|
if (d.kpis) {
|
|
const kpiYstart = d.height / 2 - (d.kpis.length * 10) - 4;
|
|
d.kpis.forEach((line, i) => {
|
|
el.append("tspan")
|
|
.attr("class", "kpi-text")
|
|
.attr("x", 0)
|
|
.attr("y", kpiYstart + (i * 10))
|
|
.text(line);
|
|
});
|
|
}
|
|
});
|
|
|
|
</script>
|
|
|
|
</body>
|
|
</html> |