D3 Adding arrowheads to edges, directed graph

I would like to add directionality to my D3 graph. The json will have an attribute called "direction" and it will either be "--", "<-", or "->" meaning:

source <- target (arrow from target to source)
source -> target (arrow from source to target)
source -- target (no direction)

I would like it to match the color of the links if possible (not crucial) Any assistance would be welcome!

My script below

<!DOCTYPE html>
<meta charset="utf-8">
<style>
    .node {
        stroke: #fff;
        stroke-width: 1.5px;
    }

    .link {
        stroke: #999;
        stroke-opacity: .6;
    }

    .axis {
        opacity: 0.5;
        font: 10px sans-serif;
        -webkit-user-select: none;
        -moz-user-select: none;
        user-select: none;
    }

    .axis .domain {
        fill: none;
        stroke: #000;
        stroke-opacity: .3;
        stroke-width: 4px;
        stroke-linecap: round;
    }

    .axis .halo {
        fill: none;
        stroke: #ddd;
        stroke-width: 3px;
        stroke-linecap: round;
    }

    text {
        pointer-events: none;
        font: 8px sans-serif;
        stroke: none;
        fill: black;
    }

    .slider .handle {
        fill: #fff;
        stroke: #000;
        stroke-opacity: .5;
        stroke-width: 1.25px;
        cursor: grab;
    }

    div.tooltip {
        position: absolute;
        text-align: center;
        width: 60px;
        height: 28px;
        padding: 2px;
        font: 12px sans-serif;
        background: lightsteelblue;
        border: 0px;
        border-radius: 8px;
        pointer-events: none;
    }
</style>

<body>
    <script src="https://d3js.org/d3.v3.min.js"></script>
    <script>
        var width = 960,
            height = 500;

        var color = d3.scale.category20();

        var force = d3.layout.force()
            .charge(-120)
            .linkDistance(30)
            .size([width, height]);

        var x = d3.scale.linear()
            .domain([0, 20])
            .range([250, 80])
            .clamp(true);

        var brush = d3.svg.brush()
            .y(x)
            .extent([0, 0]);

        var svg = d3.select("body").append("svg")
            .attr("width", width)
            .attr("height", height);

        var links_g = svg.append("g");

        var nodes_g = svg.append("g");

        // Define the div for the tooltip
        var div = d3.select("body").append("div")
            .attr("class", "tooltip")
            .style("opacity", 0);

        svg.append("g")
            .attr("class", "x axis")
            .attr("transform", "translate(" + (width - 20) + ",0)")
            .call(d3.svg.axis()
                .scale(x)
                .orient("left")
                .tickFormat(function(d) {
                    return d;
                })
                .tickSize(0)
                .tickPadding(12))
            .select(".domain")
            .select(function() {
                return this.parentNode.appendChild(this.cloneNode(true));
            })
            .attr("class", "halo");

        var slider = svg.append("g")
            .attr("class", "slider")
            .call(brush);

        slider.selectAll(".extent,.resize")
            .remove();

        var handle = slider.append("circle")
            .attr("class", "handle")
            .attr("transform", "translate(" + (width - 20) + ",0)")
            .attr("r", 5);

        svg.append("text")
            .attr("x", width - 15)
            .attr("y", 60)
            .attr("text-anchor", "end")
            .attr("font-size", "12px")
            .style("opacity", 0.5)
            .text("degree threshold")

        d3.json("test.json", function(error, graph) {
            if (error) throw error;

            graph.links.forEach(function(d, i) {d.i = i;});
            graph.nodes.forEach(function(d, i) {d.i = i;});

            function brushed() {
                var value = brush.extent()[0];

                if (d3.event.sourceEvent) {
                    value = x.invert(d3.mouse(this)[1]);
                    brush.extent([value, value]);
                }

                handle.attr("cy", x(value));
                var threshold = value;

                var thresholded_links = graph.links.filter(function(d) {
                    return (d.max_degree > threshold);
                });

                force
                    .links(thresholded_links);

                var link = links_g.selectAll(".link")
                    .data(thresholded_links, function(d) {
                        return d.i;
                    });

                link.enter().append("line")
                    .attr("class", "link")
                    .style("stroke-width", function(d) {
                        return Math.sqrt(d.value);
                    });

                link.exit().remove();


                force.on("tick", function() {
                    link.attr("x1", function(d) {return d.source.x;})
                        .attr("y1", function(d) {return d.source.y;})
                        .attr("x2", function(d) {return d.target.x;})
                        .attr("y2", function(d) {return d.target.y;});

                    node.attr("transform", function(d) {
                        return "translate(" + d.x + "," + d.y + ")";
                    });


                });

                force.start();
                //console.log(link);
                link.each(function(d) {
                    d3.select(this).style("stroke", d.min_degree >= value ? "#3182bd" : "#ccc")
                });

                node.each(function(d) {
                    d3.select(this).select("circle").style("fill", d.degree > value ? color(1) : d.weight > 0 ? color(2) : "#ccc")
                });

                node.each(function(d) {
                    d3.select(this).select("text").style("fill", d.degree > value ? color(1) : d.weight > 0 ? color(2) : "#ccc")
                });


            }

            force
                .nodes(graph.nodes);

            var node = nodes_g.selectAll(".node")
                .data(graph.nodes)
                .enter()
                .append('g')
                .attr("class", "node")
                .call(force.drag);

            node.append("circle")
                .attr("r", 5)
                .style("fill", function(d) {
                    return color(1);
                })
                .on("mouseover", function(d) {
                    div.transition()
                        .duration(200)
                        .style("opacity", .9);
                    div.html(d.name + "\ndegree:" + d.degree)
                        .style("left", (d3.event.pageX) + "px")
                        .style("top", (d3.event.pageY - 28) + "px");
                })
                .on("mouseout", function(d) {
                    div.transition()
                        .duration(500)
                        .style("opacity", 0);
                });

            node.append("text")
                .attr("dx", 4)
                .attr("dy", ".35em")
                .text(function(d) {
                    return d.name;
                });


            brush.on("brush", brushed);

            slider
                .call(brush.extent([16.5, 16.5]))
                .call(brush.event);
        });
    </script>

Here is a sample json: test.json

1 answer

  • answered 2017-11-15 02:41 LaissezPasser

    I implemented something like what I think you're trying to achieve in a geojson sankey. Maybe you can use some of the code. Here's a link to the block.

    And part of the relevant code:

           var arrow = L.polylineDecorator(line, { patterns: [{
                    offset: '55%', 
                    symbol: L.Symbol.arrowHead({
                        polygon: true, 
                        pathOptions: {
                            weight: lineWeight,
                            color: lineColor
                        }
                    })
                }]});
    

    Hope this helps.