
webapp.js.d3-graph-visualizer.js Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of corese-server Show documentation
Show all versions of corese-server Show documentation
Corese is a Semantic Web Factory (triple store and SPARQL endpoint) implementing RDF, RDFS, SPARQL 1.1
Query and Update.
The newest version!
import {GraphModel} from "./GraphModel.js";
import {Observer} from "./Observer.mjs";
import {ConfGraphModal} from "./ConfGraphModal.mjs";
import {OntologyDrawer} from "./OntologyDrawer.js";
import {TagCloudDrawer} from "./TagCloudDrawer.js";
import {SelectDrawer} from "./SelectDrawer.js";
export * from "./TagCloudDrawer.js";
import {ContextMenu} from "./ContextMenu.mjs";
export class D3GraphVisualizer extends Observer {
constructor(data, prefix) {
super();
this.prefix = prefix;
this.model = new GraphModel(data, prefix);
// this.model.displayAllEdgeLabels = false;
// this.model.displayAllNodeLabels = false;
this.model.addObserver(this);
var sheet = document.createElement('style');
document.head.appendChild(sheet); // Bug : does not support multiple graphs in the same page.
}
update(observable, data) {
super.update(observable, data);
this.graph.ticked();
}
dragstarted(_simulation) {
return function (d) {
if (!d3.event.active) _simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
}
dragged(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
dragended(_simulation) {
return function (d) {
if (!d3.event.active) _simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
}
// To be used with text for edges, in order to obtain text no upside-down.
buildPathFromEdge(scale, svgId, model) {
return links => {
return (edge, i, edges) => {
var dx = edge.source.x - edge.target.x;
var dy = edge.source.y - edge.target.y;
var r = 10 * Math.sqrt(dx * dx + dy * dy);
let dr = 0;
if (model.getOption(svgId + model.ARROW_STYLE) === "curve") {
let r = 10 * Math.sqrt(dx * dx + dy * dy);
dr = r / (2 * edge.linknum);
} else {
dr = 0;
}
var lefterpoint, righterpoint;
var sourceLeft = edge.source.x <= edge.target.x;
[lefterpoint, righterpoint] = (sourceLeft) ? [edge.source, edge.target] : [edge.target, edge.source];
var leftx = lefterpoint.x * scale;
var lefty = lefterpoint.y * scale;
var rightx = righterpoint.x * scale;
var righty = righterpoint.y * scale;
var sweep = (sourceLeft) ? "1" : "0";
return `M ${leftx},${lefty} A ${dr * scale},${dr * scale} 0 0,${sweep} ${rightx},${righty}`
};
}
}
/**
* \param _results : json representation of the graph.
* \param svgId : id of the svg element to draw the graph (do not forget the #).
*/
static drawRdf(_results, svgId) {
var results = _results;
var visualizer = new D3GraphVisualizer(_results, svgId);
var confGraphModal;
visualizer.graph = d3.select(svgId);
const stylesheet = d3.select("style");
if (_results.style !== undefined) {
stylesheet.html(_results.style)
}
// @Todo make a function counting the edges between same nodes.
// To be able to detect edges between same nodes.
results.edges.sort(function (a, b) {
if (a.source > b.source) {
return 1;
} else if (a.source < b.source) {
return -1;
} else {
if (a.target > b.target) {
return 1;
}
if (a.target < b.target) {
return -1;
} else {
return 0;
}
}
});
// counting the edges between the same edges.
for (var i = 0; i < results.edges.length; i++) {
if (i !== 0 &&
results.edges[i].source === results.edges[i - 1].source &&
results.edges[i].target === results.edges[i - 1].target) {
results.edges[i].linknum = results.edges[i - 1].linknum + 1;
} else {
results.edges[i].linknum = 1;
}
;
}
;
var scale = 1;
var fo = visualizer.graph.append('foreignObject').attr("width", "40px").attr("height", "34px");
var button = fo.append("xhtml:button")
.attr("class", "btn btn-info")
.attr("id", "configurationButton")
.on("click", e => {
if (confGraphModal.isDisplayOn()) {
confGraphModal.displayOff()
} else {
confGraphModal.displayOn();
}
});
button.append("xhtml:span")
.attr("class", "glyphicon glyphicon-cog");
results.links = results.edges;
var rootConfPanel = d3.select(d3.select(svgId).node().parentNode, visualizer.graph);
let firstTicked = true;
visualizer.graph.ticked = function (s) {
scale = (s === undefined) ? scale : s;
links.attr("d",
function (model) {
return function (edge, i, edges) {
let dx = edge.target.x - edge.source.x;
let dy = edge.target.y - edge.source.y;
let pathLength = Math.sqrt((dx * dx) + (dy * dy));
let offsetTargetX = 0;
let offsetTargetY = 0;
if (pathLength !== 0) {
offsetTargetX = (dx * edge.target.r) / pathLength;
offsetTargetY = (dy * edge.target.r) / pathLength;
}
let offsetSourceX = 0;
let offsetSourceY = 0;
if (pathLength !== 0) {
offsetSourceX = (dx * edge.source.r) / pathLength;
offsetSourceY = (dy * edge.source.r) / pathLength;
}
let dr = 0;
if (model.getOption(svgId + model.ARROW_STYLE) === "curve") {
let r = 10 * Math.sqrt(dx * dx + dy * dy);
dr = r / (2 * edge.linknum);
} else {
dr = 0;
}
d3.select(this).attr("marker-end", "url(#arrowhead)");
return `M ${edge.source.x * scale + offsetSourceX},${edge.source.y * scale + offsetSourceY} A ${dr * scale},${dr * scale} 0 0,1 ${edge.target.x * scale - offsetTargetX},${edge.target.y * scale - offsetTargetY}`
}
}(visualizer.model)
);
nodes
.attr("cx", function (d) {
return d.x * scale;
})
.attr("cy", function (d) {
return d.y * scale;
})
.each(
(d, i, nodes) => {
var current = d3.select(nodes[i]);
var father = d3.select(nodes[i].parentNode);
var image = father.select("image");
if (image !== undefined) {
var width = current.attr("r") * Math.sqrt(2);
image.attr("x", d => (d.x * scale - width / 2));
image.attr("y", d => (d.y * scale - width / 2));
}
}
);
if (true) { // @TODO : à réécrire pour parcourir les groupes à afficher.
textNodes
.attr("x",
(d) => {
const r = Number(d3.select(d.node).attr("r"));
const result = d.x * scale + r;
return result;
}
)
.attr("y",
(d) => {
const r = Number(d3.select(d.node).attr("r"));
return (d.y * scale + r);
}
).attr("visibility",
(d) => confGraphModal.model.getDisplayGroup(confGraphModal.model.ALL_NODES, d.group) ? "visible" : "hidden"
);
}
if (true) {// @TODO : à réécrire pour parcourir les groupes à afficher.
textEdges
.attr("visibility",
(d) => confGraphModal.model.getDisplayGroup(confGraphModal.model.ALL_EDGES, d.group) ? "visible" : "hidden"
);
pathLabels
.attr("d", visualizer.buildPathFromEdge(scale, svgId, confGraphModal.model)(results.links));
}
if (firstTicked) {
// Search for the first container of visualizer.graph with a non empty size (Important remark: an element in a not visible html element
// returns a null size, so this hack is required.
let currentNode = visualizer.graph.node();
let bboxView;
while (currentNode !== undefined) {
bboxView = currentNode.getBoundingClientRect();
if (bboxView.width === 0 && bboxView.height === 0) {
currentNode = currentNode.parentNode;
continue;
} else {
break;
}
}
if (currentNode !== undefined && bboxView.width !== 0 && bboxView.height !== 0) {
const tx = bboxView.width / 2;
const ty = bboxView.height / 2;
let zoomed = function () {
g.attr("transform", d3.event.transform);
};
visualizer.graph.call(d3.zoom().on("zoom", zoomed).transform, d3.zoomIdentity.translate(tx,ty));
firstTicked = false;
}
}
};
confGraphModal = ConfGraphModal.createConfigurationPanel(rootConfPanel, visualizer.graph, results, visualizer.model);
confGraphModal.model.addObserver(confGraphModal);
visualizer.graph.updateConfiguration = function () {
var updateSet = function (groupName, text) {
const nodesDisplayCriteria = (d, i, nodes) => (confGraphModal.model.getDisplayGroup(confGraphModal.model.ALL_NODES, d.group)) ? "visible" : "hidden";
text.attr(
"visibility",
(d, i, nodes) => nodesDisplayCriteria(d, i, nodes)
);
}
updateSet("nodes", textNodes);
updateSet("edges", textEdges);
};
var maxLen = [];
results.nodes.forEach(
(node, index, array) => {
maxLen[node.id] = 0;
}
);
results.links.forEach(
(link, index, array) => {
maxLen[link.source] = Math.max(maxLen[link.source], link.label.length);
maxLen[link.target] = Math.max(maxLen[link.target], link.label.length);
}
);
var color = d3.scaleOrdinal(d3.schemeCategory20);
var g = visualizer.graph.append("g")
.attr("class", "everything");
let nodes = g.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(results.nodes)
.enter()
.append("g")
.attr("class", "nodes")
.append("circle");
nodes.each(
(d, i, nodes) => {
var current = d3.select(nodes[i]);
if (d.bg_image === undefined) {
current.attr("r", 5);
} else {
current.attr("r", visualizer.model.nodeRadius);
}
d.r = current.attr("r");
}
)
visualizer.simulation = d3.forceSimulation(results.nodes)
.force("link", d3.forceLink().id(function (d) {
return d.id;
}))
.force("collide", d3.forceCollide().radius(function (d, i, nodes) {
return d.r * 8; // 2
}
).iterations(2))
.on("tick", visualizer.graph.ticked);
visualizer.simulation
.force("link")
.links(results.links);
visualizer.simulation.force("center", d3.forceCenter(0,0));
var defs = visualizer.graph.append("defs");
defs.append('marker')
.attr('id', 'arrowhead')
.attr('viewBox', '-0 -50 100 100')
.attr('refX', 130)
.attr('refY', 0)
.attr('orient', 'auto')
.attr('markerWidth', 10)
.attr('markerHeight', 10)
.attr('xoverflow', 'visible')
.attr('markerUnits', 'userSpaceOnUse')
.append('svg:path')
.attr('d', 'M 0,-20 L 100 ,0 L 0,20') // -20
// .style('stroke','grey')
.style('markerUnits', 'userSpaceOnUse')
.style('fill', 'grey')
;
let textNodes = g.append("g").attr("class", "textNodes").selectAll("text")
.data(visualizer.simulation.nodes())
.enter().append("text")
.attr("class", (edge, i, edges) => {
return (edge.class !== undefined) ? edge.class : "default";
})
.text(function (d) {
return d.label;
})
.each(
(d, i, nodes) => {
d.textNode = nodes[i];
}
);
var textEdges = g.append("g").attr("class", "textPaths").selectAll("text")
.data(results.links)
.enter().append("text")
.append("textPath")
.attr("xlink:href", d => `#${visualizer.prefix}${d.id}_path`)
.attr("startOffset", "25%")
.text(d => d.label)
.attr("class", (edge, i, edges) => (edge.class !== undefined) ? edge.class : "default")
.each((d, i, nodes) => d.textEdges = nodes[i]);
var links = g.append("g")
.attr("class", "links")
.selectAll("path")
.data(results.links)
.enter().append("path")
.attr(
"class",
d => {
if (d.class !== undefined) {
return d.class;
} else {
return "default";
}
}
)
.attr("id", d => `${visualizer.prefix}${d.id}_edge`)
.each(
(d, i, nodes) =>
d.link = nodes[i]
);
links.append("title")
.text(function (d) {
return d.label;
});
nodes.attr(
"class",
d => {
if (d.class !== undefined) {
return d.class;
} else {
return "default";
}
}
)
.attr("r", visualizer.model.nodeRadius)
.each(
(d, i, nodes) => {
var current = d3.select(nodes[i]);
var father = d3.select(current.node().parentNode);
var color = d3.scaleOrdinal(d3.schemeCategory20).domain(Array.from(confGraphModal.model.getGroups("nodes")));
d.colorMap = color;
if (d.bg_image === undefined) {
current.attr("fill", color(d.group));
current.attr("r", 5);
} else {
current.attr("r", visualizer.model.nodeRadius);
var width = Math.sqrt(2) * current.attr("r");
father.append("image")
.attr("xlink:href", d.bg_image)
.attr("height", width)
.attr("width", width)
.attr("pointer-events", "none");
}
d.r = current.attr("r");
}
)
.each(
(d, i, nodes) =>
d.node = nodes[i]
)
.each(
(_links => {
return (d, i, nodes) => {
var neighbors = new Set();
_links.forEach(link => {
if (link.source.id === d.id || link.target.id == d.id) {
neighbors.add(link.source.id);
neighbors.add(link.target.id);
}
});
d.neighbors = neighbors;
}
})(results.links)
)
.call(d3.drag()
.on("start", visualizer.dragstarted(visualizer.simulation))
.on("drag", visualizer.dragged)
.on("end", visualizer.dragended(visualizer.simulation)))
.on("click", (d) => {
if (d.url !== undefined) window.open(d.url);
if (d.link !== undefined) trans(d.link);
})
.on("mouseover", (d, i, nodes) => {
var center = d3.select(nodes[i]);
var datum = center.datum();
var counter = 0;
nodes.forEach(node => {
if (datum.neighbors.has(d3.select(node).datum().id)) {
d3.select(node).attr("fill", "red");
d3.select(node).attr("background-color", "yellow");
d3.select(textNodes.nodes()[counter]).attr("visibility", "visible");
}
counter++;
})
})
.on("mouseout", (d, i, nodes) => {
var center = d3.select(nodes[i]);
var datum = center.datum();
var counter = 0;
nodes.forEach(node => {
var datumNode = d3.select(node).datum();
if (datum.neighbors.has(datumNode.id)) {
d3.select(node).attr("fill", datumNode.colorMap(datumNode.group));
d3.select(node).attr("background-color", "white");
d3.select(textNodes.nodes()[counter]).attr("visibility", "hidden");
}
counter++;
})
visualizer.graph.updateConfiguration();
visualizer.graph.ticked();
});
nodes.append("title")
.text(function (d) {
return d.label;
});
visualizer.graph.updateConfiguration();
var pathLabels = visualizer.graph.append("defs").attr("class", "paths").selectAll("path")
.data(results.links)
.enter().append("path")
.attr("id",
function (prefix) {
return (edge, i, edges) => `${prefix}${edge.id}_path`
}(visualizer.model.prefix))
.attr("d", visualizer.buildPathFromEdge(1, svgId, visualizer.model)(results.links));
visualizer.setupZoomHandler(visualizer);
}
setupZoomHandler(visualizer) {
let g = visualizer.graph.select("g");
let zoomed = function () {
g.attr("transform", d3.event.transform);
};
// for bug #62 (begin)
let bbox = {x:0, y:0, width:0, height:0};
try {
let bbox = g.node().getBBox();
} catch (exception) {
console.warn(`An exception was caught: ${exception}`)
}
// for bug #62 (end)
let extent = [[bbox.x - bbox.width, bbox.y - bbox.height], [bbox.x+2*bbox.width, bbox.y+2*bbox.height]];
let zoom_handler = d3.zoom()
.scaleExtent([0.1,10])
// .translateExtent(extent)
.on("zoom", zoomed);
visualizer.graph.call(zoom_handler);
}
/** Visualisation of ontology.
* @param _results
* @param svgId Name of the svg id
* @param parameters Parameters that can be defined by the application
* -
*/
static drawOntology(_results, svgId, parameters = {}) {
// menuNode settings
var menuNode;
window.setDisplayRoot = function (parameters) {
drawer.setDisplayRoot(parameters.data);
drawer.computeHierarchy();
drawer.draw(svgId);
drawer.centerDisplay();
menuNode.displayOff();
}
window.switchMaskSubtree = function (parameters) {
drawer.switchVisibility(parameters.data, false);
drawer.draw(svgId);
menuNode.displayOff();
}
window.switchMaskAllSubtree = function (parameters) {
drawer.switchVisibility(parameters.data, true);
drawer.draw(svgId);
menuNode.displayOff();
}
let svg = d3.select(svgId);
let root = d3.select(svg.node().parentNode);
menuNode = ContextMenu.create(root, "nodeMenu")
.addEntry("set as root", setDisplayRoot)
.addEntry("Mask/Unmask the subtree", switchMaskAllSubtree)
;
// end of menuNode settings.
// begin of menu for the ontology graph background.
let menu = ContextMenu.create(root, "graphMenu")
.addEntry("Go to top level", function () {
drawer.goTop();
drawer.computeHierarchy();
drawer.draw(svgId);
menu.displayOff();
})
.addEntry("Up one level", function () {
drawer.up();
drawer.computeHierarchy();
drawer.draw(svgId);
drawer.centerDisplay();
menu.displayOff();
})
.addEntry("Reset centering", function () {
drawer.centerDisplay();
drawer.draw(svgId);
menu.displayOff();
})
.addEntry("Switch horizontal/vertical layout", function () {
drawer.switchLayout();
drawer.draw(svgId);
drawer.centerDisplay();
menu.displayOff();
})
;
svg.node().oncontextmenu = function () {
return false;
}
svg.on('contextmenu', function (e) {
d3.event.stopPropagation()
menu.displayOn();
});
svg.on('click', function (e) {
menu.displayOff();
menuNode.displayOff();
});
// end of menu for the ontology graph background.
parameters.menuNode = menuNode;
let drawer = new OntologyDrawer().setParameters(parameters).setData(_results).draw(svgId);
drawer.centerDisplay();
// Setup zoom handler
let g = svg.select("g");
let zoomed = function () {
g.attr("transform", d3.event.transform);
};
let bbox = g.node().getBBox();
let extent = [[bbox.x, bbox.y], [bbox.x+bbox.width, bbox.y+bbox.height]];
let zoom_handler = d3.zoom()
.scaleExtent([0.1,100])
// .translateExtent(extent)
.on("zoom", zoomed);
svg.call(zoom_handler);
return drawer;
}
static drawCircle(_results, svgId, parameters) {
let drawer2 = new OntologyDrawer().setParameters(parameters).setData(_results).drawCircle(svgId);
return drawer2;
}
/**
* @param _results JSON_LD results.
* @param svgId Name of the svg field to use (do not forget the '#').
* @param parameters TagCloud parameters.
* @TODO to be merged with drawSelect
*/
static drawTagCloud(_results, svgId, parameters) {
const tagCloudDrawer = new TagCloudDrawer().setParameters(parameters).setData(_results).draw(svgId);
return tagCloudDrawer;
}
static drawSelect(results, svgId, parameters) {
const selectDrawer = SelectDrawer.build(parameters);
selectDrawer.setData(results);
selectDrawer.setParameters(parameters)
selectDrawer.draw(svgId);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy