chart = {
let links = data.links.map(d => Object.create(d));
let nodes = data.nodes.map(d => Object.create(d));
let linkedByIndex = {};
data.links.forEach(d => {
linkedByIndex[`${d.source},${d.target}`] = true;
});
let nodesById = {};
data.nodes.forEach(d => {
nodesById[d.id] = {...d};
})
const [width, height] = [1000, 1000];
const svg = d3.select(DOM.svg(width, height));
const centerLevel = 4;
const totalLevels = 4;
const aiScoreMinDiff = 0.2;
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;
const isEqual = (a, b) => a === b;
const nodeRadius = (d) => Math.pow(40.0 * d.size, 1 / 3);
const baseGroup = svg.append("g");
function zoomed() {
baseGroup.attr("transform", d3.event.transform);
}
const zoom = d3.zoom()
.scaleExtent([0.2, 8])
//.translateExtent([[-1000, -1000], [width + 1000, height + 1000]])
.on("zoom", zoomed);
svg.call(zoom);
let ifClicked = false;
const nodeRadius2 = d => 15 * d.support;
let simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(function(d) { return d.id; }).strength(0.4))
.force("charge", d3.forceManyBody())
.force("y", d3.forceY(height/2).strength(0.1))
.force("center", d3.forceCenter(width / 2 + 100, height / 2)) // TODO: modify + 100 for center force
.force("x", d3.forceX(d => {
if (d.level === centerLevel) {
return width/2; // width/5 - 50 - 400;
}
return width/2 + width/7 * (d.level - centerLevel); // width/5 * d.group - 400;
}).strength(0.9))
.force("collide", d3.forceCollide().radius(d => nodeRadius2(d) + 0.5).iterations(2));
let link = baseGroup.append("g")
.selectAll(".link")
.data(links)
.enter().append('line')
.attr('class', 'link')
.attr('id', d => d.id)
.style("stroke-width", 0.3)
.style('stroke', d => {
const aiScoreDiff = nodesById[d.target].aiScore - nodesById[d.source].aiScore;
if (Math.abs(aiScoreDiff) < aiScoreMinDiff) {
return '#999';
} else {
if (aiScoreDiff > 0) {
return "#84c942"; // inactive -> active
} else {
return "#e85335"; // active -> inactive
}
}
});
// .style("stroke-opacity", d => {
// const aiScoreDiff = nodesById[d.target].aiScore - nodesById[d.source].aiScore;
// if (Math.abs(aiScoreDiff) < aiScoreMinDiff) {
// return 0.4;
// }
// return 1;
// });
link.exit().remove();
let node = baseGroup.append("g")
.selectAll(".node")
.data(nodes)
.enter().append('circle')
.attr('class', 'node')
.attr("r", d => nodeRadius2(d))
.style("fill", nodeColor);
node.exit().remove();
// edit context menu
const rightClickItems = ['Expand sub-network of the pattern', 'Mark the pattern', 'Mark compound population'];
const menuItems = baseGroup.selectAll(".menuitems")
.data(rightClickItems)
.join('g')
.classed('menuitems', true)
.attr('visibility', "hidden")
.attr('transform', `translate(${0}, ${0})`)
//.on('click', rightClickActions);
menuItems.append('rect')
.attr('x', 0)
.attr('y', (d,i) => i * 20)
.attr('width', 180)
.attr('height', 20);
menuItems.append('text')
.text(d => d)
.attr('x', 3)
.attr('y', (d,i) => 13 + i * 20)
.style('fill', 'black')
.style('font-size', '11px');
function ticked() {
link
.attr("x1", function(d) { return d.source.x; })
.attr("y1", function(d) { return d.source.y; })
.attr("x2", function(d) { return d.target.x; })
.attr("y2", function(d) { return d.target.y; });
node
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
}
simulation
.nodes(nodes)
.on("tick", ticked);
simulation.force("link")
.links(links);
const mouseOverFunction = d => {
console.log(d)
tooltip.style("visibility", "visible")
.html(() => {
const content = `<strong>Pattern:</strong> <span>{${d.id.replace(/-/g, ',')}}</span>`+'<br>'
+`<strong>aiScore:</strong> <span>${d3.format('.2f')(d.aiScore)}</span>`;
return content;
});
if (ifClicked) return;
node
.transition(500)
.style('opacity', o => {
const isConnectedValue = isConnected(o.id, d.id);
if (isConnectedValue) {
return 1.0;
}
return 0.1;
});
link
.transition(500)
.style('stroke-opacity', o => (o.source === d || o.target === d ? 1 : 0.1))
// .transition(500)
// .attr('marker-end', o => (o.source === d || o.target === d ? 'url(#arrowhead)' : 'url()'));
};
const mouseOutFunction = d => {
tooltip.style("visibility", "hidden");
if (ifClicked) return;
node
.transition(500)
.style('opacity', 1);
link
.transition(500)
//.style('opacity', 0.4);
.style("stroke-opacity", o => 0.4);
};
const rightClickActions = (d, menuItem, i) => {
// d is the clicked node, i is the menuItem index
console.log(d.id, menuItem, i)
// Expand sub-network of the patter
if (i === 0) {
// sending d.id (pattern) to backend to get sub-network
};
menuItems.attr('visibility', "hidden");
};
const rightClickFunction = d => {
console.log(d)
d3.event.preventDefault();
// add clicked effect: highlight right-clicked item
ifClicked = true;
node
.transition(500)
.style('opacity', 1)
link
.transition(500);
node
.transition(500)
.style('opacity', o => {
const isConnectedValue = isConnected(o.id, d.id);
if (isConnectedValue) {
return 1.0;
}
return 0.1
})
link
.transition(500)
.style('stroke-opacity', o => (o.source === d || o.target === d ? 1 : 0.1))
.transition(500)
.attr('marker-end', o => (o.source === d || o.target === d ? 'url(#arrowhead)' : 'url()'));
tooltip.style("visibility", "hidden");
const position = {x: d.x, y: d.y};
menuItems.attr('visibility', "visible")
.attr('transform', `translate(${position.x}, ${position.y})`)
.on('click', (menuItem, i) => rightClickActions(d, menuItem, i));
};
const mouseClickFunction = d => {
// we don't want the click event bubble up to svg
d3.event.stopPropagation();
menuItems.attr('visibility', "hidden");
ifClicked = true;
node
.transition(500)
.style('opacity', 1)
link
.transition(500);
node
.transition(500)
.style('opacity', o => {
const isConnectedValue = isConnected(o.id, d.id);
if (isConnectedValue) {
return 1.0;
}
return 0.1
})
link
.transition(500)
.style('stroke-opacity', o => (o.source === d || o.target === d ? 1 : 0.1))
.transition(500)
.attr('marker-end', o => (o.source === d || o.target === d ? 'url(#arrowhead)' : 'url()'));
if (true) {
const newLinks = data1.links.map(d => Object.create(d))
const newNodes = data1.nodes.map(d => Object.create(d))
console.log(newLinks, newNodes)
data1.links.forEach(d => {
linkedByIndex[`${d.source},${d.target}`] = true;
});
data1.nodes.forEach(d => {
nodesById[d.id] = {...d};
})
link = link.data(newLinks)
.join("line")
.classed('link', true)
.attr("stroke-width", 0.3)
.style('stroke', d => {
const aiScoreDiff = nodesById[d.target].aiScore - nodesById[d.source].aiScore;
if (Math.abs(aiScoreDiff) < aiScoreMinDiff) {
return '#999';
} else {
if (aiScoreDiff > 0) {
return "#84c942"; // inactive -> active
} else {
return "#e85335"; // active -> inactive
}
}
});
node = node.data(newNodes)
.join('circle')
.classed('node', true)
.attr("r", d => nodeRadius2(d))
.attr("fill", nodeColor)
.on('mouseover', mouseOverFunction)
.on('mouseout', mouseOutFunction)
.on('click', mouseClickFunction)
.on('contextmenu', rightClickFunction)
.on('mousemove', () => tooltip.style("top", (d3.event.pageY-10)+"px").style("left",(d3.event.pageX+10)+"px"));
simulation
.nodes(newNodes)
.on("tick", ticked);
simulation.force("link")
.links(newLinks);
simulation.alpha(1).restart();
console.log(baseGroup.selectAll('line'))
}
};
node.on('mouseover', mouseOverFunction)
.on('mouseout', mouseOutFunction)
.on('click', mouseClickFunction)
.on('contextmenu', rightClickFunction)
.on('mousemove', () => tooltip.style("top", (d3.event.pageY-10)+"px").style("left",(d3.event.pageX+10)+"px"));
svg.on('click', () => {
ifClicked = false;
node
.transition(500)
.style('opacity', 1);
link
.transition(500)
.style("stroke-opacity", 0.4)
menuItems.attr('visibility', "hidden");
});
//invalidation.then(() => layout.stop());
return svg.node();
}