Home Examples Documentation Playground Source

Stardust Example: Force-directed Graph

Force-directed graph visualization using D3 for layout and Stardust for rendering.

index.html

<!DOCTYPE html>
<meta charset="utf-8">
<link rel="stylesheet" href="../static/assets/style.css" type="text/css" />
<style type="text/css">
    .fps { position: fixed; top: 0; right: 0; padding: 10px; margin: 0; font-size: 15px; }
    .sliders {
        position: absolute;
        left: 710px;
        width: 220px;
        top: 50px; bottom: 0;
    }
    .sliders label { display: block; font-weight: bold; }
    .sliders input { display: block; width: 100%; margin: 5px 0; }
</style>
<canvas id="main-canvas"></canvas>
<div class="sliders"></div>
<div class="fps"></div>
<div class="initializing"><p>Initializing...</p></div>
<script src="//d3js.org/d3.v3.min.js" type="text/javascript"></script>
<script src="../static/stardust/stardust.bundle.js" type="text/javascript"></script>
<script src="../static/assets/utils.js" type="text/javascript"></script>
<script type="text/javascript">
    var canvas = document.getElementById("main-canvas");
    var width = 700;
    var height = 500;
    var platform = Stardust.platform("webgl-2d", canvas, width, height);

    var snodes = Stardust.mark.create(Stardust.mark.circle(8), platform);
    var snodesBG = Stardust.mark.create(Stardust.mark.circle(8), platform);
    var snodesSelected = Stardust.mark.create(Stardust.mark.circle(8), platform);
    var sedges = Stardust.mark.create(Stardust.mark.line(), platform);

    loadData("facebook_1912_clusters.json", (data) => {
        var nodes = data.nodes;
        var edges = data.edges;
        var N = nodes.length;

        for(var i = 0; i < N; i++) {
            nodes[i].index = i;
            nodes[i].x = Math.random() * width;
            nodes[i].y = Math.random() * height;
        }

        let colors = [[31,119,180],[255,127,14],[44,160,44],[214,39,40],[148,103,189],[140,86,75],[227,119,194],[127,127,127],[188,189,34],[23,190,207]];
        colors = colors.map((x) => [ x[0] / 255, x[1] / 255, x[2] / 255, 1 ]);

        snodes
            .attr("radius", 2)
            .attr("color", d => colors[d.cluster]);
        snodesBG
            .attr("radius", 3)
            .attr("color", [ 1, 1, 1, 0.5 ]);

        snodesSelected
            .attr("radius", 4)
            .attr("color", [ 228/255, 26/255, 28/255, 1 ]);

        sedges
            .attr("p1", (d) => [ d.source.x, d.source.y ])
            .attr("p2", (d) => [ d.target.x, d.target.y ])
            .attr("width", 0.5)
            .attr("color", d => {
                if(d.source.cluster == d.target.cluster) return colors[d.source.cluster].slice(0, 3).concat([ 0.1 ]);
                return [ 0.5, 0.5, 0.5, 0.1 ]
            });

        var force = d3.layout.force()
            .size([ width, height ])
            .nodes(nodes)
            .links(edges);

        force.linkStrength(0.05);
        force.friction(0.5);
        force.gravity(0.2);
        force.charge(-40);
        force.linkDistance(50);
        force.start();

        function makeSlider(name, attr, min, max, defaultValue) {
            d3.select(".sliders").append("label").text(name);
            var slider = d3.select(".sliders").append("input").attr({
                type: "range",
                min: 0,
                max: 1000,
                value: (defaultValue - min) / (max - min) * 1000
            });
            slider.on("input", () => {
                var val = +slider.property("value");
                var d = val / 1000 * (max - min) + min;
                force[attr](d);
                force.start();
            });
        }

        makeSlider("Friction", "friction", 0.01, 1, 0.5);
        makeSlider("Gravity", "gravity", 0.01, 1, 0.5);
        makeSlider("Charge", "charge", -200, 0, -40);
        makeSlider("Link Distance", "linkDistance", 0, 200, 50);
        makeSlider("Link Strength", "linkStrength", 0, 0.2, 0.05);

        var positions = Stardust.array("Vector2")
            .value(d => [ d.x, d.y ])
            .data(nodes);

        var positionScale = Stardust.scale.custom("array(pos, value)")
            .attr("pos", "Vector2Array", positions)
        snodesSelected.attr("center", positionScale(d => d.index));
        snodes.attr("center", positionScale(d => d.index));
        snodesBG.attr("center", positionScale(d => d.index));
        sedges.attr("p1", positionScale(d => d.source.index));
        sedges.attr("p2", positionScale(d => d.target.index));

        snodesBG.data(nodes);
        snodes.data(nodes);
        sedges.data(edges);

        force.on("tick", () => {
            force.start();
            positions.data(nodes);
            requestRender();
        });

        // Handle dragging

        let selectedNode = null;

        function requestRender() {
            requestAnimationFrame(render);
        }

        var fps = new FPS();

        function render() {
            snodesSelected.data(selectedNode ? [ selectedNode ] : []);

            // Cleanup and re-render.
            platform.clear([ 1, 1, 1, 1 ]);
            sedges.render();
            snodesBG.render();
            snodes.attr("radius", 2);
            snodes.render();

            snodesSelected.render();

            // Render the picking buffer.
            platform.beginPicking(canvas.width, canvas.height);
            snodes.attr("radius", 6); // make radius larger so it's easier to select.
            snodes.render();
            platform.endPicking();

            fps.update();
        }

        var isDragging = false;
        // Handle dragging.
        canvas.onmousedown = function(e) {
            var x = e.clientX - canvas.getBoundingClientRect().left;
            var y = e.clientY - canvas.getBoundingClientRect().top;
            var p = platform.getPickingPixel(x * 2, y * 2);
            if(p) {
                selectedNode = nodes[p[1]];
                requestRender();
                isDragging = true;
                var onMove = function(e) {
                    var nx = e.clientX - canvas.getBoundingClientRect().left;
                    var ny = e.clientY - canvas.getBoundingClientRect().top;
                    nodes[p[1]].x = nx;
                    nodes[p[1]].y = ny;
                    // requestRender();
                };
                var onUp = function() {
                    window.removeEventListener("mousemove", onMove);
                    window.removeEventListener("mouseup", onUp);
                    selectedNode = null;
                    isDragging = false;
                };
                window.addEventListener("mousemove", onMove);
                window.addEventListener("mouseup", onUp);
            }
        };

        canvas.onmousemove = function(e) {
            if(isDragging) return;
            var x = e.clientX - canvas.getBoundingClientRect().left;
            var y = e.clientY - canvas.getBoundingClientRect().top;
            var p = platform.getPickingPixel(x * 2, y * 2);
            if(p) {
                if(selectedNode != nodes[p[1]]) {
                    selectedNode = nodes[p[1]];
                    requestRender();
                }
            } else {
                if(selectedNode != null) {
                    selectedNode = null;
                    requestRender();
                }
            }
        }
    });
</script>