index.community/frontend/src/components/Cytoscape.tsx

153 lines
4.1 KiB
TypeScript

import cytoscape from "cytoscape";
import popper from "cytoscape-popper";
import * as React from "react";
import ReactDOM from "react-dom";
import styled from "styled-components";
import tippy, { Instance } from "tippy.js";
import { DEFAULT_NODE_COLOR, SELECTED_NODE_COLOR } from "../constants";
const EntireWindowDiv = styled.div`
position: absolute;
top: 50px;
bottom: 0;
right: 0;
left: 0;
`;
interface ICytoscapeProps {
elements: cytoscape.ElementsDefinition;
onInstanceSelect: (domain: string) => void;
onInstanceDeselect: () => void;
}
class Cytoscape extends React.Component<ICytoscapeProps> {
public cy?: cytoscape.Core;
public componentDidMount() {
const container = ReactDOM.findDOMNode(this);
cytoscape.use(popper as any);
this.cy = cytoscape({
autoungrabify: true,
container: container as any,
elements: this.props.elements,
hideEdgesOnViewport: true,
hideLabelsOnViewport: true,
layout: {
name: "preset"
},
maxZoom: 2,
minZoom: 0.03,
pixelRatio: 1.0,
selectionType: "single"
});
// Setup node tooltip on hover
this.cy.nodes().forEach(n => {
const domain = n.data("id");
const ref = (n as any).popperRef();
const t = tippy(ref, {
animateFill: false,
animation: "fade",
content: domain,
duration: 100,
trigger: "manual"
});
n.on("mouseover", e => {
(t as Instance).show();
});
n.on("mouseout", e => {
(t as Instance).hide();
});
});
const style = this.cy.style() as any;
style
.clear()
.selector("node")
.style({
"background-color": DEFAULT_NODE_COLOR,
// The size from the backend is log_10(userCount), which from 10 <= userCount <= 1,000,000 gives us the range
// 1-6. We map this to the range of sizes we want.
// TODO: I should probably check that that the backend is actually using log_10 and not log_e, but it look
// quite good as it is, so...
height: "mapData(size, 1, 6, 20, 200)",
label: "data(id)",
width: "mapData(size, 1, 6, 20, 200)"
})
.selector("node:selected")
.style({
"background-color": SELECTED_NODE_COLOR
})
.selector("edge")
.style({
"curve-style": "haystack", // fast edges
"line-color": DEFAULT_NODE_COLOR,
width: "mapData(weight, 0, 0.5, 1, 20)"
})
.selector("node[label]")
.style({
color: DEFAULT_NODE_COLOR,
"font-size": 50,
"min-zoomed-font-size": 16
})
.selector(".hidden")
.style({
display: "none"
})
.selector(".thickEdge")
.style({
width: 2
})
.update();
this.cy.nodes().on("select", e => {
const instanceId = e.target.data("id");
if (instanceId) {
this.props.onInstanceSelect(instanceId);
}
const neighborhood = this.cy!.$id(instanceId).closedNeighborhood();
// Reset graph visibility
this.cy!.batch(() => {
this.cy!.nodes().removeClass("hidden");
this.cy!.edges().removeClass("thickEdge");
// Then hide everything except neighborhood
this.cy!.nodes()
.diff(neighborhood)
.left.addClass("hidden");
neighborhood.connectedEdges().addClass("thickEdge");
});
});
this.cy.nodes().on("unselect", e => {
this.props.onInstanceDeselect();
this.cy!.batch(() => {
this.cy!.nodes().removeClass("hidden");
this.cy!.edges().removeClass("thickEdge");
});
});
this.cy.on("click", e => {
// Clicking on the background should also deselect
const target = e.target;
if (!target) {
this.props.onInstanceDeselect();
}
this.cy!.batch(() => {
this.cy!.nodes().removeClass("hidden");
this.cy!.edges().removeClass("thickEdge");
});
});
}
public componentWillUnmount() {
if (this.cy) {
this.cy.destroy();
}
}
public render() {
return <EntireWindowDiv />;
}
}
export default Cytoscape;