graph.js

/**
 * @file Generate nodes, labels & links for graph. Highlight, hide & display nodes & links. Set mouse events.
 * @author Guillaume Brioudes
 * @copyright MIT License ANR HyperOtlet
 */

(function() {

let link, node, circles, labels
    , simulation = d3.forceSimulation()
    , width = +svg.node().getBoundingClientRect().width
    , height = +svg.node().getBoundingClientRect().height;

d3.select(window).on("resize", function () {
    width = +svg.node().getBoundingClientRect().width;
    height = +svg.node().getBoundingClientRect().height;
    updateForces();
});

initializeDisplay();
initializeSimulation();

/**
 * Set up the simulation and event to update locations after each tick
 */

function initializeSimulation() {
    simulation.nodes(graph.nodes);
    initializeForces();
    simulation.on("tick", ticked);
}

/**
 * Set the forces to the simulation
 */

function initializeForces() {
    // add forces and associate each with a name
    simulation
        .force("link", d3.forceLink())
        .force("charge", d3.forceManyBody())
        .force("collide", d3.forceCollide())
        .force("center", d3.forceCenter())
        .force("forceX", d3.forceX())
        .force("forceY", d3.forceY());
    // apply properties to each of the forces
    updateForces();
}

/**
 * Update the forces to the simulation
 */

function updateForces() {
    // get each force by name and update the properties

    simulation.force("center")
        .x(width * graphProperties.position.x)
        .y(height * graphProperties.position.y);

    simulation.force("charge")
        // turn force value to negative number
        .strength(-Math.abs(graphProperties.attraction.force))
        .distanceMax(graphProperties.attraction.distance_max);

    simulation.force("forceX")
        .strength(graphProperties.attraction.horizontale)

    simulation.force("forceY")
        .strength(graphProperties.attraction.verticale)

    simulation.force("link")
        .id((d) => d.id)
        .links(graph.links);

    // restarts the simulation
    simulation.alpha(1).restart();
}

window.updateForces = updateForces;

function updateGraphTextSize() {
    node.selectAll("text")
        .attr('font-size', graphProperties.text_size);
}

window.updateGraphTextSize = updateGraphTextSize;

/**
 * Initialize visualisation
 */

function initializeDisplay() {

    // set the data and properties of link lines
    link = svg.append("g")
        .attr("class", "links")
        .selectAll("line")
        .data(graph.links)
        .enter().append("line");

    // set the data and properties of node circles
    node = svg.append("g")
        .attr("class", "nodes")
        .selectAll("g")
        .data(graph.nodes)
        .enter().append("g")
        .attr("data-node", (d) => d.id)
        .on('click', function(nodeMetas) {
            openRecord(nodeMetas.id);
        })

    circles = node.append("circle")
        .attr("r", (d) => d.size)
        .attr("class", (d) => "n_" + d.type)
        .call(d3.drag()
            .on("start", function(d) {
                if (!d3.event.active) simulation.alphaTarget(0.3).restart();
                d.fx = d.x;
                d.fy = d.y; })
            .on("drag", function(d) {
                d.fx = d3.event.x;
                d.fy = d3.event.y; })
            .on("end", function(d) {
                if (!d3.event.active) simulation.alphaTarget(0.0001);
                d.fx = null;
                d.fy = null; })
        )
        .on('mouseenter', function(nodeMetas) {
            if (!graphProperties.highlight_on_hover) { return; }

            let nodesIdsHovered = [nodeMetas.id];

            const linksToModif = link.filter(function(link) {
                if (link.source.id === nodeMetas.id || link.target.id === nodeMetas.id) {
                    nodesIdsHovered.push(link.source.id, link.target.id);
                    return false;
                }
                return true;
            })

            const nodesToModif = node.filter(function(node) {
                if (nodesIdsHovered.includes(node.id)) {
                    return false;
                }
                return true;
            })

            const linksHovered = link.filter(function(link) {
                if (link.source.id !== nodeMetas.id && link.target.id !== nodeMetas.id) {
                    return false;
                }
                return true;
            })

            const nodesHovered = node.filter(function(node) {
                if (!nodesIdsHovered.includes(node.id)) {
                    return false;
                }
                return true;
            })

            nodesHovered.classed('hover', true);
            linksHovered.classed('hover', true);
            nodesToModif.classed('translucent', true);
            linksToModif.classed('translucent', true);
        })
        .on('mouseout', function() {
            if (!graphProperties.highlight_on_hover) { return; }

            node.classed('hover', false);
            node.classed('translucent', false);
            link.classed('hover', false);
            link.classed('translucent', false);
        })

    labels = node.append("text")
        .each(function(d) {
            const words = d.label.split(' ')
                , max = 25
                , text = d3.select(this);
            let label = '';

            for (let i = 0; i < words.length; i++) {
                // combine words and seperate them by a space caracter into label
                label += words[i] + ' ';

                // if label (words combination) is longer than max & not the single iteration
                if (label.length < max && i !== words.length - 1) { continue; }

                text.append("tspan")
                    .attr('x', 0)
                    .attr('dy', '1.2em')
                    .text(label.slice(0, -1)); // remove last space caracter
    
                label = '';
            }
        })
        .attr('font-size', graphProperties.text_size)
        .attr('x', 0)
        .attr('y', (d) => d.size)
        .attr('dominant-baseline', 'middle')
        .attr('text-anchor', 'middle');

    link.attr("class", (d) => 'l_' + d.type)
        .attr("data-source", (d) => d.source)
        .attr("data-target", (d) => d.target)
        .attr("stroke-dasharray", function(d) {
            if (d.shape.stroke === 'dash' || d.shape.stroke === 'dotted') {
                return d.shape.dashInterval }
            return false;
        })
        .attr("filter", function(d) {
            if (d.shape.stroke === 'double') {
                return 'url(#double)' }
            return false;
        });

    if (graphProperties.arrows === true) {
        link.attr("marker-end", 'url(#arrow)');
    }
}

/**
 * Update elements position
 */

 function ticked() {
    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("transform", function(d) {
        d.x = Math.max(d.size, Math.min(width - d.size, d.x));
        d.y = Math.max(d.size, Math.min(height - d.size, d.y));

        return "translate(" + d.x + "," + d.y + ")";
    })

    d3.select('#load-bar-value').style('flex-basis', (simulation.alpha() * 100) + '%');
}

/**
 * Get nodes and their links
 * @param {array} nodeIds - List of nodes ids
 * @returns {array} - DOM elts : nodes and their links
 */

function getNodeNetwork(nodeIds) {
    const diplayedNodes = graph.nodes.filter(item => item.hidden === false)
        .map(item => item.id);

    const nodes = node.filter(node => nodeIds.includes(node.id));

    const links = link.filter(function(link) {
        if (!nodeIds.includes(link.source.id) && !nodeIds.includes(link.target.id)) {
            return false; }
        if (!diplayedNodes.includes(link.source.id) || !diplayedNodes.includes(link.target.id)) {
            return false; }

        return true;
    });

    return {
        nodes: nodes,
        links: links
    }
}

/**
 * Display none nodes and their link
 * @param {array} nodeIds - List of nodes ids
 */

function hideNodeNetwork(nodeIds) {
    const ntw = getNodeNetwork(nodeIds);

    ntw.nodes.style('display', 'none');
    ntw.links.style('display', 'none');
}

window.hideNodeNetwork = hideNodeNetwork;

/**
 * Reset display nodes and their link
 * @param {array} nodeIds - List of nodes ids
 */

function displayNodeNetwork(nodeIds) {
    const ntw = getNodeNetwork(nodeIds);

    ntw.nodes.style('display', null);
    ntw.links.style('display', null);
}

window.displayNodeNetwork = displayNodeNetwork;

/**
 * Apply highlightColor (from config) to somes nodes and their links
 * @param {array} nodeIds - List of nodes ids
 */

function highlightNodes(nodeIds) {
    const ntw = getNodeNetwork(nodeIds);

    ntw.nodes.classed('highlight', true);
    ntw.links.classed('highlight', true);

    view.highlightedNodes = view.highlightedNodes.concat(nodeIds);
}

window.highlightNodes = highlightNodes;

/**
 * remove highlightColor from all highlighted nodes and their links
 */

function unlightNodes() {
    if (view.highlightedNodes.length === 0) { return; }

    const ntw = getNodeNetwork(view.highlightedNodes);

    ntw.nodes.classed('highlight', false);
    ntw.links.classed('highlight', false);

    view.highlightedNodes = [];
}

window.unlightNodes = unlightNodes;

/**
 * Toggle display/hide nodes links
 * @param {bool} isChecked - 'checked' value send by a checkbox input
 */

function linksDisplayToggle(isChecked) {
    if (isChecked) {
        link.style('display', null);
    } else {
        link.style('display', 'none');
    }
}

window.linksDisplayToggle = linksDisplayToggle;

/**
 * Toggle display/hide nodes label
 * @param {bool} isChecked - 'checked' value send by a checkbox input
 */

function labelDisplayToggle(isChecked) {
    if (isChecked) {
        labels.style('display', null);
    } else {
        labels.style('display', 'none');
    }
}

window.labelDisplayToggle = labelDisplayToggle;

/**
 * Add 'highlight' class to texts linked to nodes ids
 * @param {array} nodeIds - List of node ids
 */

function labelHighlight(nodeIds) {
    const labelsToHighlight = node
        .filter(node => nodeIds.includes(node.id)).select('text');

    graph.nodes = graph.nodes.map(function(node) {
        if (nodeIds.includes(node.id)) {
            node.highlighted = true; }
        return node;
    });

    labelsToHighlight.classed('highlight', true);
}

window.labelHighlight = labelHighlight;

/**
 * Remove 'highlight' class from texts linked to nodes ids
 * @param {array} nodeIds - List of node ids
 */

function labelUnlight(nodeIds) {
    const labelsToHighlight = node
        .filter(node => nodeIds.includes(node.id)).select('text');

    graph.nodes = graph.nodes.map(function(node) {
        if (nodeIds.includes(node.id)) {
            node.highlighted = false; }
        return node;
    });

    labelsToHighlight.classed('highlight', false);
}

window.labelUnlight = labelUnlight;

/**
 * Remove 'highlight' class from all texts
 */

function labelUnlightAll() {
    graph.nodes = graph.nodes.map(function(node) {
        node.highlighted = false;
        return node;
    });

    labels.classed('highlight', false);
}

window.labelUnlightAll = labelUnlightAll;

})();