Published
Edited
Oct 7, 2020
4 forks
4 stars
Insert cell
Insert cell
chart = {
const links = data.links.map(d => Object.create(d));
const nodes = data.nodes.map(d => Object.create(d));

var connected = [];

const simulation = d3
.forceSimulation(nodes)
.force(
"link",
d3
.forceLink(links)
.id(d => d.id)
.distance(100)
)
.force(
"collide",
d3
.forceCollide()
.strength(0.5)
.radius(50)
.iterations(90)
)
.force(
"center",
d3
.forceCenter()
.x(0.5)
.y(0.5)
);

const svg = d3
.create("svg")
.attr("viewBox", [-width / 2, -height / 2, width, height]);

////////////////////////////////////////////////////////////////////////////////
// GRAPH
////////////////////////////////////////////////////////////////////////////////
// Rendering functions
var reset_node_opacity = function(d) {
return 1;
};
var reset_link_opacity = function(d) {
return 0.4;
};

const link = svg
.append("g")
.selectAll("line")
.data(links)
.join("line")
.attr("value", d => d.value)
.attr("target", d => d.target.id)
.attr("source", d => d.source.id)
.attr("stroke", '#000')
.attr("stroke-width", d => link_width(d.value))
.attr("stroke-opacity", d => reset_link_opacity(d));

const node = svg
.append("g")
.attr("stroke", "#fff")
.attr("stroke-width", 1.5)
.selectAll("circle")
.data(nodes)
.join("circle")
.style("cursor", "pointer")
.attr("id", d => d.id)
.attr("r", d => d.radius)
.attr("fill", color)
.call(drag(simulation));

const texts_widgets = svg
.selectAll(".id")
.data(nodes)
.enter()
.filter(d => d.group == 'widget')
.append("text")
.attr("class", "labels")
.attr("font-family", "bebas neue")
.attr("font-size", 14)
.attr("dx", 15)
.attr("dy", "0.35em")
.style('fill', '#1f77b4')
.style("cursor", "pointer")
.attr("id", d => d.id)
.text(d => d.id)
.call(drag(simulation));

const texts_insights = svg
.selectAll(".id")
.data(nodes)
.enter()
.filter(d => d.group == 'insight')
.append("text")
.attr("class", "labels")
.attr("font-family", "bebas neue")
.attr("font-size", 18)
.attr("dx", 23)
.attr("dy", "0.35em")
.style('fill', '#ff7f0e')
.style("cursor", "pointer")
.attr("id", d => d.id)
.text(d => d.id)
.call(drag(simulation));

/////////////////////////////////////////////////////////////////////////////////////
///// UX FUNCTIONS
/////////////////////////////////////////////////////////////////////////////////////

const reset_all_opacity = function() {
d3.selectAll('circle').attr('opacity', d => reset_node_opacity(d));
d3.selectAll('line').attr('stroke-opacity', d => reset_link_opacity(d));
d3.selectAll('.labels').style('opacity', 1);
};

const hide_all_light = function() {
d3.selectAll('circle').attr('opacity', 0.08);
d3.selectAll('line').attr('stroke-opacity', 0.03);
d3.selectAll('.labels').style('opacity', 0.03);
};

var restore_default_display = function() {
d3.selectAll('.legend_session').attr('active', 'no');
d3.selectAll('.legend_channel').attr('active', 'no');
reset_all_opacity();
};

/////////////////////////////////////////////////////////////////////////////////////
///// INTERACTIONS
/////////////////////////////////////////////////////////////////////////////////////

var selection = null;
const selected = d => selection === d;
var neighbors_nodes_id = [];

texts_insights.on('click', (e, d) => {
onClickHandler(e, "#de700d");
});

texts_widgets.on('click', (e, d) => {
onClickHandler(e, "#185e8f");
});

node.on('click', (e, d) => {
var nodecolor;
if (e.group === "insight") nodecolor = "#de700d";
else nodecolor = "#185e8f";
onClickHandler(e, nodecolor);
});

function onClickHandler(e, nodecolor) {
if (selection == null) {
selection = e.id;
d3.selectAll('circle[id="' + e.id + '"]')
.attr("stroke", nodecolor)
.attr("stroke-width", 5);
onMouseOver(e);
} else if (selection == e.id) {
d3.selectAll('circle[id="' + selection + '"]').attr("stroke-width", 0);
selection = null;
onMouseOut(e);
} else if (neighbors_nodes_id.includes(e.id)) {
d3.selectAll('circle[id="' + selection + '"]').attr("stroke-width", 0);
selection = e.id;
d3.selectAll('circle[id="' + e.id + '"]')
.attr("stroke", nodecolor)
.attr("stroke-width", 5);
onMouseOver(e);
}
}

node.on('mouseover', d => {
if (!selection) {
onMouseOver(d);
} else if (neighbors_nodes_id.includes(d.id)) {
onMouseOverNeighbors(d);
}
});

var last_targeted_node;
function onMouseOver(d) {
last_targeted_node = d;
neighbors_nodes_id = [];
// the hover interaction is not active if a node is selected
hide_all_light();

// Higlight basic links
d3.selectAll('circle[id="' + d.id + '"]').attr('opacity', 1);

var links_src = d3.selectAll('line[source="' + d.id + '"]');
var links_tar = d3.selectAll('line[target="' + d.id + '"]');
links_src.attr('stroke-opacity', dd => reset_link_opacity(dd));
links_tar.attr('stroke-opacity', dd => reset_link_opacity(dd));

links_src.each(e => {
neighbors_nodes_id.push(e.target.id);
d3.selectAll('circle[id="' + e.target.id + '"]').attr(
'opacity',
reset_node_opacity(e.target)
);
});
links_tar.each(e => {
neighbors_nodes_id.push(e.source.id);
d3.selectAll('circle[id="' + e.source.id + '"]').attr(
'opacity',
reset_node_opacity(e.source)
);
});

// Highlight selected node label
texts_widgets.filter(w => w.id === d.id).style('opacity', 1);
texts_insights.filter(i => i.id === d.id).style('opacity', 1);

// Highlight neighbors labels
neighbors_nodes_id.forEach(e =>
texts_widgets.filter(w => w.id === e).style('opacity', 1)
);
neighbors_nodes_id.forEach(e =>
texts_insights.filter(w => w.id === e).style('opacity', 1)
);
}

function onMouseOverNeighbors(d) {
var temp_neighbors = [];
// the hover interaction is not active if a node is selected
// Higlight basic links
d3.selectAll('circle[id="' + d.id + '"]').attr('opacity', 1);

var links_src = d3.selectAll('line[source="' + d.id + '"]');
var links_tar = d3.selectAll('line[target="' + d.id + '"]');
links_src.attr('stroke-opacity', dd => reset_link_opacity(dd));
links_tar.attr('stroke-opacity', dd => reset_link_opacity(dd));

links_src.each(e => {
temp_neighbors.push(e.target.id);
d3.selectAll('circle[id="' + e.target.id + '"]').attr(
'opacity',
reset_node_opacity(e.target)
);
});
links_tar.each(e => {
temp_neighbors.push(e.source.id);
d3.selectAll('circle[id="' + e.source.id + '"]').attr(
'opacity',
reset_node_opacity(e.source)
);
});

// Highlight selected node label
texts_widgets.filter(w => w.id === d.id).style('opacity', 1);
texts_insights.filter(i => i.id === d.id).style('opacity', 1);

// Highlight neighbors labels
temp_neighbors.forEach(e =>
texts_widgets.filter(w => w.id === e).style('opacity', 1)
);
temp_neighbors.forEach(e =>
texts_insights.filter(w => w.id === e).style('opacity', 1)
);
}

node.on('mouseout', d => {
onMouseOut(d);
});

function onMouseOut(d) {
if (!selection) {
restore_default_display();
} else if (neighbors_nodes_id.includes(d.id)) {
hide_all_light();
onMouseOver(last_targeted_node);
}
}

simulation.on("tick", () => {
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("cx", d => d.x).attr("cy", d => d.y);

texts_widgets.attr("x", d => d.x).attr("y", d => d.y);

texts_insights.attr("x", d => d.x).attr("y", d => d.y);
});

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

return svg.node();
}
Insert cell
data = FileAttachment("D3links.json").json()
Insert cell
Insert cell
color = {
const scale = d3.scaleOrdinal(d3.schemeCategory10);
return d => scale(d.group);
}
Insert cell
Insert cell
drag = simulation => {
function dragstarted(d) {
if (!d3.event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
function dragended(d) {
if (!d3.event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
return d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended);
}
Insert cell
d3 = require("d3@5")
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