function SankeyChart({
nodes,
links
}, {
format = ",",
nodeId = d => d.id,
nodeGroup,
nodeGroups,
nodeLabel,
nodeTitle = d => `${d.id}\n${format(d.value)}`,
nodeAlign = d3Sankey.sankeyLeft,
nodeWidth = _nodeWidth,
nodePadding = _padding,
nodeLabelPadding = 5,
nodeStroke = "currentColor",
nodeStrokeWidth,
nodeStrokeOpacity,
nodeStrokeLinejoin,
linkSource = ({source}) => source,
linkTarget = ({target}) => target,
linkValue = ({value}) => value,
linkPath = straightLine,
linkTitle = d => `${d.source.id} → ${d.target.id}\n${format(d.value)}`,
linkColor = 'layered',
linkStrokeOpacity = linkOpacity/100,
colors = colorScale,
width = _width,
height = _height,
marginTop = 5,
marginRight = 1,
marginBottom = 5,
marginLeft = 1,
} = {}) {
const LS = d3.map(links, linkSource).map(intern);
const LT = d3.map(links, linkTarget).map(intern);
const LV = d3.map(links, linkValue);
if (nodes === undefined) nodes = Array.from(d3.union(LS, LT), id => ({id}));
const N = d3.map(nodes, nodeId).map(intern);
const G = nodeGroup == null ? null : d3.map(nodes, nodeGroup).map(intern);
// Replace the input nodes and links with mutable objects for the simulation.
nodes = d3.map(nodes, (_, i) => ({id: N[i]}));
links = d3.map(links, (_, i) => ({source: LS[i], target: LT[i], value: LV[i]}));
// Compute default domains.
if (G && nodeGroups === undefined) nodeGroups = G;
// Construct the scales.
const color = nodeGroup == null ? null : d3.scaleOrdinal(nodeGroups, colors);
// Compute the Sankey layout.
const sankey = d3Sankey
.sankey()
.nodeId(({index: i}) => N[i])
.nodeAlign(nodeAlign)
.nodeWidth(nodeWidth)
.nodePadding(nodePadding)
.nodeSort(null)
.iterations(_itterations)
.linkSort(linkSort)
.extent([[marginLeft, marginTop], [width - marginRight, height - marginBottom]])
({nodes, links});
// Compute titles and labels using layout nodes, so as to access aggregate values.
if (typeof format !== "function") format = d3.format(format);
const Tl = nodeLabel === undefined ? N : nodeLabel == null ? null : d3.map(nodes, nodeLabel);
const Tt = nodeTitle == null ? null : d3.map(nodes, nodeTitle);
const Lt = linkTitle == null ? null : d3.map(links, linkTitle);
//const Vl = nodeValue === undefined ? N : nodeValue == null ? null : d3.map(nodes, nodeValue);
// A unique identifier for clip paths (to avoid conflicts).
const uid = `O-${Math.random().toString(16).slice(2)}`;
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [0, 0, width, height])
.attr("style", "max-width: 100%; height: auto; height: intrinsic;");
const link = svg.append("g")
.attr("fill", "none")
.selectAll("g")
.data(links.sort(drawSort))
.join("g")
.attr("stroke-opacity", d => {
if (d.value <= linkThreshold) return 0;
return linkStrokeOpacity;
})
var totalNodes = 16;
link.append("path")
.attr("d", linkPath)
.attr("stroke", function(d) {
if (linkColor === "layered") {
// Use the color of the target node for the last two nodes
if (d.target.index >= totalNodes - 2) {
return color(G[d.target.index]);
} else {
// Use the color of the source node for other nodes
return color(G[d.source.index]);
}
} else if (linkColor === "source-target") {
return `url(#${uid}-link-${d.index})`;
} else if (linkColor === "source") {
return color(G[d.source.index]);
} else if (linkColor === "target") {
return color(G[d.target.index]);
} else {
// Handle other cases or default behavior here
return linkColor;
}
})
.attr("stroke-width", ({width}) => Math.max(1, width))
.attr('stroke-linejoin', 'bevel')
.call(Lt ? path => path.append("title").text(({index: i}) => Lt[i]) : () => {});
const node = svg.append("g")
.attr("stroke", nodeStroke)
.attr("stroke-width", nodeStrokeWidth)
.attr("stroke-opacity", 1)
.attr("stroke-linejoin", nodeStrokeLinejoin)
.selectAll("rect")
.data(nodes)
.join("rect")
.attr("x", d => d.x0)
.attr('y', d => {
if (d.id === 'Grid') return 0;
return d.y0;
})
.attr("height", d => d.y1 - d.y0,30)
.attr("width", d => d.x1 - d.x0);
if (G) node.attr("fill", ({index: i}) => color(G[i]));
if (Tt) node.append("title").text(({index: i}) => Tt[i]);
const nodeVal = 12.75;
const nodeValsm = 4.5;
const Labels = svg.append("g")
.attr("font-family", "sans-serif")
.attr("font-size", 10)
.selectAll("text")
.data(nodes)
.join("text")
.attr("x", d => d.x0 + 34 + (d.layer * 2))
.attr("y", d => {
if(_mode==='Verbose'){
if (d.id === 'Grid') return ((d.y1 - d.y0) / 2) - 5;
else if (d.value <= nodeValsm) {
if (["Coal","Biomass","Petroleum"].includes(d.id)) return d.y0 - 20;
return d.y1 + 8;
}
else if (d.value > nodeVal) return ((d.y1 + d.y0) / 2) - 5;
return (d.y1 + d.y0) / 2;
}
else {
if (d.value <= nodeValsm) {
if (d.id === 'Solar') return d.y1 + 8;
return d.y0 - 8;
}
if (d.id === 'Grid') return (d.y1 - d.y0) / 2;
return (d.y1 + d.y0) / 2;
}
})
.attr('fill', d => {
if (['Nuclear', 'Hydro', 'Wind', 'Geothermal', 'Coal', 'Petroleum', 'Energy Services'].includes(d.id)) {
if (d.value > nodeValsm ) return 'white';
}
return 'black';
})
.attr("dy", "0.35em")
.attr("text-anchor", "middle")
.text(({ index: i }) => Tl[i]);
if (_mode === "Verbose") Labels.append("tspan")
.attr("x", d => d.x0 + 34 + (d.layer * 2))
.attr("y", d => {
if (d.value <= nodeValsm) {
if (["Coal","Biomass","Petroleum"].includes(d.id)) return d.y0 - 8;
return d.y1 + 20;
}
else if(d.value <= nodeVal){
if (["Coal","Biomass","Petroleum"].includes(d.id)) return d.y0 - 8;
return d.y1 + 10;
}
if (d.id === 'Grid') return ((d.y1 - d.y0) / 2) + 6;
return ((d.y1 + d.y0) / 2) + 6;
})
.attr('fill', d => {
if (['Nuclear', 'Hydro', 'Wind', 'Geothermal', 'Coal', 'Petroleum', 'Energy Services'].includes(d.id)) {
if ((d.value > nodeValsm && _mode !== "Verbose") || (d.value > nodeVal && _mode === "Verbose")) return 'white';
}
return 'black';
})
.attr("dy", "0.35em")
.attr("text-anchor", "middle")
.text(d => String(d.value.toFixed(2)) + " Quads");
const linkVal = 6;
if (_mode === "Verbose") svg.append("g")
.attr("font-family", "sans-serif")
.attr("font-size", 10)
.selectAll("text")
.data(links)
.join("text")
.attr("x", d => {if (d.source.id === "Hydro" || d.source.id === "Wind") return d.source.x1 + 12;
else if (d.target.id === "Rejected Energy" || d.target.id === "Energy Services") {
if (d.source.id === "Grid") return ((d.target.x0 - d.source.x1 )/2) + d.source.x1;
return d.source.x1+15;}
else if (d.value > linkVal ) return d.target.x0 - 15;
else if (["Residential","Commercial","Industrial","Transportation"].includes(d.target.id) && ["Grid","Petroleum"].includes(d.source.id)) {
if (d.source.id === "Grid" || d.source.id === "Petroleum") return d.target.x0 - 15;
}
else if (d.source.id === "Geothermal"){
if (d.y0 <= _height/2)
return startKneeOffset + (d.source.y0-d.y0) + ((3/2)*(_layerValEntry*((d.target.layer-d.source.layer)-1)))+50;
return startKneeOffset - (d.source.y0-d.y0) + ((3/2)*(_layerValEntry*((d.target.layer-d.source.layer)-1)))+50;}
else if (d.source.id === "Solar"){
return startKneeOffset - (d.source.y0-d.y0) + ((3/2)*(_layerValEntry*((d.target.layer-d.source.layer)-1)))+100;}
else if (d.source.id === "Natural Gas"){
if (d.y0 <= _height/2)
return startKneeOffset + (d.source.y0-d.y0) + ((3/2)*(_layerValEntry*((d.target.layer-d.source.layer)-1)))+105;
return startKneeOffset - (d.source.y0-d.y0) + ((3/2)*(_layerValEntry*((d.target.layer-d.source.layer)-1)))+105;}
else if (d.y0 <= _height/2)
return startKneeOffset + (d.source.y0-d.y0) + ((3/2)*(_layerValEntry*((d.target.layer-d.source.layer)-1)))+85;
return startKneeOffset - (d.source.y0-d.y0) + ((3/2)*(_layerValEntry*((d.target.layer-d.source.layer)-1)))+85;})
.attr("y", d => {if (d.source.id === "Hydro" || d.source.id === "Wind") return d.y0 - 8;
else if (d.value > linkVal) {
if (d.target.id === "Rejected Energy" || d.target.id === "Energy Services") {
if (d.source.id === "Grid") return ((d.y1 - (d.y0 - d.source.y0))/2)+((d.source.y1-d.source.y0)/2)+10;
return d.y0;}
else if(d.target.id === 'Grid') return d.y1 - d.target.y0 ;
return d.y1;}
else if (["Residential","Commercial","Industrial","Transportation"].includes(d.target.id) && ["Grid","Petroleum"].includes(d.source.id)) {
if (d.source.id === "Grid" ) return d.y1 - 10;
if (d.source.id === "Petroleum") return d.y1 + 10;
}
else if (d.target.id === "Rejected Energy") return d.y0 - 10;
else if (d.target.id === "Energy Services") return d.y0 + 10;
else if (d.target.id === "Grid") {if (d.y0 <= _height/2)
if(d.y0 <10) return d.y0 + 10;
return d.y0 - 14;
if(d.y0>(_height-10)) return d.y0 - 14;
return d.y0 + 10;}
else if (d.source.id === "Natural Gas"){
if (d.y1 <= _height/2)
if (d.y0 <= height/2) return ((d.y1-d.y0)/1.15)+d.y0;
return ((d.y1-d.y0)/1.15)+d.y0 - 10;
return ((d.y1-d.y0)/1.6)+d.y0 + 25;}
else if (d.source.id === "Solar"){
return ((d.y1-d.y0)/1.2)+d.y0 -25;}
else if (d.source.id === "Geothermal"){
if (d.y1 <= _height/2)
return ((d.y1-d.y0)/1.7)+d.y0 - 3;
return ((d.y1-d.y0)/1.7)+d.y0 + 7;
}
else if (d.y1 <= _height/2)
return ((d.y1-d.y0)/1.75)+d.y0 - 10;
return ((d.y1-d.y0)/1.75)+d.y0 + 10;})
.attr('fill', d => {if(d.value > linkThreshold) {
if (d.value > linkVal) {if (d.source.id === "Solar" || d.target.id === "Rejected Energy" || d.target.id === "Energy Services") return "black";
return "white";}
else if (d.source.id === "Coal" || d.source.id === "Solar" || d.target.id === "Rejected Energy" || d.target.id === "Energy Services") return "black";
else if (d.target.index >= totalNodes - 2) {
return color(G[d.target.index]);
} else {
// Use the color of the source node for other nodes
return color(G[d.source.index]);
}
}
return "none";
})
.attr("dy", "0.35em")
.attr("text-anchor", "middle")
.text(d => String(d.value.toFixed(2)));
let val1 = data1.data.reduce(add,0)
let val2 = data2.data.reduce(add,0)
if (_view !== "Emissions"){
const cloudData = [
{ x: 10, y: 40 },
{ x: 20, y: 32 },
{ x: 30, y: 40 },
{ x: 40, y: 32 },
{ x: 50, y: 40 },
{ x: 60, y: 32 },
{ x: 70, y: 40 },
{ x: 60, y: 48 },
{ x: 50, y: 56 },
{ x: 40, y: 48 },
{ x: 30, y: 56 },
{ x: 20, y: 48 },
{ x: 10, y: 56 },
];
svg.append("g")
.selectAll("circle")
.data(cloudData)
.enter()
.append("circle")
.attr("cx", d => d.x+(width*4/5))
.attr("cy", d => d.y-10)
.attr("r", val1/450)
.attr("fill", "grey")
.attr("stroke", "grey");
svg.append("text")
.attr("x", 38+(width*4/5))
.attr("y", 37)
.attr("font-size", 12)
.attr("fill","white")
.attr("text-anchor", "middle")
.text(String(val1.toFixed(1))+' MT C02');}
if(_view !== "Cost of Energy") {
svg.append("text")
.attr("x", width*9/10)
.attr("y", 45)
.attr("font-size", val2/50)
.attr("fill","green")
.text("$");
svg.append("text")
.attr("x", width*9.1/10)
.attr("y", 45)
.attr("font-size", 15)
.attr("fill","green")
.text(String(val2.toFixed(1))+' per MWh');}
if(_view != "Consumption"){
// Define the lightning bolt path data
const lightningBoltPathData = "M53.183,1.1c-0.924-0.022-1.774,0.479-2.245,1.268L26.827,49.174c-0.389,0.652-0.387,1.45,0.006,2.101 c0.393,0.65,1.084,1.058,1.846,1.058h40.005c0.848,0,1.569-0.557,1.816-1.331l28.97-71.554c0.294-0.73-0.053-1.548-0.733-1.943 C54.52,1.266,53.851,1.119,53.183,1.1z";
// Append the lightning bolt path to the SVG element
svg.append("path")
.attr("d", lightningBoltPathData)
.attr("transform",_view === "Emissions" ? `translate(${9/10 * svg.attr("width")}, ${5/10 * svg.attr("height")}))` : `translate(${4/5 * svg.attr("width")}, ${5/10 * svg.attr("height")}))`)
.attr("stroke", "black")
.attr("stroke-width", 2)
.attr("fill", "yellow");
svg.append("text")
.attr("x", _view === "Emissions" ? width*9.2/10 : width*4.1/5)
.attr("y", 45)
.attr("font-size", 12)
.attr("fill","black")
.text(_view === "Emissions" ? String(val1.toFixed(1))+' Quads of work done' : String(val2.toFixed(1))+' Quads of work done');
}
function intern(value) {
return value !== null && typeof value === "object" ? value.valueOf() : value;
}
return Object.assign(svg.node(), {scales: {color}});
}