mareoraft
mareoraft

Reputation: 3912

Variable position of svg defs def in d3

I am using a d3.js force-directed-graph. It has nodes and links connecting them. To create an arrow head, I use the svg and d3 combined like so:

    gA.svg.append('defs').selectAll('marker').data(['arrow-head']) // binding 'arrow-head' is our way of attaching a name!
        .enter().append('marker')
            .classed('arrow-head', true)
            .attr({
                id: String,
                viewBox: '0 -5 10 10',
                refX: 24, // this controls the distance of the arrowhead from the end of the line segment!
                refY: 0,
                markerWidth: 5,
                markerHeight: 5,
                orient: 'auto',
            })
            .append('path')
                .attr('d', 'M0,-5L10,0L0,5')

To attach that arrowhead to the end of each edge/link, I do:

    let link = gA.svg.selectAll('.link').data(gA.force.links(), link => gA.node_id(link.source) + '--->' + gA.node_id(link.target)) // links before nodes so that lines in SVG appear *under* nodes
    link.enter().append('line')
        .classed('link', true)
        .attr('marker-end', 'url(#arrow-head)') // add in the marker-end defined above
    link.exit().remove()

What is the best way to make the distance of the arrowhead from the end of the line segment variable? The purpose of this is to have the arrowhead positioned at the edge of each node circle, as opposed to in the middle of the node. It would be preferable not to make a separate SVG def for each possible distance :)

I was thinking something like:

.attr('marker-end', var_arrow_head)

function var_arrow_head(link){
    radius = link.target.radius
    refX = 2 + 5*radius
    magically get an altered version of arrow-head with the new refX value
    return 'url(#magic-arrow-head)'
}

Upvotes: 0

Views: 631

Answers (2)

mareoraft
mareoraft

Reputation: 3912

I am using plain old straight lines/edges/links. So the solution for me is simpler than the solutions on the other SO page:

function _tick(){
    gA.svg.selectAll('.node')
        .attr({
            transform: node => 'translate('+node.x+','+node.y+')',
        })
    gA.svg.selectAll('.link')
        .attr({
            x1: link => endpointLessRadius(link, 'x1'),
            y1: link => endpointLessRadius(link, 'y1'),
            x2: link => endpointLessRadius(link, 'x2'),
            y2: link => endpointLessRadius(link, 'y2'),
        })
}
function endpointLessRadius(link, attr_name) { // subtract radius away from line ends
    let x1 = link.source.x
    let y1 = link.source.y
    let x2 = link.target.x
    let y2 = link.target.y

    let distance = Math.sqrt( Math.pow(x1-x2, 2) + Math.pow(y1-y2, 2) )
    let radius1 = gA.node_radius(link.source)
    let radius2 = gA.node_radius(link.target)

    if( attr_name === 'x1' ) return x1 + (x2-x1) * radius1/distance
    if( attr_name === 'y1' ) return y1 + (y2-y1) * radius1/distance
    if( attr_name === 'x2' ) return x2 + (x1-x2) * radius2/distance
    if( attr_name === 'y2' ) return y2 + (y1-y2) * radius2/distance
}

Upvotes: 1

Gilsha
Gilsha

Reputation: 14589

It seems that you want to place the marker at the end of each line but the radius of the nodes are variable. In such cases, I would suggest you to use a single marker definition in the defs and just update the length of links according to the variable node radius.

This stackoverflow question contains the possible answers - linking nodes of variable radius with arrows

Upvotes: 1

Related Questions