Published
Edited
May 27, 2021
Insert cell
md`# Force Directed Graph`
Insert cell
chart = {
const mainGraphNodes = data.nodes.map((d) => Object.create(d));
const mainGraphLinks = data.links.map((d) => Object.create(d));
const mainGraphCategories = data.categories.map(d => Object.create(d));
const simulation = d3
.forceSimulation()
.velocityDecay(0.1)
.nodes(mainGraphNodes)
.force('x', d3.forceX(width).strength(0.05))
.force('y', d3.forceY(height).strength(0.05))
.force(
'link',
d3.forceLink(mainGraphLinks).id((d) => d.id),
)
.force('charge', d3.forceManyBody().strength(-240))
.force('center', d3.forceCenter(width / 2, height / 2))
.force('collision', d3.forceCollide().radius(7));
const zoom = d3.zoom().scaleExtent([0.2, 5]);
const svg = d3
.create('svg')
.attr('viewBox', `0 0 ${width} ${height}`)
.attr('preserveAspectRatio', 'xMinYMid')
// .append('g')
// .attr('id', 'zoomController')
.call(zoom)
.call(zoom.transform, d3.zoomIdentity);
// const zoomRect = svg
// .append('rect')
// .attr('width', '100%')
// .attr('height', '100%')
// .style('fill', 'none')
// .style('pointer-events', 'all');
const container = svg.append('g').attr('id', 'graphContainer');
zoom.on('zoom', (event) => {
container.attr('transform', event.transform);
});
svg.append('style').text(`
text.hover {
font-weight: bold;
}
line:hover{
stroke: #000;
stroke-opacity: 1;
}
line.pactive {
stroke: #000;
stroke-opacity: 1;
}
circle.pactive{
stroke: #000;
}
line.gactive {
stroke-opacity: 1;
}
text.gactive{
font-weight: bold;
font-size: 25;
}
.inactive {
fill: #808080;
opacity: 0.4;
}
line.inactive {
stroke-opacity: 0.4;
}
`);
const graphLinks = container
.append('g')
.attr('class', 'links')
.selectAll('line')
.data(mainGraphLinks)
.enter()
.append('line')
.attr('pointer-events', 'visible')
.attr('class', (linkData) => {
return (
'id' +
linkData.source.id +
' id' +
linkData.target.id +
' category' +
linkData.source.categories.map((category) => category.id).join(' category') +
' category' +
linkData.target.categories.map((category) => category.id).join(' category')
);
})
.attr('stroke', '#999')
.attr('stroke-opacity', 0.8)
.attr('stroke-width', '1')
.on('mouseover', (event, linkData) => {
d3.select(event.target).style('stroke', '#4576d1').style('stroke-width', '5');
})
.on('mouseout', (event) => {
d3.select(event.target)
.style('stroke', '#999')
.style('stroke-width', '1');
});

graphLinks
.append('title')
.text((linkData) => linkData.tags.map((tag) => tag.text).join(' '));
const graphNodes = container
.append('g')
.selectAll('.node')
.data(mainGraphNodes)
.join('g')
.attr('class', 'node')
.attr('pointer-events', 'visible');

graphNodes
.on('mouseover', (event, nodeData) => {
d3.select(event.target).style('stroke', '#3f51b5').style('stroke-width', '5');
d3.selectAll(`line.id${nodeData.id}`).style('stroke', '#4576d1').style('stroke-width', '5');
})
.on('mouseout', (event, nodeData) => {
d3.select(event.target).style('stroke', '#666').style('stroke-width', '1');
d3.selectAll(`line.id${nodeData.id}`)
.style('stroke', '#999')
.style('stroke-width', '1');
})
graphNodes.call(drag(simulation));
const categoryArc = d3.arc().innerRadius(5).outerRadius(7);

graphNodes
.append('circle')
.attr('r', 5)
.attr('class', (nodeData) => {
return (
'id' +
nodeData.id +
' category' +
nodeData.categories.map((category) => category.id).join(' category')
);
})
.attr('fill', '#3b3939')
.each((nodeData, index) => {
const pieData = [];
nodeData.categories.forEach((element) => {
pieData.push({
category: element.id,
categoryName: element.text,
color: element.color,
value: 1 / nodeData.categories.length,
});
});
const GlobalVariables = Object.freeze({
pie: d3.pie().value(data => data.value),
});
const arcData = GlobalVariables.pie(pieData);
const selectedNode = graphNodes.filter((element, id) => {
return id === index;
});
selectedNode
.selectAll()
.data(arcData)
.enter()
.append('path')
.attr('fill', (category) => category.data.color)
.attr('d', categoryArc)
.attr('class', (category) => `category${category.data.category} arcs`)
.append('title')
.text((category) => category.data.categoryName);
});
graphNodes.append('title').text((nodeData) => nodeData.data_entity_text);
simulation.on('tick', () => {
graphLinks
.attr('x1', (linkData) => linkData.source.x)
.attr('y1', (linkData) => linkData.source.y)
.attr('x2', (linkData) => linkData.target.x)
.attr('y2', (linkData) => linkData.target.y);

graphNodes.attr('transform', (nodeData) => `translate(${nodeData.x}, ${nodeData.y})`);
});
let legendState = new Map();
mainGraphCategories.forEach((category) => {
legendState.set(category.id, false);
});
svg
.append('g')
.attr('fill', 'none')
.attr('pointer-events', 'all')
.attr('font-family', 'sans-serif')
.attr('font-size', 15)
.attr('text-anchor', 'start')
.attr('class', 'categoryLegend')
.attr('opacity', 1)
.selectAll('g')
.data(mainGraphCategories)
.join('g')
.attr(
'transform',
(category, index) =>
`translate(${15},${index * (15 + 5) + 10})`,
)
.call((g) =>
g
.append('text')
.attr('x', 6)
.attr('y', '0.35em')
.attr('class', (category) => 'category' + category.id)
.attr('fill', (category) => (category.color ? category.color : '#c3c3c3'))
.text((category) => category.text),
)
.call((g) =>
g
.append('circle')
.attr('r', 5)
.attr('class', (category) => 'category' + category.id + ' legendNodes')
.attr('fill', (category) => (category.color ? category.color : '#c3c3c3')),
)
.on('mouseover', (event, category) => {
d3.selectAll(`text.${'category' + category.id}`).classed('hover', true);
})
.on('mouseout', (event, category) => {
d3.selectAll(`text.${'category' + category.id}`).classed('hover', false);
})
.on('click', (event, category) => {
selectGroup(category.id, 'category', legendState);
});
invalidation.then(() => simulation.stop());
return svg.node();
}
Insert cell
function selectGroup(
categoryID,
classSuffix,
legendState,
) {
let hasTrue = false;
if (legendState.get(categoryID) === true) {
legendState.set(categoryID, false);
} else if (legendState.get(categoryID) === false) {
legendState.set(categoryID, true);
}
d3.selectAll(`line`).classed('inactive', true);
d3.selectAll(`path`).classed('inactive', true);
d3.selectAll(`circle`).classed('inactive', true);
legendState.forEach((category, key) => {
if (category === true) {
hasTrue = true;
}
});
if (hasTrue) {
legendState.forEach((element, key) => {
if (element) {
d3.selectAll(`.${classSuffix + key}`).classed('gactive', true);
d3.selectAll(`.${classSuffix + key}`).classed('inactive', false);
} else {
d3.selectAll(`.${classSuffix + key}`).classed('gactive', false);
}
});
} else {
legendState.forEach((element, key) => {
d3.selectAll(`.${classSuffix + key}`).classed('gactive', false);
d3.selectAll(`.${classSuffix + key}`).classed('inactive', false);
});
d3.selectAll(`line`).classed('inactive', false);
d3.selectAll(`path`).classed('inactive', false);
d3.selectAll(`circle`).classed('inactive', false);
}
}
Insert cell
data = FileAttachment("KnowledgeGraph.json").json()
Insert cell
height = 800
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
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