pir
pir

Reputation: 5923

Make patch of line different color in D3

My visualization uses lines between nodes to indicate their relationship. The wider the line, the more connected the two nodes are. In this context I would like to set a small section of the lines to a darker hue to attach more information to the visualization. Whichever node is closest to the "mark" on the line is the dominant node.

Is there some command to change the color for only part of the line? Or would it be easier to create a rectangle with the same width and angle as the line?

I've attached the current visualization, where I've created some ugly little rectangles to illustrate what I'm trying to do. Also, the code is attached (although it sadly is a bit messy).

enter image description here

<!DOCTYPE html>
<meta charset="utf-8">
<style>

.node circle {
  stroke: white;
  stroke-width: 2px;
  opacity: 1.0;
}

line {
  stroke-width: 3.5px;
  stroke-opacity: 1.0;
}

</style>
<body>
<script src="http://d3js.org/d3.v3.js"></script>
<script>

data = {
        nodes: [
                  {size: 200, fill: '#ffffff', line: '#8C8C8C', id: 'Me'}, 
                  {size: 30, fill: '#D2D2D2', line: '#ffffff', id: 'Bob'},
                  {size: 30, fill: '#D2D2D2', line: '#ffffff', id: 'Alice'},
                  {size: 30, fill: '#D2D2D2', line: '#ffffff', id: 'Tim Tim'},
                  {size: 30, fill: '#D2D2D2', line: '#ffffff', id: 'Mustafa'},
                  {size: 30, fill: '#D2D2D2', line: '#ffffff', id: 'Zeus'},
                  {size: 30, fill: '#D2D2D2', line: '#ffffff', id: 'McHammer'},
                  {size: 30, fill: '#D2D2D2', line: '#ffffff', id: 'Thoooor'},
                  {size: 30, fill: '#D2D2D2', line: '#ffffff', id: 'Le Viking'}
                  ],
        links: [
                  {source: 0,target: 1, calls: 20, texts:0},
                  {source: 0,target: 2, calls: 5, texts:0},
                  {source: 0,target: 3, calls: 8, texts:0},
                  {source: 0,target: 4, calls: 3, texts:0},
                  {source: 0,target: 5, calls: 2, texts:0},
                  {source: 0,target: 6, calls: 3, texts:0},
                  {source: 0,target: 7, calls: 5, texts:0},
                  {source: 0,target: 8, calls: 2, texts:0}
                ]
        }

var total_interactions = data.links.reduce(function(result, currentObject) {
  return result + currentObject.calls;
}, 0);
console.log(total_interactions);

var mouseOverFunction = function(d) {
  var circle = d3.select(this);

  circle
    .transition(500)
      .attr("r", function(){ return 1.4 * node_radius(d)});
}

var mouseOutFunction = function() {
  var circle = d3.select(this);

  circle
    .transition(500)
      .attr("r", node_radius);
}

function isConnected(a, b) {
    return isConnectedAsTarget(a, b) || isConnectedAsSource(a, b) || a.index == b.index;
}

function isConnectedAsSource(a, b) {
    return linkedByIndex[a.index + "," + b.index];
}

function isConnectedAsTarget(a, b) {
    return linkedByIndex[b.index + "," + a.index];
}

function isEqual(a, b) {
    return a.index == b.index;
}

var line_diff = 0

function tick() {
  callLink
    .attr("x1", function(d) { return d.source.x-line_diff; })
    .attr("y1", function(d) { return d.source.y-line_diff; })
    .attr("x2", function(d) { return d.target.x-line_diff; })
    .attr("y2", function(d) { return d.target.y-line_diff; });
  textLink
    .attr("x1", function(d) { return d.source.x+line_diff; })
    .attr("y1", function(d) { return d.source.y+line_diff; })
    .attr("x2", function(d) { return d.target.x+line_diff; })
    .attr("y2", function(d) { return d.target.y+line_diff; });

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

function node_radius(d) { return Math.pow(40.0 * d.size, 1/3); } 

var width = 1000;
var height = 500;

var nodes = data.nodes
var links = data.links

var force = d3.layout.force()
              .nodes(nodes)
              .links(links)
              .charge(-3000) // -3000
              .friction(0.6) 
              .gravity(0.6)
              .size([width,height])
              .start();

var linkedByIndex = {};
links.forEach(function(d) {
  linkedByIndex[d.source.index + "," + d.target.index] = true;
});

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

var callLink = svg.selectAll(".call-line")
            .data(links)
            .enter().append("line");
var textLink = svg.selectAll(".text-line")
            .data(links)
            .enter().append("line");
var link = svg.selectAll("line");

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

node
  .append("circle")
  .attr("r", node_radius)
  .style("fill", function(d)  {return d.fill; })
  .style("stroke", function(d)  {return d.line; })
  .on("mouseover", mouseOverFunction)
  .on("mouseout", mouseOutFunction);

svg
  .append("marker")
  .attr("id", "arrowhead")
  .attr("refX", 6 + 7)
  .attr("refY", 2)
  .attr("markerWidth", 6)
  .attr("markerHeight", 4)
  .attr("orient", "auto")
  .append("path")
  .attr("d", "M 0,0 V 4 L6,2 Z");

// link
  // .style("stroke-width", function stroke(d)  {return d.calls/data.total_interactions*20; })

 var line_width_factor = 40 // width for a line with all points

 textLink
  .style("stroke-width", function stroke(d)  {return d.texts / total_interactions*line_width_factor; })
  .style("stroke", "#70C05A")

 callLink
  .style("stroke-width", function stroke(d)  {return d.calls / total_interactions*line_width_factor; })
  .style("stroke", "#438DCA")

force
  .on("tick",tick);

</script>
</body>

Upvotes: 0

Views: 1050

Answers (1)

ahmohamed
ahmohamed

Reputation: 2970

In fact, you can't apply CSS gradients to SVG elements. You have to use SVG gradients.

To create an SVG gradient along a link in a graph, the general form would be:

defs.append("linearGradient")                
        .attr("id", gradient_id)
        .attr("gradientUnits", "userSpaceOnUse")
        .attr("x1", source.px)
        .attr("y1", source.py)
        .attr("x2", target.px)
        .attr("y2", target.py)    
    .selectAll("stop")                      
        .data([                             
            {offset: "0%", color: "cyan"},   //color 1
            {offset: "50%", color: "cyan"},  //start the gradient from haf distance
            {offset: "100%", color: "darkBlue"} //to a darker color
        ])                  
    .enter().append("stop")         
        .attr("offset", function(d) { return d.offset; })   
        .attr("stop-color", function(d) { return d.color; });   

In your case, the positions of source and target are changing with each force.tick(). So, you need to calculate and apply gradients for all links in each tick. By adding this line to the tick function:

callLink.each(function(d){applyGradient(this, d)});

Next, we need to define applyGradient(line, d) function:

function applyGradient(line, d){
    var self = d3.select(line); //select the current link

    // store the currently used gradient to delete it later.
    var current_gradient = self.style("stroke")
    current_gradient = current_gradient.substring(4, current_gradient.length-1);

    // Generate a random id for the gradient calculated for this link
    var new_gradient_id = "line-gradient" + getRandomInt();

    // Determine the direction of the gradient. In your example, you need
    // the darker hue to be near the bigger node.
    var from = d.source.size < d.target.size? d.source: d.target;
    var to = d.source.size < d.target.size? d.target: d.source;


    // Add the gradient to the svg:defs
    defs.append("linearGradient")                
        .attr("id", new_gradient_id)
        .attr("gradientUnits", "userSpaceOnUse")
        .attr("x1", from.px)
        .attr("y1", from.py)
        .attr("x2", to.px)
        .attr("y2", to.py)    
    .selectAll("stop")                      
        .data([                             
            {offset: "0%", color: "cyan"},       
            {offset: "50%", color: "cyan"},         
            {offset: "100%", color: "darkBlue"}
        ])                  
    .enter().append("stop")         
        .attr("offset", function(d) { return d.offset; })   
        .attr("stop-color", function(d) { return d.color; });   

    //apply the gradient
    self.style("stroke", "url(#" + new_gradient_id + ")")

    //Remove the old gradient from defs.
    defs.select(current_gradient).remove();
}

Here is a working fiddle

Upvotes: 1

Related Questions