Published
Edited
Feb 19, 2021
5 stars
Insert cell
Insert cell
chart = {
const links = data.links.map(d => Object.create(d));
const nodes = data.nodes.map(d => Object.create(d));
const nodeRadius = 17;

const simulation = d3
.forceSimulation(nodes)
.force(
"link",
d3
.forceLink(links)
.distance(d => d.distance)
.id(d => d.id)
)
.force("charge", d3.forceManyBody().strength(-200))
.force("collide", d3.forceCollide(d => getRadius(d.group)))
.force("center", d3.forceCenter(width / 2, height / 2))
.force("x", d3.forceX(width / 2).strength(0))
.force("y", d3.forceY(height * 0.4).strength(0));

const svg = d3.select(DOM.svg(width, height));
const defs = svg.append('defs');
defs
.selectAll('clipPath')
.data(groupData)
.join('clipPath')
.attr('id', d => d.name + '-clip')
.append('circle')
.attr('r', d => nodeRadius * d.mx);

const g = svg.append("g")

const linkG = g
.append('g')
.selectAll('g')
.data(links)
.join('g');

const line = linkG
.append('path')
.attr('id', d => d.index)
.attr('stroke-opacity', 0.6)
.attr('stroke', 'darkgrey')
.attr('stroke-width', '2')
.on('mousemove', linkMouseOver)
.on('mouseout', linkMouseOut );
const node = g
.append("g")
.selectAll("g")
.data(nodes)
.join("g")
.attr("data-name", d => d.id)
.attr("class", d => `img-group ${d.group}`)
.attr("fill", "none")
.call(drag(simulation))
.on('mousemove', nodeMouseOver)
.on('mouseout', nodeMouseOut);

const cir = node
.append('circle')
.attr('r', d => getRadius(d.group))
.attr('stroke', 'darkgrey')
.attr('stroke-width', 5)
.attr('fill', '#fff')

const img = node
.append('image')
.attr('xlink:href', function(d) { return d.image })
.attr('clip-path', d => `url(#${d.group}-clip)`)
.attr('clip-path', d => `url(#${d.group}-clip)`)
.attr('width', d => getRadius(d.group))
.attr('height', d => getRadius(d.group))
.attr('x', d => getRadius(d.group)/2 * -1)
.attr('y', d => getRadius(d.group)/2 * -1)

let tVict = node.filter(d => d.id == 'Development RSP').datum();
let tOrg = node.filter(d => d.id == 'USER').datum();

simulation.on("tick", function() {

tVict.fy = height / 1.75;
tVict.fx = width / 2;
tOrg.fy = height / 1.25;
tOrg.fx = width / 2;

line.attr(
"d",
d => `M ${d.source.x} ${d.source.y} L ${d.target.x} ${d.target.y}`
);
node.attr('transform', d => `translate(${d.x},${d.y})`);
});


invalidation.then(() => simulation.stop());


return svg.node();
}
Insert cell
data = (
{
links: [
{source: "Development RSP", target: "RSC", relationship: "Without RSC blah blah", distance: 100},
{source: "Development RSP", target: "NFS", relationship: "NFS makes your...", distance: 200},
{source: "Development RSP", target: "RSPM", relationship: "RSPM helps....", distance: 200},
{source: "Development RSP", target: "SETTINGS", relationship: "Configure your RSP", distance: 600},
{source: "Development RSP", target: "USER", relationship: "This is how you interact with RSP", distance: 200},
{source: "NFS", target: "SETTINGS", relationship: "Blah", distance: 200},
{source: "RSPM", target: "SETTINGS", relationship: "Blah", distance: 200},
{source: "USER", target: "RSC", relationship: "Blah", distance: 200},
{source: "R", target: "SETTINGS", relationship: "Blah", distance: 3},
{source: "Python", target: "SETTINGS", relationship: "Blah", distance: 3},
{source: "Other", target: "SETTINGS", relationship: "Blah", distance: 3},
],
nodes: [
{image: "https://cdn4.iconfinder.com/data/icons/48-bubbles/48/29.Mac-256.png",
group: "Computer",
id: "Development RSP",
tooltip: "This is something about what the development RSP is",
level: "1",
distance: 200,
},
{
image: "https://cdn4.iconfinder.com/data/icons/ionicons/512/icon-ios7-cloud-upload-outline-256.png",
group: "Package",
id: "RSC",
tooltip: "Here's some info on RSC",
level: "2",
distance: 200,
},
{
image: "https://cdn1.iconfinder.com/data/icons/network-technology-17/64/Files_Sharing-Network-Technology-Document-Folder-256.png",
group: "Package",
id: "NFS",
tooltip: "And now some info on NFS",
level: "2",
distance: 200
},
{
image: "https://cdn0.iconfinder.com/data/icons/set-app-incredibles/24/Configuration-01-256.png",
group: "Package",
id: "SETTINGS",
tooltip: "Settings things?",
level: "2",
distance: 200
},
{
image: "https://cdn4.iconfinder.com/data/icons/48-bubbles/48/30.User-512.png",
group: "User",
id: "USER",
tooltip: "It you!",
level: "2",
distance: 200
},
{
image: "https://cdn0.iconfinder.com/data/icons/logistics-and-delivery-line/48/Computer_with_Box-256.png",
group: "Package",
id: "RSPM",
tooltip: "What is the RStudio Package Manager anyways?",
level: "2",
distance: 200
},
{image: "https://cdn4.iconfinder.com/data/icons/logos-and-brands-1/512/285_R_Project_logo-256.png",
group: "User",
id: "R",
tooltip: "Add R",
level: "1",
distance: 3
},
{image: "https://cdn4.iconfinder.com/data/icons/scripting-and-programming-languages/512/Python_logo-256.png",
group: "User",
id: "Python",
tooltip: "Add Python",
level: "1",
distance: 3
},
{image: "https://cdn0.iconfinder.com/data/icons/nature-life-line-art/128/honeycomb-ol-256.png",
group: "User",
id: "Other",
tooltip: "This is something about what the development RSP is",
level: "1",
distance: 3
},
]
}
)
Insert cell
function nodeMouseOver(event, d){
toolTip.style("left", event.pageX + 18 + "px")
.style("top", event.pageY + 18 + "px")
.style("display", "block")
.html(`<strong>${d.id}:</strong> ${d.tooltip}`);
// Optional cursor change on target
d3.select(event.target).style("cursor", "pointer");
console.log(d3.select(event.target))
// Optional highlight effects on target
d3.select(this).selectAll("circle")
.transition()
.attr('fill', '#80CEC8')
.attr('stroke-width', 5);
}
Insert cell
function linkMouseOver(event, d){
toolTip.style("left", event.pageX + 18 + "px")
.style("top", event.pageY + 18 + "px")
.style("display", "block")
.html(`${d.relationship}`);
// Optional cursor change on target
d3.select(event.target).style("cursor", "pointer");
// Optional highlight effects on target
d3.select(event.target)
.transition()
.style("stroke", "red")
.attr("stroke-width", 3)
}
Insert cell
groupData = [
{ name: 'Computer', mx: 3 },
{ name: 'Package', mx: 2.5 },
{ name: 'User', mx: 1.75 },
]
Insert cell
function nodeMouseOut(event, d){
// Hide tooltip on mouse out
toolTip.style("display", "none"); // Hide toolTip
// Optional cursor change removed
d3.select(event.target).style("cursor", "default");
// Optional highlight removed
d3.select(event.target)
.transition()
.attr('fill', '#fff')
.attr('stroke-width', 5);
}
Insert cell
function linkMouseOut(event, d){
// Hide tooltip on mouse out
toolTip.style("display", "none"); // Hide toolTip
// Optional cursor change removed
d3.select(event.target).style("cursor", "default");
// Optional highlight removed
d3.select(event.target)
.transition()
.style("stroke", "darkgrey")
.attr('stroke-width', '2')
}
Insert cell
toolTip = d3.select("body").append("div").attr("class", "toolTip")
Insert cell
getRadius = group => {
let res;
switch (group) {
case 'Computer':
res = groupData[0].mx;
break;
case 'Package':
res = groupData[1].mx;
break;
case 'User':
res = groupData[2].mx;
break;
}
return res * 17;
}
Insert cell
forceVariables = ({ nodeRadius: 15,
nodeDistance: 100,
chargeStr: -500,
xDenom: 3,
xStr: 0.0,
yDenom: 0.9,
yStr: 0.0
})
Insert cell
height = 800
Insert cell
color = {
const scale = d3.scaleOrdinal(d3.schemeCategory10);
return d => scale(d.group);
}
Insert cell
drag = simulation => {
function dragstarted(event) {
if (!event.active) simulation.alphaTarget(0.3).restart();
event.subject.fx = event.subject.x;
event.subject.fy = event.subject.y;
}
function dragged(event) {
event.subject.fx = event.x;
event.subject.fy = event.y;
}
function dragended(event) {
if (!event.active) simulation.alphaTarget(0);
event.subject.fx = null;
event.subject.fy = null;
}
return d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended);
}
Insert cell
html`
<style>
.toolTip {
position: absolute;
display: none;
min-width: 30px;
max-width: 240px;
border-radius: 4px;
height: auto;
background: rgba(250,250,250, 0.9);
border: 1px solid #DDD;
padding: 4px 8px;
font-size: .85rem;
text-align: left;
z-index: 1000;
}
</style>
`
Insert cell
d3 = require("d3@6")
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more