chart = {
const links = data.links.map(d => Object.create(d));
const nodes = data.nodes.map(d => Object.create(d));
const [width, height] = [1000, 1000];
const svg = d3.select(DOM.svg(width, height));
const aiScoreMinDiff = 0.05;
let linkedByIndex = {};
data.links.forEach(d => {
linkedByIndex[`${d.source},${d.target}`] = true;
});
let nodesById = {};
data.nodes.forEach(d => {
nodesById[d.id] = {...d};
})
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 => 15 * d.support;
const baseGroup = svg.append("g");
function zoomed() {
baseGroup.attr("transform", d3.event.transform);
}
const zoom = d3.zoom()
.scaleExtent([0.2, 8])
//.translateExtent([[-100, -100], [width + 1000, height + 1000]])
.on("zoom", zoomed);
svg.call(zoom);
let ifClicked = false;
const simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(function(d) { return d.id; }).strength(0.3))
.force("charge", d3.forceManyBody())
.force("y", d3.forceY(height/2).strength(0.1))
.force("center", d3.forceCenter(width / 2, height / 2))
.force("x", d3.forceX(d => {
if (d.level === 1) {
return width/7 - 50; // width/5 - 50 - 400;
}
return width/7 * d.level; // width/5 * d.group - 400;
}).strength(0.95))
.force("collide", d3.forceCollide().radius(d => nodeRadius(d) + 1).iterations(2));
// const group = baseGroup.selectAll('.group')
// .data(groups)
// .enter().append('rect')
// .classed('group', true)
// .attr('rx',5)
// .attr('ry',5)
// .call(layout.drag);
// layout.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);
// group
// .attr('x', function (d) { return d.bounds.x })
// .attr('y', function (d) { return d.bounds.y })
// .attr('width', function (d) { return d.bounds.width() })
// .attr('height', function(d) { return d.bounds.height() });
// });
const link = baseGroup.append("g")
.selectAll("line")
.data(links)
.join("line")
.classed('link', true)
.style('stroke', d => {
console.log(d.target)
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", 0.5);
const node = baseGroup.append("g")
.selectAll("circle")
.data(nodes)
.join("circle")
.classed('node', true)
.attr("r", d => nodeRadius(d))
.attr("fill", nodeColor);
// 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 => {
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 => {
console.log(o.source === d)
return (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("stroke-opacity", o => {
console.log(o.value)
});
};
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()'));
};
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 => {
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));
};
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.5)
menuItems.attr('visibility', "hidden");
});
//invalidation.then(() => layout.stop());
return svg.node();
}