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="../common/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.v4.min.js" type="text/javascript"></script>
<script src="../common/stardust/stardust.bundle.js" type="text/javascript"></script>
<script src="../common/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);
    platform.pixelRatio = window.devicePixelRatio || 1;

    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("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.forceSimulation()
            .force("link", d3.forceLink().id(function (d) { return d.index }))
            .force("charge", d3.forceManyBody())
            .force("forceX", d3.forceX(width / 2))
            .force("forceY", d3.forceY(height / 2))

        force.nodes(nodes);
        force.force("link").links(edges);

        force.force("forceX").strength(0.5);
        force.force("forceY").strength(0.5);
        force.force("link").distance(50);
        force.force("link").strength(0.05);
        force.force("charge").strength(-40);


        function makeSlider(name, attr, min, max, defaultValue) {
            d3.select(".sliders").append("label").text(name);
            var slider = d3.select(".sliders").append("input");
            slider
                .attr("type", "range")
                .attr("min", 0)
                .attr("max", 1000)
                .attr("value", (defaultValue - min) / (max - min) * 1000)
            slider.on("input", () => {
                var val = +slider.property("value");
                var d = val / 1000 * (max - min) + min;
                if(attr == "gravity") {
                    force.force("forceX").strength(d);
                    force.force("forceY").strength(d);
                }
                if(attr == "linkDistance") {
                    force.force("link").distance(d);
                }
                if(attr == "linkStrength") {
                    force.force("link").strength(d);
                }
                if(attr == "charge") {
                    force.force("charge").strength(d);
                }
                force.alphaTarget(0.3).restart();
            });
        }

        // 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", () => {
            if (isDragging && selectedNode && draggingLocation) {
                selectedNode.x = draggingLocation[0];
                selectedNode.y = draggingLocation[1];
            }
            positions.data(nodes);
            requestRender();
        });

        requestRender();

        // Handle dragging

        let selectedNode = null;

        var requested = null;
        function requestRender() {
            if (requested) return;
            requested = requestAnimationFrame(render);
        }

        var fps = new FPS();

        function render() {
            requested = null;
            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;
        var draggingLocation = null;
        // 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 * platform.pixelRatio, y * platform.pixelRatio);
            if (p) {
                selectedNode = nodes[p[1]];
                requestRender();
                isDragging = true;
                draggingLocation = [selectedNode.x, selectedNode.y];
                var onMove = function (e) {
                    var nx = e.clientX - canvas.getBoundingClientRect().left;
                    var ny = e.clientY - canvas.getBoundingClientRect().top;
                    selectedNode.x = nx;
                    selectedNode.y = ny;
                    draggingLocation = [nx, ny];
                    force.alphaTarget(0.3).restart();
                    requestRender();
                };
                var onUp = function () {
                    window.removeEventListener("mousemove", onMove);
                    window.removeEventListener("mouseup", onUp);
                    selectedNode = null;
                    draggingLocation = 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 * platform.pixelRatio, y * platform.pixelRatio);
            if (p) {
                if (selectedNode != nodes[p[1]]) {
                    selectedNode = nodes[p[1]];
                    requestRender();
                }
            } else {
                if (selectedNode != null) {
                    selectedNode = null;
                    requestRender();
                }
            }
        }
    });

</script>