chart = {
const width = 928;
const height = width;
const {names, linkValues, categoryFilteredValues, categorySpecificValues, countryToContinent, categoryFilter, categoryPairsMap} = data;
const outerRadius = Math.min(width, height) * 0.5 - 70;
const innerRadius = outerRadius - 10;
const tickStep = d3.tickStep(0, d3.sum(data.flat()), 100);
const formatValue = d3.format(".1~%");
const isDarkMode = darkMode === "dark";
const backgroundColor = isDarkMode ? "#1a1a1a" : "#ffffff";
const textColor = isDarkMode ? "#ffffff" : "#000000";
const fadedTextColor = isDarkMode ? "#cccccc" : "#666666";
const volumeByName = {};
names.forEach((name, i) => {
volumeByName[name] = d3.sum(data[i]);
});
const sortedNames = [...names].sort((a, b) => {
if (a === "Other") return 1;
if (b === "Other") return -1;
return volumeByName[b] - volumeByName[a];
});
const nameToOldIndex = {};
names.forEach((name, i) => nameToOldIndex[name] = i);
const reorderedMatrix = sortedNames.map(sourceName =>
sortedNames.map(targetName =>
data[nameToOldIndex[sourceName]][nameToOldIndex[targetName]]
)
);
const chord = d3.chord()
.padAngle(10 / innerRadius)
.sortSubgroups(d3.descending)
.sortChords(d3.descending);
const arc = d3.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius);
const ribbon = d3.ribbon()
.radius(innerRadius - 1)
.padAngle(1 / innerRadius);
// Use the dynamic color scale based on selection
const color = getColorScale(colorScheme, sortedNames, reorderedMatrix, countryToContinent);
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [-width / 2, -height / 2, width, height])
.attr("style", `width: 100%; height: auto; font: 10px sans-serif; background-color: ${backgroundColor};`);
// Add CSS styles for hover effects (with dark mode support)
svg.append("style").text(`
.chord-arc {
opacity: 1;
}
.chord-arc.fade {
opacity: 0.2;
}
.chord-arc:hover {
opacity: 0.9;
}
.chord-ribbon {
opacity: 0.8;
mix-blend-mode: ${isDarkMode ? 'lighten' : 'multiply'};
}
.chord-ribbon.fade {
opacity: 0.1;
}
.chord-ribbon.filtered {
display: none;
}
text {
fill: ${textColor};
font-weight: bold;
}
.tick-text {
fill: ${fadedTextColor};
font-weight: normal;
}
`);
const chords = chord(reorderedMatrix);
const group = svg.append("g")
.selectAll("g")
.data(chords.groups)
.join("g");
group.append("path")
.attr("class", "chord-arc")
.attr("fill", d => color(sortedNames[d.index]))
.attr("d", arc)
.attr("id", d => `arc-${d.index}`)
.on("mouseenter", function(event, d) {
// Fade all chords and arcs
svg.selectAll(".chord-ribbon, .chord-arc")
.classed("fade", true);
// Build array of connected indices
const connectedIndices = new Set();
// First identify all direct connections through ribbons
svg.selectAll(`.chord-ribbon.source-${d.index}, .chord-ribbon.target-${d.index}`)
.each(function(ribbonData) {
// Only consider visible (not filtered) ribbons
if (!d3.select(this).classed("filtered")) {
// Only add the connected index (not this arc's index)
if (ribbonData.source.index === d.index) {
connectedIndices.add(ribbonData.target.index);
} else {
connectedIndices.add(ribbonData.source.index);
}
}
})
.classed("fade", false);
// Add the current arc index
connectedIndices.add(d.index);
// Unfade only directly connected arcs and current arc
connectedIndices.forEach(index => {
svg.select(`#arc-${index}`).classed("fade", false);
});
})
.on("mouseleave", function() {
// Reset all elements to normal state
svg.selectAll(".chord-ribbon, .chord-arc")
.classed("fade", false);
});
group.append("title")
.text(d => {
// Format raw value in dollars
const rawValue = data.volumeByItem ? data.volumeByItem[sortedNames[d.index]] : 0;
const dollarValue = formatBigNumber(rawValue);
return `${sortedNames[d.index]}\n$${dollarValue}`;
});
const groupTick = group.append("g")
.selectAll("g")
.data(d => groupTicks(d, tickStep))
.join("g")
.attr("transform", d => `rotate(${d.angle * 180 / Math.PI - 90}) translate(${outerRadius},0)`);
groupTick.append("text")
.attr("class", "tick-text")
.attr("x", 8)
.attr("dy", "0.35em")
.attr("transform", d => d.angle > Math.PI ? "rotate(180) translate(-16)" : null)
.attr("text-anchor", d => d.angle > Math.PI ? "end" : null);
group.select("text")
.attr("font-weight", "bold")
.text(function(d) {
return this.getAttribute("text-anchor") === "end"
? `↑ ${sortedNames[d.index]}`
: `${sortedNames[d.index]} ↓`;
});
const ribbons = svg.append("g")
.attr("fill-opacity", 0.8)
.selectAll("path")
.data(chords)
.join("path")
.attr("class", d => `chord-ribbon source-${d.source.index} target-${d.target.index}`)
.attr("fill", d => color(sortedNames[d.source.index]))
.attr("d", ribbon);
// Apply category filtering to ribbons if needed
if (categoryFilter && categoryFilter !== "all") {
ribbons.each(function(d) {
const sourceName = sortedNames[d.source.index];
const targetName = sortedNames[d.target.index];
// Check if this ribbon is related to the selected category
const sourceToTargetKey = `${sourceName}_${targetName}`;
const targetToSourceKey = `${targetName}_${sourceName}`;
// If filtered values for this pair don't exist, hide the ribbon
if (!categoryFilteredValues[sourceToTargetKey] && !categoryFilteredValues[targetToSourceKey]) {
d3.select(this).classed("filtered", true);
}
});
}
ribbons.on("mouseenter", function(event, d) {
// Fade all chords and arcs
svg.selectAll(".chord-ribbon, .chord-arc")
.classed("fade", true);
// Keep only this chord visible
d3.select(this)
.classed("fade", false);
// Keep only connected arcs visible
const sourceArc = svg.select(`#arc-${d.source.index}`);
const targetArc = svg.select(`#arc-${d.target.index}`);
sourceArc.classed("fade", false);
targetArc.classed("fade", false);
})
.on("mouseleave", function() {
// Reset all elements to normal state
svg.selectAll(".chord-ribbon, .chord-arc")
.classed("fade", false);
});
ribbons.append("title")
.text(function(d){
// Get the names of source and target
let sourceName = sortedNames[d.source.index];
let targetName = sortedNames[d.target.index];
// If the ribbon is filtered (hidden), don't show any tooltip
if (d3.select(this).classed("filtered")) {
return "";
}
// Create keys for both directions
const sourceToTargetKey = `${sourceName}_${targetName}`;
const targetToSourceKey = `${targetName}_${sourceName}`;
// CHANGED: Handle source-target values based on category filter
let sourceToTargetValue, targetToSourceValue;
let lineTitle = '';
if (categoryFilter !== "all") {
// Try to get category-specific values from categories CSV
const sourceOrigKey = `${sourceName}_${targetName}`;
const targetOrigKey = `${targetName}_${sourceName}`;
sourceToTargetValue = categorySpecificValues[sourceOrigKey];
targetToSourceValue = categorySpecificValues[targetOrigKey];
} else {
// Use total values for "All Categories"
sourceToTargetValue = categoryFilteredValues[sourceToTargetKey];
targetToSourceValue = categoryFilteredValues[targetToSourceKey];
}
// Build tooltip text
if (sourceToTargetValue) {
let fromSourceToTargetValue = formatBigNumber(sourceToTargetValue);
lineTitle += `${fromSourceToTargetValue} ${sourceName} → ${targetName}`;
}
if (targetToSourceValue && d.source.index !== d.target.index) {
let fromTargetToSourceValue = formatBigNumber(targetToSourceValue);
lineTitle += `\n${fromTargetToSourceValue} ${targetName} → ${sourceName}`;
}
return lineTitle;
});
return svg.node();
}