shreyasbharath/cpp_dependency_graph

View on GitHub
views/index.html.template

Summary

Maintainability
Test Coverage
<!DOCTYPE html>
<meta charset="utf-8">

<head>
    <style>
        .node.filtered {
            fill-opacity: 0.3;
            stroke-opacity: 0.3;
        }

        .structNode.filtered {
            fill-opacity: 0.3;
            stroke-opacity: 0.3;
        }

        text.filtered {
            fill-opacity: 0;
            stroke-opacity: 0;
        }

        .link.filtered {
            stroke: #ddd;
            fill-opacity: 0.1;
            stroke-opacity: 0.1;
        }

        .link.dependency {
            stroke: #900;
            fill: #900;
            pointer-events: none;
        }

        .link.dependants {
            stroke: #090;
            fill: #090;
            pointer-events: none;
        }

        .node.skipped {
            fill-opacity: 0.0;
            stroke-opacity: 0.0;
        }

        text.skipped {
            fill-opacity: 0;
            stroke-opacity: 0;
        }

        .structNode.skipped {
            fill-opacity: 0.0;
            stroke-opacity: 0.0;
        }


        .link.skipped {
            stroke: #ddd;
            fill-opacity: 0.0;
            stroke-opacity: 0.0;
        }

        .node {
            stroke: #000;
            stroke-width: 0.5px;
        }

        .structNode {
            stroke: #000;
            stroke-width: 0.5px;
        }

        .link {
            stroke: #999;
            stroke-opacity: .6;
            fill: none;
            stroke-width: 1.5px;
            pointer-events: none;
        }

        .marker#default {
            stroke: #999;
            fill: #999;
            pointer-events: none;
        }

        .marker#dependency {
            stroke: #900;
            fill: #900;
            pointer-events: none;
        }

        .marker#dependants {
            stroke: #090;
            fill: #090;
            pointer-events: none;
        }

        body {
            margin: 0px;
            padding: 0px;
        }

        html {
            overflow: hidden;
        }

        text {
            stroke: #000;
            stroke-width: 0.5px;
            text-anchor: middle;
            font: 10px sans-serif;
            font-weight: normal;
            font-style: normal;
            pointer-events: none;
        }

        svg text {
            -webkit-user-select: none;
            -moz-user-select: none;
            -ms-user-select: none;
            user-select: none;
            cursor: default;
        }

        svg text::selection {
            background: none;
        }

        form {
            position: absolute;
            right: 10px;
            top: 60px;
        }

        #simple-menu {
            position: absolute;
            right: 10px;
            top: 10px;
        }
    </style>
</head>

<body>

    <script type="text/javascript" src="https://unpkg.com/jquery@3.3.1/dist/jquery.min.js"></script>
    <script type="text/javascript" src="https://unpkg.com/underscore@1.8.3/underscore-min.js"></script>
    <script type="text/javascript" src="https://unpkg.com/d3@4.7.4/build/d3.min.js"></script>
    <script type="text/javascript" src="https://cdn.rawgit.com/edeno/d3-save-svg/gh-pages/assets/d3-save-svg.min.js"></script>

    <!-- ================================================= -->
    <!-- ===========ACTUAL HTML           ================ -->
    <!-- ================================================= -->

    <form id="form">
        <label>
            <input type="range" name="circle_size" min="1" max="50" value="15" /> Circle size</label>
        <br>
        <label>
            <input type="range" name="charge_multiplier" min="1" max="500" value="100" /> Charge multiplier</label>
        <br>
        <label>
            <input type="range" name="link_strength" min="0.1" max="100" value="7" /> Link strength</label>
        <br>
        <label>
            <input type="checkbox" name="show_texts_near_circles" /> Show names</label>
        <br>
        <input id="search_input" placeholder="Type regexp to filter nodes" style="width:100%%;">
        <br>
    </form>

    <button id="export">Export as SVG</button>

    <div id="chart">
        <!-- Here the SVG will be placed-->
    </div>

    <script type='text/javascript'>
        var dependencies = {
            links: %{dependency_links},
            objects: { }
        };
    </script>
    <script type="text/javascript">
        //  ===================================================
        //  ===============  SELECTING_NODE       =============
        //  ===================================================

        let graph_actions = {
            create: function (svg, dvgraph) {

                return {
                    selectedIdx: -1,
                    selectedType: "normal",
                    svg: svg,
                    selectedObject: {},
                    dvgraph: dvgraph,

                    deselect_node: function (d) {
                        this._unlockNode(d);
                        this.selectedIdx = -1;
                        this.selectedObject = {};

                        this.svg.selectAll('.node, .structNode')
                            .each(function (node) {
                                node.filtered = false
                            })
                            .classed('filtered', false)
                            .transition();

                        this.svg.selectAll('path, text')
                            .classed('filtered', false)
                            .transition();


                        this.svg.selectAll('.link')
                            .attr("marker-end", "url(#default)")
                            .classed('filtered', false)
                            .classed('dependency', false)
                            .classed('dependants', false)
                            .transition();
                    },

                    deselect_selected_node: function () {
                        this.deselect_node(this.selectedObject)
                    },

                    _lockNode: function (node) {
                        node.fixed = true;
                        node.fx = node.x;
                        node.fy = node.y;
                    },

                    _unlockNode: function (node) {
                        delete node.fixed;
                        node.fx = null;
                        node.fy = null;
                    },

                    _selectAndLockNode: function (node, type) {
                        this._unlockNode(this.selectedObject);
                        this.selectedIdx = node.idx;
                        this.selectedObject = node;
                        this.selectedType = type;
                        this._lockNode(this.selectedObject);
                    },

                    _deselectNodeIfNeeded: function (node, type) {
                        if (node.idx === this.selectedIdx && this.selectedType === type) {
                            this.deselect_node(node);
                            return true;
                        }
                        return false;
                    },

                    _fadeOutAllNodesAndLinks: function () {
                        // Fade out all circles
                        this.svg.selectAll('.node, .structNode')
                            .classed('filtered', true)
                            .each(function (node) {
                                node.filtered = true;
                                node.neighbours = false;
                            }).transition();

                        this.svg.selectAll('text')
                            .classed('filtered', true)
                            .transition();

                        this.svg.selectAll('.link')
                            .classed('dependency', false)
                            .classed('dependants', false)
                            .transition()
                            .attr("marker-end", "");

                    },

                    _highlightNodesWithIndexes: function (indexesArray) {
                        this.svg.selectAll('.node, .structNode, text')
                            .filter((node) => indexesArray.indexOf(node.index) > -1)
                            .classed('filtered', false)
                            .each((node) => {
                                node.filtered = false;
                                node.neighbours = true;
                            })
                            .transition();
                    },

                    _isDependencyLink: (node, link) => (link.source.index === node.index),
                    _nodeExistsInLink: (node, link) => (link.source.index === node.index || link.target.index === node.index),
                    _oppositeNodeOfLink: (node, link) => (link.source.index === node.index ? link.target : link.target.index === node.index ? link.source : null),

                    _highlightLinksFromRootWithNodesIndexes: function (root, nodeNeighbors, maxLevel) {
                        this.svg.selectAll('.link')
                            .filter((link) => nodeNeighbors.indexOf(link.source.index) > -1)
                            .classed('filtered', false)
                            .classed('dependency', (l) => this._nodeExistsInLink(root, l) && this._isDependencyLink(root, l))
                            .classed('dependants', (l) => this._nodeExistsInLink(root, l) && !this._isDependencyLink(root, l))
                            .attr("marker-end", (l) => this._nodeExistsInLink(root, l) ? (this._isDependencyLink(root, l) ? "url(#dependency)" : "url(#dependants)") : (maxLevel == 1 ? "" : "url(#default)"))
                            .transition();
                    },

                    selectNodesStartingFromNode: function (node, maxLevel = 100) {
                        if (this._deselectNodeIfNeeded(node, "level" + maxLevel)) {
                            return
                        }
                        this._selectAndLockNode(node, "level" + maxLevel);

                        let neighborIndexes =
                            this.dvgraph.nodesStartingFromNode(node, { max_level: maxLevel, use_backward_search: maxLevel == 1 })
                                .map((n) => n.index);

                        this._fadeOutAllNodesAndLinks();
                        this._highlightNodesWithIndexes(neighborIndexes);
                        this._highlightLinksFromRootWithNodesIndexes(node, neighborIndexes, maxLevel);
                    }

                };
            }
        };


    </script>
    <script type="text/javascript">
        //  ===================================================
        //  =============== PARSING ===========================
        //  ===================================================
        // Input
        // { links : [ {source: sourceName, dest : destName} * ] }
        // Output:
        let objcdv = {
            version: "0.0.1",
            _createGraph: function (_objects) {
                return {
                    nodes: [],
                    links: [],
                    nodesSet: {},
                    objects: setDefaultValue(_objects, []),

                    addLink: function (link) {

                        var source_node = this.getNode(link.source);
                        source_node.source++;

                        var dest_node = this.getNode(link.dest);
                        dest_node.dest++;

                        this.links.push({
                            // d3 js properties
                            source: source_node.idx,
                            target: dest_node.idx,

                            // Additional link information
                            sourceNode: source_node,
                            targetNode: dest_node
                        })
                    },

                    getNode: function (nodeName) {
                        var node = this.nodesSet[nodeName];
                        if (node == null) {
                            var idx = Object.keys(this.nodesSet).length;
                            let object = setDefaultValue(this.objects[nodeName], {})
                            this.nodesSet[nodeName] = node = { idx: idx, name: nodeName, source: 1, dest: 0, type: object.type };
                        }
                        return node
                    },

                    updateNodes: function (f) {
                        _.values(this.nodesSet).forEach(f)
                    },

                    d3jsGraph: function () {
                        // Sorting up nodes, since, in some cases they aren't returned in correct number
                        var nodes = _.values(this.nodesSet).slice(0).sort((a, b) => a.idx - b.idx);
                        return { nodes: nodes, links: this.links };
                    },

                    nodesStartingFromNode: function (node, { max_level = 100, use_backward_search = false, use_forward_search = true } = {}) {
                        // Figure out the neighboring node id's with brute strength because the graph is small
                        var neighbours = {};
                        neighbours[node.index] = node;

                        var nodesToCheck = [node.index];
                        let current_level = 0;
                        while (Object.keys(nodesToCheck).length != 0) {
                            var forwardNeighbours = [];
                            var backwardNeighbours = [];

                            let tmpNeighbours = {};
                            if (use_forward_search) {
                                forwardNeighbours = this.links
                                    .filter((link) => link.source.index in neighbours)
                                    .filter((link) => !(link.target.index in neighbours))
                                    .map((link) => {
                                        tmpNeighbours[link.target.index] = link.target;
                                        return link.target.index;
                                    });
                            }
                            if (use_backward_search) {
                                backwardNeighbours = this.links
                                    .filter((link) => link.target.index in neighbours)
                                    .filter((link) => !(link.source.index in neighbours))
                                    .map((link) => {
                                        tmpNeighbours[link.source.index] = link.source;
                                        return link.source.index;
                                    });
                            }

                            _.extend(neighbours, tmpNeighbours);


                            nodesToCheck = forwardNeighbours.concat(backwardNeighbours);
                            console.log("Nodes to check" + nodesToCheck);

                            // Skip if we reached max level
                            current_level++;
                            if (current_level == max_level) {
                                console.log("Reached max at level" + current_level);
                                break;
                            }
                        }
                        return _.values(neighbours);

                    }

                };

            },
            _createPrefixes: function () {
                return {
                    _prefixesDistr: {},

                    _sortedPrefixes: null,

                    addName: function (name) {
                        this._sortedPrefixes = null;

                        var prefix = name.substring(0, 2);
                        if (!(prefix in this._prefixesDistr)) {
                            this._prefixesDistr[prefix] = 1;
                        } else {
                            this._prefixesDistr[prefix]++;
                        }
                    },

                    prefixIndexForName: function (name) {
                        var sortedPrefixes = this._getSortedPrefixes();
                        var prefix = name.substring(0, 2);
                        return _.indexOf(sortedPrefixes, prefix)
                    },

                    _getSortedPrefixes: function () {
                        if (this._sortedPrefixes == null) {
                            this._sortedPrefixes = _.map(this._prefixesDistr, (v, k) => ({ "key": k, "value": v }))
                                .sort((a, b) => b.value - a.value)
                                .map(o => o.key)
                        }
                        return this._sortedPrefixes
                    }
                };
            },


            parse_dependencies_graph: function (dependencies) {

                var graph = this._createGraph(dependencies.objects);
                var prefixes = this._createPrefixes();

                dependencies.links
                    .filter(link => link.source != link.dest)
                    .forEach(link => {
                        graph.addLink(link);

                        prefixes.addName(link.source);
                        prefixes.addName(link.dest);
                    });

                // Make sure all nodes are present, even if they aren't connected
                if (dependencies.objects != null) {
                    for (p in dependencies.objects) {
                        graph.getNode(p)
                    }
                }

                graph.updateNodes((node) => {
                    node.weight = node.source;
                    node.group = prefixes.prefixIndexForName(node.name) + 1
                });

                return graph

            }

        };


        function setDefaultValue(value, defaultValue) {
            return (value === undefined) ? defaultValue : value;
        }

    </script>
    <script type="text/javascript">
        let dvconfig = {
            create: function () {
                return {
                    default_link_distance: 10,

                    // How far can we change default_link_distance?
                    // 0   - I don't care
                    // 0.5 - Change it as you want, but it's preferrable to have default_link_distance
                    // 1   - One does not change default_link_distance
                    default_link_strength: 0.7,

                    // Should I comment this?
                    default_circle_radius: 15,

                    // you can set it to true, but this will not help to understanf what's going on
                    show_texts_near_circles: false,

                    default_max_texts_length: 100,

                    charge_multiplier: 200
                }
            }
        };

    </script>
    <script type="text/javascript">
        let dvvisualizer = {
            version: "0.0.1",

            create: function (_svg, _config, _d3graph) {
                var visualizer = {};
                Object.assign(visualizer, {
                    config: _config,
                    svg: _svg,
                    d3graph: _d3graph,
                    simulation: null,
                    color: null,

                    _link: null,          // d3 selection of all links
                    _node: null,          // d3 selection of all nodes (non-struct)
                    _textNode: null,      // d3 selection of all text nodes
                    _structNode: null,    // d3 selection of all struct nodes
                    objectNodes: null,    // d3 selection for struct and other nodes
                    allNodes: null,       // d3 selection for all Possible nodes

                    updateMarkers: function (size) {
                        function viewBox(x, y, w, h) { return [x + "", y + "", w + "", h + ""].join(" ") }
                        function moveTo(x, y) { return "M" + x + "," + y }
                        function lineTo(x, y) { return "L" + x + "," + y }

                        function arrow(size) {
                            return [
                                moveTo(0, -size),
                                lineTo(size * 2, 0),
                                lineTo(0, size),
                            ].join("")
                        }

                        svg.selectAll("marker")
                            .transition()
                            .attr("viewBox", viewBox(0, -size, size * 2, size * 2))
                            .attr("refX", size * 2)
                            .attr("refY", 0)
                            .attr("markerWidth", size * 2)
                            .attr("markerHeight", size * 2);

                        svg.selectAll("marker path")
                            .transition()
                            .attr("d", arrow(size));
                    },

                    _setupMarkers: function (size) {

                        svg.append("defs").selectAll("marker")
                            .data(["default", "dependency", "dependants"])
                            .enter().append("marker")
                            .attr("id", (d) => d)
                            .attr("orient", "auto")
                            .attr("class", "marker")
                            .append("path");

                        this.updateMarkers(size);
                    },

                    _setupLinks: function () {
                        svg.append("g").selectAll("path")
                            .data(this.d3graph.links)
                            .enter().append("path")
                            .attr("class", "link")
                            .attr("marker-end", "url(#default)")
                            .style("stroke-width", (d) => d);

                        this._link = svg.selectAll("path.link")
                    },

                    _d3graphAllNodes: function () { return this.d3graph.nodes },
                    _d3graphNodes: function (type) { return this.d3graph.nodes.filter(node => node.type === type) },
                    _d3graphNodesSkipped: function (type) { return this.d3graph.nodes.filter(node => node.type !== type) },

                    _setupNodes: function () {

                        svg.append("g").selectAll(".node")
                            .data(this._d3graphNodesSkipped("struct"))
                            .enter()
                            .append("circle")
                            .attr("class", "node")
                            .attr("r", this._radius)
                            .style("stroke-dasharray", d => d.type === "protocol" ? [5, 5] : "")  // TODO: Move to styling
                            .style("stroke-width", d => d.type === "protocol" ? 5 : 1);           // TODO: Move to styling

                        svg.append("g").selectAll(".structNode")
                            .data(this._d3graphNodes("struct"))
                            .enter()
                            .append("polygon")
                            .attr("class", "structNode")
                            .attr("points", this._structurePoints)
                            .style("stroke-width", 1);


                        this.objectNodes = svg.selectAll('.node, .structNode');
                        // Setting up source/ dest and coloring

                        this.objectNodes
                            .style("fill", d => this.color(d.group))
                            .attr("source", d => d.source)
                            .attr("dest", d => d.dest);

                        this._node = svg.selectAll('.node');
                        this._structNode = svg.selectAll('.structNode');

                    }.bind(visualizer),

                    _radius: function (node) {
                        return config.default_circle_radius + config.default_circle_radius * node.source / 10;
                    },

                    _setupSimulation: function () {

                        this.simulation = d3.forceSimulation(d3.values(d3graph.nodes))
                            .force("x", d3.forceX())
                            .force("y", d3.forceY())
                            .force("center", d3.forceCenter(x / 2, y / 2)) // TODO Move to somewhere else?
                            .force("charge", d3.forceManyBody().strength(this._chargeStrength))
                            .force("link", d3.forceLink(d3graph.links)
                                .distance(this._linkDistance)
                                .strength(this._linkStrength)
                            )
                            .on("tick", this._ticked);

                    },

                    _linkDistance: function (link) {
                        if (link.source.filtered || link.target.filtered) {
                            return 500;
                        }
                        return this._radius(link.source) + this._radius(link.target) + this.config.default_link_distance;
                    }.bind(visualizer),

                    _linkStrength: function (link) {
                        if (link.source.filtered || link.target.filtered) {
                            return 0.01;
                        }
                        return config.default_link_strength;
                    },

                    _chargeStrength: function (node) {
                        if (node.filtered) {
                            return -0.01;
                        }
                        return -node.weight * config.charge_multiplier;
                    },

                    _structurePoints: function (d) {
                        let r = this._radius(d);
                        let pts = [
                            { x: -r, y: 0 },
                            { x: -r * 0.707, y: -r * 0.707 },
                            { x: 0, y: -r },
                            { x: r * 0.707, y: -r * 0.707 },
                            { x: r, y: 0 },
                            { x: r * 0.707, y: r * 0.707 },
                            { x: 0, y: r },
                            { x: -r * 0.707, y: r * 0.707 },
                        ];

                        return pts.map(p => p.x + "," + p.y).join(" ")
                    }.bind(visualizer),

                    updateRadiuses: function (value) {

                        this._node.transition().attr("r", this._radius);
                        this._structNode.transition().attr("points", this._structurePoints);

                        this.updateMarkers(value / 3);
                        this.simulation.alphaTarget(0.3).restart()
                    },

                    reapply_charge_and_links: function () {
                        this.reapply_charge();
                        this.reapply_links_strength()
                    },

                    reapply_charge: function (value) {
                        config.charge_multiplier = setDefaultValue(value, config.charge_multiplier);
                        this.simulation.force("charge", d3.forceManyBody().strength(this._chargeStrength));
                        this.simulation.alphaTarget(0.3).restart()
                    },

                    updateTextVisibility: function (visible) {
                        this.config.show_texts_near_circles = visible;
                        this._textNode.attr("visibility", visible ? "visible" : "hidden");
                        this.simulation.alphaTarget(0.3).restart()
                    },

                    reapply_links_strength: function (linkStrength) {
                        config.default_link_strength = setDefaultValue(linkStrength, config.default_link_strength);
                        this.simulation.force("link", d3.forceLink(d3graph.links)
                            .distance(this._linkDistance)
                            .strength(this._linkStrength)
                        );
                        this.simulation.alphaTarget(0.3).restart()
                    },

                    updateCenter: function (x, y) {
                        this.simulation.force("center", d3.forceCenter(x / 2, y / 2))
                    },

                    _setupDragging: function () {
                        let dragstarted = function (d) {
                            if (!d3.event.active) this.simulation.alphaTarget(0.3).restart();
                            d.fx = d.x;
                            d.fy = d.y;
                        }.bind(visualizer);

                        let dragged = function (d) {
                            d.fx = d3.event.x;
                            d.fy = d3.event.y;
                        }.bind(visualizer);

                        let dragended = function (d) {
                            if (!d3.event.active) this.simulation.alphaTarget(0);
                            if (!d.fixed) {
                                d.fx = null;
                                d.fy = null;
                            }
                        }.bind(visualizer);

                        this.objectNodes
                            .call(d3.drag()
                                .on("start", dragstarted)
                                .on("drag", dragged)
                                .on("end", dragended));
                    },

                    _setupTexts: function () {
                        svg.append("g").selectAll("text")
                            .data(this.simulation.nodes())
                            .enter()
                            .append("text")
                            .attr("visibility", "hidden")
                            .text(d => d.name.substring(0, this.config.default_max_texts_length));

                        this._textNode = svg.selectAll("text");
                    },

                    _link_line: function (d) {
                        const dx = d.target.x - d.source.x,
                            dy = d.target.y - d.source.y,
                            dr = Math.sqrt(dx * dx + dy * dy);

                        if (dr === 0) { return "M0,0L0,0" }

                        const rsource = this._radius(d.sourceNode) / dr;
                        const rdest = this._radius(d.targetNode) / dr;
                        const startX = d.source.x + dx * rsource;
                        const startY = d.source.y + dy * rsource;

                        const endX = d.target.x - dx * rdest;
                        const endY = d.target.y - dy * rdest;
                        return "M" + startX + "," + startY + "L" + endX + "," + endY;
                    }.bind(visualizer),

                    setupZoom: function (container) {
                        const w = window,
                            d = document,
                            e = d.documentElement,
                            g = d.getElementsByTagName('body')[0],
                            x = w.innerWidth || e.clientWidth || g.clientWidth,
                            y = w.innerHeight || e.clientHeight || g.clientHeight;

                        const zoom = d3.zoom()
                            .on("zoom", function () { svg.attr("transform", d3.event.transform) });

                        container.append("rect")
                            .attr("width", x)
                            .attr("height", y)
                            .style("fill", "none")
                            .style("pointer-events", "all")
                            .lower()
                            .call(zoom);
                    },

                    _transform: function (d) {
                        return "translate(" + d.x + "," + d.y + ")";
                    },


                    _ticked: function () {
                        this._link.attr("d", this._link_line);
                        this._node.attr("transform", this._transform);
                        this._structNode.attr("transform", this._transform);
                        if (config.show_texts_near_circles) {
                            this._textNode.attr("transform", this._transform);
                        }
                    }.bind(visualizer),

                    _setupColors: function () {
                        // https://github.com/mbostock/d3/wiki/Ordinal-Scales#categorical-colors
                        this.color = d3.scaleOrdinal(d3.schemeCategory10);
                    },

                    _setupAllNodes: function () {
                        this.allNodes = svg.select(".node, .structNode, text")
                    },
                    initialize: function () {
                        this._setupColors();
                        this._setupMarkers(this.config.default_circle_radius / 3);
                        this._setupLinks();
                        this._setupNodes();
                        this._setupSimulation();
                        this._setupTexts();
                        this._setupDragging();
                        this._setupAllNodes();
                    }
                });
                return visualizer

            }
        };


        function setDefaultValue(value, defaultValue) {
            return (value === undefined) ? defaultValue : value;
        }
    </script>
    <script>

        //  ===================================================
        //  =============== CONFIGURABLE PARAMS  ==============
        //  ===================================================

        let config = dvconfig.create();

        const dvgraph = objcdv.parse_dependencies_graph(dependencies);
        const d3graph = dvgraph.d3jsGraph();

        var w = window,
            d = document,
            e = d.documentElement,
            g = d.getElementsByTagName('body')[0],
            x = w.innerWidth || e.clientWidth || g.clientWidth,
            y = w.innerHeight || e.clientHeight || g.clientHeight;

        //  ===================================================
        //  =============== http://d3js.org/ Magic ===========
        //  ===================================================

        const container = d3.select("#chart").append("svg")
            .attr("width", x)
            .attr("height", y)
            .style("overflow", "hidden");

        const svg = container.append('g');
        const actions = graph_actions.create(svg, dvgraph);
        let visualizer = dvvisualizer.create(svg, config, d3graph);
        visualizer.initialize();
        visualizer.setupZoom(container);

        //  ===================================================
        //  ===============  NODES SETUP     ==================
        //  ===================================================

        // Handling pressing
        visualizer.objectNodes
            .on("click", d => {
                if (d3.event.defaultPrevented) { return }
                actions.selectNodesStartingFromNode(d, 1);
                visualizer.reapply_charge_and_links()
            })
            .on("contextmenu", d => {
                if (d3.event.defaultPrevented) { return }
                // Don't actually show context menu
                d3.event.preventDefault();

                actions.selectNodesStartingFromNode(d);
                visualizer.reapply_charge_and_links()
            });


        /*
         Window resize update
         */
        w.onresize = () => {
            x = w.innerWidth || e.clientWidth || g.clientWidth;
            y = w.innerHeight || e.clientHeight || g.clientHeight;

            container.attr("width", Math.ceil(x)).attr("height", Math.ceil(y));
            visualizer.updateCenter(x / 2, y / 2);
        };
    </script>

    <script>
        //  ===================================================
        //  =============== INPUTS HANDLING      ==============
        //  ===================================================
        d3.selectAll("input").on("change", function change() {

            if (this.name === "circle_size") {
                config.default_circle_radius = parseInt(this.value);
                visualizer.updateRadiuses(parseInt(this.value));
            }

            if (this.name === "charge_multiplier") {
                let chargeMultiplier = parseInt(this.value);
                visualizer.reapply_charge(chargeMultiplier)
            }

            if (this.name === "link_strength") {
                let linkStrength = parseInt(this.value) / 10;
                visualizer.reapply_links_strength(linkStrength)
            }

            if (this.name === "show_texts_near_circles") {
                visualizer.updateTextVisibility(this.checked)
            }
        });
    </script>

    <script>
        //  ===================================================
        //  =============== LIVE FILTERING      ==============
        //  ===================================================

        function live_filter_graph(regexp, classname, invert) {
            classname = setDefaultValue(classname, "filtered");
            invert = setDefaultValue(invert, false);

            const re = new RegExp(regexp, "i");
            visualizer.allNodes
                .classed(classname, node => {
                    let filtered = !node.name.match(re);
                    filtered = invert ? !filtered : filtered;
                    node.filtered = filtered;
                    node.neighbours = !filtered;
                    return filtered;
                })
                .transition();

            svg.selectAll('.link')
                .classed(classname, l => {
                    let filtered = !(l.sourceNode.name.match(re) && l.targetNode.name.match(re));
                    filtered = invert ? !filtered : filtered;
                    return filtered;
                })
                .attr("marker-end", l => {
                    let filtered = !(l.sourceNode.name.match(re) && l.targetNode.name.match(re));
                    filtered = invert ? !filtered : filtered;
                    return filtered ? "" : "url(#default)"
                })
                .transition()
        }

        d3.select("#search_input").on("input", function () {
            // Filter all items
            console.log("Input changed to" + this.value);
            actions.deselect_selected_node();

            if (this.value && this.value.length) {
                live_filter_graph(this.value, "filtered");
            }
            visualizer.reapply_charge_and_links();
        });

        d3.select('#export').on('click', function() {
        var config = {
            filename: 'deps',
        }
        d3_save_svg.save(d3.select('svg').node(), config);
        });
    </script>