Public
Edited
Sep 27, 2023
1 fork
10 stars
Insert cell
Insert cell
dataOrig = fetch("https://reactome.org/download/current/diagram/187037.json").then((response) => response.json())
Insert cell
Insert cell
function getNodesAndEdges(edges) {
const reactionNodes = []
const reactionEdges = []
edges.forEach((edge)=>
{
// the "edge" array contains reactions, a reaction is a node connected with the educts and products
// a graph node is of the form:
// {id: id, schemaClass: see reactom scheme (and reactionNode), position: tuple of x- and y- coords}
reactionNodes.push({id: edge.id, schemaClass: "reactionNode", position: edge.position})

// an edge is of the follwing shape: {source: ID-source, target: ID-target, type: Type of edge}
edge.inputs?.forEach((input)=>{
reactionEdges.push({source: input.id, target: edge.id, type: "input"})
})
edge.outputs?.forEach((output)=>{
reactionEdges.push({source: edge.id, target: output.id, type: "output"})
})
edge.catalysts?.forEach((catalyst)=>{
reactionEdges.push({source: catalyst.id, target: edge.id, type: "catalysts"})
})
}
)
// besides the edges links also contains edges to be drawn in the node link diagram
const links = dataOrig.links.map((link) => {
return {source: link.inputs[0].id, target: link.outputs[0].id, type: "link"}
})
return {nodes: [...reactionNodes,...dataOrig.nodes], edges: [...reactionEdges, ...links] };
}
Insert cell
plotData = {
const {nodes, edges} = getNodesAndEdges(dataOrig.edges, dataOrig.links)
return {nodes: nodes, links: edges}
}
Insert cell
Insert cell
schemaClasses = {

let schemaTemp = []

dataOrig.nodes.forEach((node) => schemaTemp.push(node.schemaClass))

return schemaTemp
}
// TODO extract schema classes
Insert cell
Insert cell
colorScaleNodes = d3.scaleOrdinal(d3.schemePastel2).domain(schemaClasses)
//TODO generate correct colorscale
Insert cell
Insert cell
Insert cell
// based on example....
chart = {
// Specify the dimensions of the chart.
const width = 800;
const height = 800;
const radius = 5

// The force simulation mutates links and nodes, so create a copy
// so that re-evaluating this cell produces the same result.
const links = structuredClone(plotData.links);
const nodes = structuredClone(plotData.nodes);

// Create a simulation with several forces.
const simulation = d3.forceSimulation(nodes).alphaDecay(0.0001)
.force('boxing', boxingForce)
.force("link", d3.forceLink(links).id(d => d.id).strength(1.5))
.force("charge", d3.forceManyBody().strength(()=>-15).distanceMax(75))
.force("center", d3.forceCenter(width / 2, height / 2))
.force('collision',d3.forceCollide().radius((d) => radius))
.on("tick", ticked);

function blockNodeCoordinates(
radius,
node,
svgSize
) {
// This code is for keeping the nodes within the svg canvas.
// The node.x and node.y variables are set to be within the bounds of the svg canvas +/- the radius of the node.

node.x = Math.max(radius, Math.min(svgSize.width - radius, node.x));
node.y = Math.max(radius, Math.min(svgSize.height - radius, node.y));
return node;
}
function boxingForce() {
nodes.forEach((node) => {
node = blockNodeCoordinates(2*radius, node, {width: width, height: height});
});
}
// Create the SVG container.
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [0, 0, width, height])
.attr("style", "width:auto; max-width:100%; background-color: #f0f0f0");

// Add a line for each link, and a circle for each node.
console.log('links', links)
const link = svg.append("g")
.attr("stroke", "#555")
.selectAll()
.data(links)
.join("line")

const node = svg.append("g")
.attr("stroke", "#000")
.attr("stroke-width", 1)
.selectAll()
.data(nodes)
.join("circle")
.attr("r", radius)
.attr("fill", d => colorScaleNodes(d.schemaClass));

node.append("title")
.text(d => d.displayName);

// Add a drag behavior.
node.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));

// Set the position attributes of links and nodes each time the simulation ticks.
function ticked() {
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);
}

// Reheat the simulation when drag starts, and fix the subject position.
function dragstarted(event) {
if (!event.active) simulation.alphaTarget(0.3).restart();
event.subject.fx = event.subject.x;
event.subject.fy = event.subject.y;
}

// Update the subject (dragged node) position during drag.
function dragged(event) {
event.subject.fx = event.x;
event.subject.fy = event.y;
}

// Restore the target alpha so the simulation cools after dragging ends.
// Unfix the subject position now that it’s no longer being dragged.
function dragended(event) {
if (!event.active) simulation.alphaTarget(0);
event.subject.fx = null;
event.subject.fy = null;
}

// When this cell is re-run, stop the previous simulation. (This doesn’t
// really matter since the target alpha is zero and the simulation will
// stop naturally, but it’s a good practice.)
invalidation.then(() => simulation.stop());

return svg.node();
}
Insert cell
### Manually curated layout from Reactome

While the force layout generated previously is interesting, as it is generated fully automatically it is of limited usefulness, due to the unfamiliarity to the viewer, and the lack of semantic meaning.
Thankfully, the reactome data contains manually curated layouts and positions
Insert cell
Insert cell
reactomeLayoutChart = {
// Specify the dimensions of the chart.
const width = 3500;
const height = 1653;
const radius = 10

// Create the SVG container.
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [0, 0, width, height])
.attr("style", "height: auto; width:auto; max-width:100%; background-color: #f0f0f0");

// Add a line for each link, and a circle for each node.
const link = svg.append("g")
.attr("stroke", "#555")
.selectAll()
.data(plotData.links)
.join("line")
.attr("x1", d=> plotData.nodes.find((node) => node.id == d.source)?.position.x)
.attr("x2", d=> plotData.nodes.find((node) => node.id == d.target)?.position.x)
.attr("y1", d=> plotData.nodes.find((node) => node.id == d.source)?.position.y)
.attr("y2", d=> plotData.nodes.find((node) => node.id == d.target)?.position.y)

const node = svg.append("g")
.attr("stroke", "#000")
.attr("stroke-width", 1)
.selectAll()
.data(plotData.nodes)
.join("circle")
.attr("r", radius)
.attr("fill", d => colorScaleNodes(d.schemaClass))
.attr("cx", d=>d.position.x)
.attr("cy", d=>d.position.y)

node.append("title")
.text(d => d.displayName);


return svg.node();
}
Insert cell
Insert cell
Insert cell
// as the nodes are stored in an array, finding a specific node id is very tedious. Thus, we generate a map (of key:value pairs) in order to link the id of a entitiy with the index in the node array
function mapNodeId2Idx(nodes){
const map = new Map();
nodes.forEach( (value, index) => map.set(value.id, parseInt(index)));
return map;
}
Insert cell
Insert cell
adjacenyMatrix = {
const width = 1000;
const height = 1000;
const margin = {top: 50, right: 0, bottom: 0, left: 50};
const svg = d3.create("svg")
.attr("width", width+margin.left+margin.right)
.attr("height", height+margin.top+margin.bottom)
.attr("style", "background-color: white");
// this generates a bandscale mapping indices to width positions
let x = d3.scaleBand()
.range([0, width])
.paddingInner(.2)
.align(0)
.domain(d3.range(plotData.nodes.length));
// this generates a mapping from node to index
let nodeIdMap = mapNodeId2Idx(plotData.nodes);
let matrix = [] //todo fill the matrix such that no adjacency: 0, same node (diagonal) 1, adjacency: 2
let matrixGroup = svg.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");

//this function appends a group for each row and translates it the correct amount in y-direction, using each allows us to execute a function for each row in our matrix
let row = matrixGroup.selectAll('g.row')
.data(matrix)
.enter().append('g')
.attr('class', 'row')
.attr('transform', function (d, i) { return 'translate(0,' + x(i) + ')'; })
.each(makeRow);

function makeRow(rowData) {
let cell = d3.select(this).selectAll('rect.cell')
//todo draw the cells
}
//todo: add text to the axes
return svg.node()
}

Insert cell
Insert cell
import {Swatches} from "@d3/color-legend"
Insert cell
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