All Downloads are FREE. Search and download functionalities are using the official Maven repository.

webapp.js.OntologyDrawer.js Maven / Gradle / Ivy

Go to download

Corese is a Semantic Web Factory (triple store and SPARQL endpoint) implementing RDF, RDFS, SPARQL 1.1 Query and Update.

The newest version!
"use strict";
import {SvgDrawer} from "./SvgDrawer.js";
export class OntologyDrawer extends SvgDrawer {
    constructor() {
        super();
        this.radius = 2;
        this.horizontalLayout = false;
        this.setProperties(new Set(["rdfs:subClassOf"])); // - minus means that the representation of the link must be inverted.
        /** !Object */ this.rawData = undefined;
        /** !Object */ this.dataMap = undefined;
    }

    setData(data) {
        this.rawData = data;
        this.dataMap = {};
        for (let d of this.rawData.nodes) {
            /** @TODO dataMap objects should be a class instead of an adhoc structure. */
            this.dataMap[d.id] = {};
            for (let p in d) {
                this.dataMap[d.id][p] = d[p];
            }
            this.dataMap[d.id].children = [];
            this.dataMap[d.id].edgeChildren = [];
            /** !Set */ this.dataMap[d.id].parents = new Set();
        }
        this.edgesMapId = {};
        /** @TODO this should be moved to computeHierarchy(). The dataMap structure should be a read-only data
         * structure keeping the graph returned by Corese. */
        for (let e of data.edges) {
            this.edgesMapId[e.id] = e;
            if (this.properties.has(e.label)) {
                let s = e.source.id;
                let t = e.target.id;
                if (this.invertProperties[e.label]) {
                    let temp = s;
                    s = t;
                    t = temp;
                }
                this.dataMap[s].children.push(t);
                this.dataMap[s].edgeChildren.push(e);
                this.dataMap[t].parents.add(s);
            }
        }

        // compute numbers of tree roots.
        this.nbRoots = 0;
        this.roots = [];
        for (let d of data.nodes) {
            this.dataMap[d.id].value = this.dataMap[d.id].children.length;
            this.dataMap[d.id].r = this.radius;
            if (this.dataMap[d.id].parents.size === 0 || (this.dataMap[d.id].parents.size === 1 && this.dataMap[d.id].parents.has(d.id))) {
                if (this.dataMap[d.id].children.length === 0) {
                    continue;
                }
                console.log(`new tree root : ${d.id}`);
                this.nbRoots++;
                this.roots.push(d.id);
            }
        }
        if (this.nbRoots === 0 && data.nodes.length !== 0) { // cyclic graph choosing an arbitrary root.
            this.nbRoots = 1;
            let idPseudoRoot = data.nodes[0].id;
            this.roots.push(idPseudoRoot);
        }
        if (this.nbRoots > 1) {
            this.dataMap["Root"] = {
                id: "Root",
                label: "Root",
                r: this.radius,
                value: 1,
                isFolded: false,
                children: [],
                edgeChildren: []
            };
            for (let child of this.roots) {
                this.dataMap["Root"].children.push(child);
                this.dataMap["Root"].edgeChildren.push({source: "Root", target: child, label: "root", class: "root"});
                this.dataMap[child].parents.add("Root");
            }
            this.root = "Root";
        } else {
            this.root = this.roots.pop();
        }
        this.computeHierarchy();
        return this;
    }

    /**
     *
     * @param params Expect { rootId : "id", properties: {"prop1", "prop2"}, ["menuNode": menu]}
     */
    setParameters(params) {
        if (params !== undefined) {
            this.parameters = params;
            if ("rootId" in params) {
                this.setDisplayRoot(params.rootId);
            }
            if ("properties" in params) {
                this.setProperties(params.properties);
            }
            if ("menuNode" in params) {
                this.menuNode = params.menuNode;
            }
        }
        return this;
    }

    /**
     *  Set the root used by the tree layout algorithm. I.e. draw the subtree below root (included).
     *  @param {!string} root
     */
    setDisplayRoot(root) {
        this.displayRoot = root.id;
        this.displayRootNode = root;
        return this;
    }

    setVisibility(node, value, recursive) {
        if (!node.isLeaf()) {
            node.isFolded = value;
            if (recursive) {
                for (let child of node.children) {
                    this.setVisibility(child, value, true);
                }
            }
        }
    }

    switchVisibility(node, recursive) {
        if (!node.isLeaf()) {
            node.isFolded = !node.isFolded;
            this.setVisibility(node, node.isFolded, recursive);
        }
    }

    /*
     *  Set the properties to be used when extracting the tree.
     */
    setProperties(properties) {
        this.properties = properties;
        this.invertProperties = {};
        let newProperties = new Set();
        for (let currentProp of this.properties.values()) {
            if (currentProp[0] === "-") {
                // this.properties.delete(currentProp);
                currentProp = currentProp.substring(1, currentProp.length);
                this.invertProperties[currentProp] = false;
            } else {
                if (currentProp[0] === "+") {
                    currentProp = currentProp.substring(1, currentProp.length);
                }
                this.invertProperties[currentProp] = true;
            }
            newProperties.add(currentProp);
        }
        this.properties = newProperties;
    }

    /**
     * The method builds a *tree* from the graph stored in this.dataRaw and this.dataMap.
     * @returns {OntologyDrawer}
     */
    computeHierarchy() {
        if (this.displayRoot === undefined) {
            this.displayRoot = this.root;
        }

        /**
         *  Represents a tree node.
         *  Note that children store all the children, and getVisibleChildren() returns only the visible ones.
         */
        class Node {

            constructor(id, depth, parent, uplink) {
                /** string */ this.id = id;
                /** !number */ this.depth = depth;
                /** !Node */   this.parent = parent;
                /** Edge  */   this.uplink = uplink;
                /** !Array */ this.children = [];
                /** !boolean */ this.evenNode = undefined;
                /** string */  this.label = Node.dataMap[this.id].label;
                /** string */ this.url = Node.dataMap[this.id].url;
                /** string */ this.link = Node.dataMap[this.id].link;
                /** boolean */ this.isFolded = false;
                /** boolean */ this.isCycle = false; // Whether the node begins a new cycle.
                /** number */  this.nbChildren = 0; // Used only when a cycle is detected (to be able to say how many childs are hidden).
            }

            addChild(newChild) {
                this.children.push(newChild);
            }

            /** @return {string} */ toString() {
                let /** !string */ s = "";
                for (let k of ["id", "depth", "evenNode"]) {
                    s += `${k}: ${this[k]} `;
                }
                if (this.parent !== undefined) {
                    s += `parent ${this.parent.id} `;
                } else {
                    s += `no parent `;
                }
                s += "children [";
                for (let k of this.children) {
                    s += `${k.id} `;
                }
                s += "]";
                return s;
            }


            getVisibleChildren() {
                if (this.isFolded) {
                    return [];
                } else {
                    return this.children;
                }
            };

            isLeaf() {
                return (this.children.length === 0);
            };
        }

        Node.dataMap = this.dataMap;

        // Fill the children map with { id: dataMap[id] }, in order to make the dataMap structure compatible
        // with the layout algorithm.
        /** !Object> */ this.slices = {}; // in order to know which nodes are at the same depth.
        /** !Node */ this.hierarchy = new Node(this.displayRoot, 0, undefined, undefined);
        let /** !Array */ stack = [];
        stack.push(this.hierarchy);
        let /** !Set */ alreadySeen = new Set();
        while (stack.length !== 0) {
            let /** !Node */ summit = stack.pop();
            console.log(`summit ${summit}`);
            if (this.slices[summit.depth] === undefined) {
                this.slices[summit.depth] = [];
            }
            this.slices[summit.depth].push(summit);
            if (alreadySeen.has(summit.id)) {
                console.log("cycle detected including node:" + summit.id);
                summit.isCycle = true;
                summit.nbChildren = this.dataMap[summit.id].children.length;
            } else {
                alreadySeen.add(summit.id);
                let /** !Object */ data = this.dataMap[summit.id];
                for (let i = 0; i < data.children.length; i++) {
                    let /** number */ childId = data.children[i];
                    let /** !Edge */ childEdge = data.edgeChildren[i];
                    let /** !Node */ childNode = new Node(childId, summit.depth + 1, summit, childEdge);
                    summit.addChild(childNode);
                    stack.push(childNode);
                }
            }
        }
        for (let /** number */ slice in this.slices) {
            console.log(`slice: ${slice}`);
            let /** number */ i = 0;
            for (let /** !number */ node of this.slices[slice]) {
                node.evenNode = (i % 2 === 0);
                i++;
                console.log(`${node} `);
            }
        }

        /** compute width and height of the tree.
         *  @param {!Node} tree
         *  @return {Object}
         * */
        let recurNode = function (tree) {
            let height = 0;
            let width = 0;
            if (tree !== undefined) {
                for (let /** !Node */ child of tree.children) {
                    let result = recurNode(child);
                    height = Math.max(height, result.height);
                    width += result.width;
                }
                height += 1; // count "node" itself.
                width = Math.max(width, 1);
            }
            return {"height": height, "width": width};
        }.bind(this);
        let geomTree = recurNode(this.hierarchy);
        this.width = geomTree.width;
        this.height = geomTree.height;
        return this;
    }

    /** return {number} */
    getWidth() {
        return this.width;
    }

    /** return {number} */
    getHeight() {
        return this.height;
    }

    /**
     * @param svgId
     */
    draw(svgId) {
        this.svgId = svgId;
        d3.select(svgId).node().oncontextmenu = function () {
            return false;
        }
        // set the dimensions and margins of the diagram
        let margin = {top: 20, right: 20, bottom: 20, left: 20};
        let width = Math.max(this.getWidth(), 5) * 45 - margin.left - margin.right;
        let height = Math.max(this.getHeight(), 5) * 125 - margin.top - margin.bottom;
        if (!this.horizontalLayout) {
            let temp = width;
            width = height;
            height = temp;
        }

// declares a tree layout and assigns the size
        var treemap = d3.tree()
            .separation(function (a, b) {
                let nodeA = this.dataMap[a.data.id];
                let nodeB = this.dataMap[b.data.id];
                return (nodeA.label.length + nodeB.label.length) / 20;
            }.bind(this))
            .nodeSize([50, 150])
        ;

//  assigns the data to a hierarchy using parents-child relationships
        var nodes = d3.hierarchy(this.hierarchy, function (d) {
            return d.getVisibleChildren();
        });

// maps the node data to the tree layout
        nodes = treemap(nodes);

// append the svg object to the body of the page
// appends a 'group' element to 'svg'
// moves the 'group' element to the top left margin
        this.svg = d3.select(svgId);
        if (this.g === undefined) {
            this.g = this.svg
                .append("g")
        }
        this.g.selectAll(".link").remove();
        const link = this.g.selectAll(".link")
            .data(nodes.descendants().slice(1))
            .enter().append("path")
            .attr("class", function (d) {
                let result = "link";
                if (d.data.uplink.class !== undefined) {
                    result = `${result} ${d.data.uplink.class}`;
                }
                return result;
            })
            .attr("d", function (d) {
                if (this.horizontalLayout) {
                    return "M" + d.y + "," + d.x
                        + "C" + (d.y + d.parent.y) / 2 + "," + d.x
                        + " " + (d.y + d.parent.y) / 2 + "," + d.parent.x
                        + " " + d.parent.y + "," + d.parent.x;
                } else {
                    return "M" + d.x + "," + d.y
                        + "C" + (d.x + d.parent.x) / 2 + "," + d.y
                        + " " + (d.x + d.parent.x) / 2 + "," + d.parent.y
                        + " " + d.parent.x + "," + d.parent.y;
                }
            }.bind(this));
        link.append("title")
            .text((d) => {
                    // return `link between ${d.parents.data.id} -- [${this.dataMap[d.data.id].parentEdge.label}] --> ${d.data.id}` ;
                    let result = "";
                    let first = true;
                    if ("edgePropertiesToDisplay" in this.parameters) {
                        for (let prop of this.parameters.edgePropertiesToDisplay) {
                            if (first) {
                                first = false;
                            } else {
                                result += "\n";
                            }
                            if (this.dataMap[d.data.id].parentEdge !== undefined) {
                                result += `${prop}: ${this.dataMap[d.data.id].parentEdge[prop]}`;
                            }
                        }
                    }
                    return result;
                }
            );

// begin: draw each node.
        this.g.selectAll(".node").remove();
        const node = this.g.selectAll(".node")
            .data(nodes.descendants())
            .enter().append("g")
            .attr("class", function (d) {
                    let result = "node" +
                        (d.data.isFolded ?
                            " node--folded" :
                            (d.data.isLeaf() ? " node--leaf" : " node--internal"));
                    if (d.data.isCycle) {
                        result = `${result} node--cycle`;
                    }
                    if (this.dataMap[d.data.id].class !== undefined) {
                        result = `${result} ${this.dataMap[d.data.id].class}`;
                    }
                    return result;
                }.bind(this)
            )
            .attr("transform", function (d) {
                if (this.horizontalLayout) {
                    return "translate(" + d.y + "," + d.x + ")";
                } else {
                    return "translate(" + d.x + "," + d.y + ")";
                }

            }.bind(this));
        node.on("click", (d) => {
            if (d.data.url !== undefined) window.open(d.data.url);
            if (d.data.link !== undefined) trans(d.data.link);
        });
        node
            .append("title")
            .text((d) => {
                let result = "";
                let first = true;
                if ("nodePropertiesToDisplay" in this.parameters) {
                    for (let prop of this.parameters.nodePropertiesToDisplay) {
                        if (first) {
                            first = false;
                        } else {
                            result += "\n";
                        }
                        result += `${prop}: ${this.dataMap[d.data.id][prop]}`;
                    }
                    return result;
                }
            });
        node.on("contextmenu", (currentNode) => {
            d3.event.preventDefault();
            d3.event.stopImmediatePropagation();
            this.menuNode.setParameters(currentNode);
            this.menuNode.displayOn();
        });

// adds the circle to the node
        node.append("circle")
            .attr("r", (d) => this.radius);

// adds the text to the node
        let textNode = node.append("text");
        textNode.attr("dy", ".35em")
            .attr("y", function (d) {
                    if (!this.horizontalLayout) {
                        return d.data.evenNode ? "20" : "-20";
                    } else {
                        return "20";
                    }
                }.bind(this)
            )
            .style("text-anchor", "middle")
            .text(function (d) {
                let result = d.data.label;
                if (d.data.isCycle && d.data.nbChildren > 0) {
                    result += ` (${d.data.nbChildren})`;
                }
                return result;
            });

// end: draw each node.

        this.zoomed = function () {
            this.g.attr("transform", d3.event.transform);
        };
        this.zoomListener = d3.zoom().on("zoom", this.zoomed.bind(this));

        this.svg.call(this.zoomListener);
        return this;
    }

    switchLayout() {
        this.horizontalLayout = !this.horizontalLayout;
    }

    centerDisplay() {
        let divHeight = this.svg.node().parentNode.clientHeight;
        let divWidth = this.svg.node().parentNode.clientWidth;
        let bbox = this.g.node().getBBox();
        let tx = -bbox.x;
        let ty = -bbox.y;
        let scale = Math.min(divHeight / bbox.height, divWidth / bbox.width);
        let zoom = d3.zoomIdentity.translate(tx*scale, ty*scale+divHeight/3).scale(scale);
        this.svg.call(this.zoomListener.transform, zoom);
    }

    goTop() {
        this.displayRoot = this.root;
    }

    up() {
        if (this.displayRootNode.parent !== undefined) {
            this.displayRoot = this.displayRootNode.parent.id;
            this.displayRootNode = this.displayRootNode.parent;
        } else if (this.dataMap["Root"] !== undefined) {
            this.displayRoot = "Root";
        }
    }

    drawCircle(svgId) {
        this.svgId = svgId;
        this.root = "Root";
        this.computeHierarchy();
        var svg = d3.select(svgId),
            width = +svg.attr("width"),
            height = +svg.attr("height");
        var format = d3.format(",d");

        var color = d3.scaleSequential(d3.interpolateMagma)
            .domain([-4, 4]);

        var stratify = d3.stratify()
            .id((d) => d.id)
            .parentId((d) => d.parent);

        var pack = d3.pack()
            .size([width - 2, height - 2])
            .padding(3);
        var root = stratify(Object.values(this.dataMap))
            .sum(function (d) {
                return d.value + 1;
            })
            .sort(function (a, b) {
                return b.value - a.value;
            })
        ;
        pack(root);

        var node = svg.select("g")
            .selectAll("g")
            .data(root.descendants())
            .enter().append("g")
            .attr("transform", function (d) {
                return "translate(" + d.x + "," + d.y + ") scale(1)";
            })
            .attr("class", function (d) {
                return "node" + (!d.children ? " node--leaf" : d.depth ? "" : " node--root");
            })
            .each(function (d) {
                d.node = this;
            });

        node.append("circle")
            .attr("id", function (d) {
                return "node-" + d.id;
            })
            .attr("r", function (d) {
                return d.r;
            })
            .style("fill", function (d) {
                return color(d.depth);
            });

        var leaf = node.filter(function (d) {
            return !d.children;
        });

        leaf.append("clipPath")
            .attr("id", function (d) {
                return "clip-" + d.id;
            })
            .append("use")
            .attr("xlink:href", function (d) {
                return "#node-" + d.id + "";
            });

        leaf.append("text")
            .attr("clip-path", function (d) {
                return "url(#clip-" + d.id + ")";
            })
            .selectAll("tspan")
            .data(function (d) {
                console.log(d);
                return [d.data.label];
            })
            .enter().append("tspan")
            .attr("x", function (d, i, nodes) {
                return -d.length * 2 - 13
            })
            .attr("y", function (d, i, nodes) {
                return 3;
            })
            .text(function (d) {
                return d;
            });

        node.append("title")
            .text(function (d) {
                return d.data.label;
            });
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy