chart = {
const width_ = width > 650 ? 650 : 350;
const height = 700;
const nodes = Array.from(
new Set(data.flatMap((l) => [l.source, l.target])),
(id) => ({ id })
);
const links = data.map((d) => Object.create(d));
const cities = Array.from(new Set(data.map((d) => d.source)));
console.log("cities", cities, links);
const color = d3.scaleOrdinal(d3.schemeCategory10);
const simulation = d3
.forceSimulation(nodes)
.force(
"link",
d3.forceLink(links).id((d) => d.id)
)
.force("charge", d3.forceManyBody().strength(width > 650 ? -600 : -300))
.force("x", d3.forceX())
.force("y", d3.forceY());
const svg = d3
.create("svg")
.attr("viewBox", [-width_ / 2, -height / 2.1, width_, height])
.attr("width", width_)
.attr("height", height)
.attr("style", "max-width: 100%; height: auto; font: 12px sans-serif;");
// .selectAll("marker")
// .data(types)
// .join("marker")
// .attr("id", (d) => `arrow-${d}`)
// .attr("viewBox", "0 -5 10 10")
// .attr("refX", 15)
// .attr("refY", -0.5)
// .attr("markerWidth", 6)
// .attr("markerHeight", 6)
// .attr("orient", "auto")
// .append("path")
// .attr("fill", color)
// .attr("d", "M0,-5L10,0L0,5");
const tooltip = d3
.select("body")
.append("div")
.attr("class", "toolTip")
.style("position", "absolute")
.style("visibility", "hidden")
.text("Placeholder");
//link connections taken straight from Raven Gao: https://observablehq.com/@ravengao/force-directed-graph-with-cola-grouping -------
// create link reference
let linkedByIndex = {};
links.forEach((d) => {
linkedByIndex[`${d.source.id},${d.target.id}`] = true;
});
// nodes map
let nodesById = {};
nodes.forEach((d) => {
nodesById[d.id] = { ...d };
});
const isConnectedAsSource = (a, b) => linkedByIndex[`${a},${b}`];
const isConnectedAsTarget = (a, b) => linkedByIndex[`${b},${a}`];
const isConnected = (a, b) =>
isConnectedAsTarget(a, b) || isConnectedAsSource(a, b) || a === b;
//mouse over and mouse out functions adapted from Raven Gao: https://observablehq.com/@ravengao/force-directed-graph-with-cola-grouping -------
const nodeMouseOver = (d, links) => {
tooltip.style("visibility", "visible").html(createTooltipText(links, d.id));
node.transition(500).style("opacity", (o) => {
console.log("o", o, d);
const isConnectedValue = isConnected(o.id, d.id);
if (isConnectedValue) {
return 1.0;
}
return 0.1;
});
link
.transition(500)
.style("stroke-opacity", (o) => {
// console.log(o.source.id === d.id);
return o.source.id === d.id || o.target.id === d.id ? 1 : 0.1;
})
.transition(500)
.attr("marker-end", (o) =>
o.source.id === d.id || o.target.id === d.id
? "url(#arrowhead)"
: "url()"
);
};
const nodeMouseOut = (e, d) => {
tooltip.style("visibility", "hidden");
node.transition(500).style("opacity", 1);
link.transition(500).style("stroke-opacity", (o) => {
// console.log(o.value);
});
};
const link = svg
.append("g")
.attr("stroke", "#D0D0D0")
.attr("stroke-opacity", 0.6)
.selectAll("line")
.data(links)
.join("line")
.attr("stroke-width", (d) => (toggle ? Math.sqrt(d.value) / 10 : 1));
const node = svg
.append("g")
.attr("fill", "currentColor")
.attr("stroke-linecap", "round")
.attr("stroke-linejoin", "round")
.selectAll("g")
.data(nodes)
.join("g")
.call(drag(simulation));
const rus = [
"Artik & Asti",
"Jakone",
"Niletto",
"ANNA ASTI",
"Руки Вверх",
"Кино",
"Михайло Круг",
"NЮ",
"Король и Шут",
"Stas Mikhaylov",
"MACAN",
"HammAli & Navai",
"Гіо Піка",
"Miyagi & Andy Panda",
"Баста",
"Jah Khalib",
"Каспійський груз",
"Guf",
"Egor Kreed",
"Ендшпіль",
"Єгор Ракітін",
"MiyaGi",
"Криминальный бит",
"Скриптоніт"
];
node
.append("circle")
.attr("stroke", "white")
.attr("stroke-width", 1.5)
.attr("opacity", 0.9)
.attr("fill", (d) => (rus.includes(d.id) ? "red" : "#333"))
.attr("r", (d) =>
width > 650 ? (rus.includes(d.id) ? 24 : 4) : rus.includes(d.id) ? 18 : 3
);
width > 650
? node
.append("image")
.attr("xlink:href", (d) => {
const matchingRefImg = images.find((i) => i.artistName == d.id);
return matchingRefImg ? matchingRefImg.image : "";
})
.attr("x", "-20px")
.attr("clip-path", "inset(0 0 0 0 round 50%)")
.attr("y", "-20px")
.attr("width", "40px")
.attr("height", "40px")
.on("mouseover", (e, d) => nodeMouseOver(d, links))
.on("mouseout", nodeMouseOut)
: node
.append("image")
.attr("xlink:href", (d) => {
const matchingRefImg = images.find((i) => i.artistName == d.id);
return matchingRefImg ? matchingRefImg.image : "";
})
.attr("x", "-15px")
.attr("clip-path", "inset(0 0 0 0 round 50%)")
.attr("y", "-15px")
.attr("width", "30px")
.attr("height", "30px")
.on("mouseover", (e, d) => nodeMouseOver(d, links))
.on("mouseout", nodeMouseOut);
node
.append("text")
.attr("y", (d) => (cities.includes(d.id) ? 12 : 30))
.attr("text-anchor", "middle")
.text((d) => d.id)
.attr("fill", "none")
.attr("stroke", "white")
.attr("stroke-width", 2);
node
.append("text")
.attr("y", (d) => (cities.includes(d.id) ? 12 : 30))
.attr("text-anchor", "middle")
.text((d) => d.id)
.attr("fill", (d) => (cities.includes(d.id) ? "#000" : "#777"))
.attr("font-weight", (d) => (cities.includes(d.id) ? 600 : 300));
simulation.on("tick", () => {
// link.attr("d", linkArc);
link
.attr("x1", (d) => d.source.x)
.attr("y1", (d) => d.source.y)
.attr("x2", (d) => d.target.x)
.attr("y2", (d) => d.target.y);
node.attr("transform", (d) => `translate(${d.x},${d.y})`);
});
invalidation.then(() => simulation.stop());
return Object.assign(svg.node(), { scales: { color } });
}