I'm trying to make some modifications to a path, defined using D3 programmatically. The change I want to make is quite simple, modifying the opacity of the path. The problem I've got is while the path itself will change, the end marker does not, and I'm not quite sure how to make it do so.
The marker is defined as so:
// define arrow markers for graph links
svg.append('svg:defs').append('svg:marker')
.attr('id', 'end-arrow')
.attr('viewBox', '0 -5 10 10')
.attr('refX', 6)
.attr('markerWidth', 3)
.attr('markerHeight', 3)
.attr('orient', 'auto')
.append('svg:path')
.attr('d', 'M0,-5L10,0L0,5')
.attr('fill', '#CCCCCC');
The path:
// Create the links between the nodes
var links = svg.append("g")
.selectAll(".link")
.data(data.links)
.enter()
.append("path")
.attr("class", "link")
.attr("d", sankey.link())
.style('marker-end', "url(#end-arrow)")
.style("stroke-width", function (d) { return Math.max(1, d.dy); })
.sort(function (a, b) { return b.dy - a.dy; });
The code that I use to change the paths, which doesn't update the markers:
d3.selectAll("path.link")
.filter(function (link) {
// Find all the links that come to/from this node
if (self.sourceLinksMatch(self, link, node)) {
return true;
}
if (self.targetLinksMatch(self, link, node)) {
return true;
}
return false;
})
.transition()
.style("stroke-opacity", 0.5);
Can anyone suggest what I might need to change to modify the marker-end style too?
Modifying the opacity instead of the stroke-opacity works.. so
d3.selectAll("path.link")
.transition()
.style("stroke-opacity", 0.5);
becomes
d3.selectAll("path.link")
.transition()
.style("opacity", 0.5);
You should be able to do the same for the marker path definition:
d3.selectAll("marker path")
.transition()
.style("stroke-opacity", 0.5);
You can set define preset names for your arrow markers
// build the arrow.
svg.append("svg:defs").selectAll("marker")
.data(["HELPS","HELPED_BY","DAMAGES","REPELS","FAMILY", "KINGDOM"]) // Different link/path types can be defined here
.enter().append("svg:marker") // This section adds in the arrows
.attr("id", String)
.attr("viewBox", "0 -5 10 10")
.attr("refX", 15)
.attr("refY", -1.5)
.attr("markerWidth", 6)
.attr("markerHeight", 6)
.attr("orient", "auto")
.append("svg:path")
.attr("d", "M0,-5L10,0L0,5");
// add the links and the arrows
var path = svg.append("svg:g").selectAll("path")
.data(force.links())
.enter().append("svg:path")
.attr("class", function(d) { return "link " + d.type; })
.attr("marker-end", function(d) { return "url(#" + d.type +")"; });
And configure their respective styles with CSS
marker#HELPS{fill:green;}
path.link.HELPS {
stroke: green;
}
marker#HELPED_BY{fill:#73d216;}
path.link.HELPED_BY {
stroke: #73d216;
}
marker#DAMAGES{fill:red;}
path.link.DAMAGES {
stroke: red;
}
Related
I have generated a Sankey diagram as shown above using d3 code (.js file) mentioned below [the .html and .css files are not quoted here].
Now I want the Sankey diagram to look like below with node "Technology" and "Strategy" appearing apart as a fourth level:
What are the necessary changes to be done in the D3 code?
var svg = d3.select("svg").attr("style", "outline: thin solid grey;"),
width = +svg.attr("width"),
height = +svg.attr("height");
var formatNumber = d3.format(",.0f"),
format = function(d) { return formatNumber(d) + " TWh"; },
color = d3.scaleOrdinal(d3.schemeCategory10);
var school = {"nodes": [
{"name":"High School"}, // 0
{"name":"Community College"}, // 1
{"name":"Finance"}, // 2
{"name":"Accounting"}, // 3
{"name":"ITS"}, // 4
{"name":"Marketing"}, // 5
{"name":"Analytics"}, // 6
{"name":"Security"}, // 7
{"name":"Consulting"}, // 8
{"name":"Banking"}, // 9
{"name":"Internal"}, // 10
{"name":"Securities"}, // 11
{"name":"Public"}, // 12
{"name":"Audting"}, // 13
{"name":"Internal"}, // 14
{"name":"Retail"}, // 15
{"name":"Technology"}, // 16
{"name":"Strategy"} // 17
],
"links":[
// FirstYear
{"source":0,"target":2,"value":33},
{"source":0,"target":3,"value":42},
{"source":0,"target":4,"value":74},
{"source":0,"target":5,"value":60},
// Community College
{"source":1,"target":2,"value":7},
{"source":1,"target":3,"value":13},
{"source":1,"target":4,"value":11},
{"source":1,"target":5,"value":9},
// Finance
{"source":2,"target":9,"value":16},
{"source":2,"target":10,"value":14},
{"source":2,"target":11,"value":10},
// Accounting
{"source":3,"target":12,"value":20},
{"source":3,"target":13,"value":12},
{"source":3,"target":7,"value":8},
{"source":3,"target":14,"value":15},
// Marketing
{"source":5,"target":6,"value":30},
{"source":5,"target":15,"value":39},
// ITS
{"source":4,"target":6,"value":40},
{"source":4,"target":7,"value":20},
{"source":4,"target":12,"value":6},
{"source":4,"target":8,"value":19},
// ITS Consulting
{"source":8,"target":16,"value":10},
{"source":8,"target":17,"value":9},
]};
var sankey = d3.sankey()
.nodeWidth(15)
.nodePadding(10)
.extent([[1, 1], [width - 1, height - 6]]);
var link = svg.append("g")
.attr("class", "links")
.attr("fill", "none")
.attr("stroke", "#000")
.attr("stroke-opacity", 0.2)
.selectAll("path");
var node = svg.append("g")
.attr("class", "nodes")
.attr("font-family", "sans-serif")
.attr("font-size", 10)
.selectAll("g");
sankey(school);
link = link
.data(school.links)
.enter().append("path")
.attr("d", d3.sankeyLinkHorizontal())
.attr("stroke-width", function(d) { return Math.max(1, d.width); });
// link hover values
link.append("title")
.text(function(d) { return d.source.name + " → " + d.target.name + "\n" + format(d.value); });
node = node
.data(school.nodes)
.enter().append("g");
node.append("rect")
.attr("x", function(d) { return d.x0; })
.attr("y", function(d) { return d.y0; })
.attr("height", function(d) { return d.y1 - d.y0; })
.attr("width", function(d) { return d.x1 - d.x0; })
.attr("fill", function(d) { return color(d.name.replace(/ .*/, "")); })
.attr("stroke", "#000");
node.append("text")
.attr("x", function(d) { return d.x0 - 6; })
.attr("y", function(d) { return (d.y1 + d.y0) / 2; })
.attr("dy", "0.35em")
.attr("text-anchor", "end")
.text(function(d) { return d.name; })
.filter(function(d) { return d.x0 < width / 2; })
.attr("x", function(d) { return d.x1 + 6; })
.attr("text-anchor", "start");
svg.append("text")
.attr("x", 10)
.attr("y", 30)
.attr("class", "graphTitle")
.text("STUDENT CHOICES");
svg.append("text")
.attr("x", width - 80)
.attr("y", height - 10)
.attr("class", "footnote")
.text("data is fictitious");
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://unpkg.com/d3-sankey#0"></script>
<svg width="600" height="500"></svg>
The alignment of d3.sankey can be configured using .nodeAlign(), and for your requirement, you will need .nodeAlign(d3.sankeyLeft)
If it is not specified, the alignment defaults to d3.sankeyJustify, which is what you are currently seeing.
https://github.com/d3/d3-sankey#alignments
For those who are looking for a quick ans. Have a nice day!
var sankey = d3.sankey()
.nodeWidth(15)
.nodePadding(10)
.nodeAlign(function (node) {
// you may specify the horizatonal location here
// i.e. if your data structure contain node.horizontalPosition (an integer)
// you can return node.horizontalPosition
return node.depth; //align left
})
.extent([[1, 1], [width - 1, height - 6]]);
I am using d3 for visualizing gene networks using a fixed force-directed layout.
The graph contains rectangular / elliptic / round rectangular shaped nodes with markers at the end of links between those nodes. So far (and as I understand) those markers are positioned by refX and refX and thus follow a radial shape around the end of the path which links two nodes. Is there any way that I can define a "path" or marker in such a manner that the marker moves along the shape of the node instead of around this node with a fixed distance relative to the end of the path?
To illustrate my problem:
var graph = {
"nodes": [{
"name": "from",
"fixed": true,
x: 100,
y: 100,
w: 60,
h: 20
}, {
"name": "to",
"fixed": true,
x: 250,
y: 250,
w: 60,
h: 20
}],
"links": [{
"source": 0,
"target": 1
}]
}
var width = 960,
height = 500;
var force = d3.layout.force()
.charge(-120)
.linkDistance(300)
.size([width, height]);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
force.nodes(graph.nodes)
.links(graph.links)
.start();
var defs = svg.append("svg:defs");
var marker = defs.selectAll("marker");
marker = marker.data([{
"type": "arrow",
"d": "M0,-5L10,0L0,5L2,0",
"view": "0 -5 10 10",
"color": "#000000"
}])
.enter()
.append("svg:marker")
.attr("id", function(d) {
return d.type;
})
.attr("viewBox", function(d) {
return d.view;
})
.attr("refX", 30)
.attr("refY", 0)
.attr("markerWidth", 5)
.attr("markerHeight", 5)
.attr("orient", "auto");
marker.append("svg:path")
.attr("d", function(d) {
return d.d;
})
.style("fill", function(d) {
return d.color;
});
var link = svg.selectAll(".link")
.data(graph.links)
.enter().append("line")
.attr("class", "link")
.style("stroke-width", "5")
.attr("marker-end", "url(#arrow)");
var node = svg.selectAll(".node")
.data(graph.nodes)
.enter().append("rect")
.attr("class", "node")
.attr("width", function(d) {
return d.w;
})
.attr("height", function(d) {
return d.h;
})
.style("fill", "blue")
.call(force.drag);
node.append("title")
.text(function(d) {
return d.name;
});
force.on("tick", function() {
link.attr("x1", function(d) {
return d.source.x + d.source.w / 2;
})
.attr("y1", function(d) {
return d.source.y + d.source.h / 2;
})
.attr("x2", function(d) {
return d.target.x + d.target.w / 2;
})
.attr("y2", function(d) {
return d.target.y + d.target.h / 2;
})
node.attr("x", function(d) {
return d.x;
})
.attr("y", function(d) {
return d.y;
});
});
.node {
stroke: #fff;
stroke-width: 1.5px;
}
.link {
stroke: #999;
stroke-opacity: .6;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
JsFiddle example:
http://jsfiddle.net/millermaximilian/w3eq6ccc/
I am really thankful for any advice!
Max
There is no built in way. You can create the code to parameterize the location of the marker based on the mathematical definition of the shape. This is, fundamentally, what's going on when you set a marker to draw at X px from a node when that node is a circle, since mathematically it's just the radius. With a more complex shape, it's harder, though of course squares, rectangles and ellipses are still relatively easy to compute. With a complex svg:path shape, you could, I imagine, use some combination of path's built-in getPointAtLength and computing the angle from one node to another to do that, but I don't know of any implementations of any of the earlier examples, much less something like that.
My D3 circle pack looks like this: (also accesible via jsfiddle)
However, I would like the diagram to look like this: (don't pay attention on labels, or circle pack placement, they are not essential for my case; I meant just co show "3d" looks of circles, and their coloring)
What would be the good way to achieve this?
After #Delapouite answer, I put together another jsfiddle:
The key code is:
var data2 = pack.nodes(data);
var grads = svg.append("defs").selectAll("radialGradient")
.data(data2)
.enter()
.append("radialGradient")
.attr("gradientUnits", "objectBoundingBox")
.attr("cx", 0)
.attr("cy", 0)
.attr("r", "100%")
.attr("id", function(d, i) { return "grad" + i; });
grads.append("stop").attr("offset", "0%").style("stop-color", "white");
grads.append("stop").attr("offset", "100%").style("stop-color", "navy");
and
var circles = vis.append("circle")
.attr("stroke", "black")
.attr("fill", function(d, i) {
return !d.children ? "url(#grad" + i + ")" : "beige";
})
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.attr("r", function(d) { return d.r; });
You can fake the 3D effect of each ball by applying a soft radial gradient to the fill property of the circles :
https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Gradients
I'm following this zoom example. In my case, I don't know how to redraw my data for my svg.
In the example, the svg is initialized like this
chartBody.append("svg:path")
.datum(data)
.attr("class", "line")
.attr("d", line);
var line = d3.svg.line()
.x(function (d) {
return x(d.date);
})
.y(function (d) {
return y(d.value);
});
And is redrawn like this in function "zoomed"
svg.select(".line")
.attr("class", "line")
.attr("d", line);
While in my case
Init:
userSvg.selectAll("circle")
.data(userNodes.slice(1))
.enter().append("svg:circle")
.on("click", function(d){ console.log(d.ind); })
.on("mousemove", function(d){ brushOnUser(d.ind); })
.on("mouseout", function(){ brushOnUser(); })
.attr("r", function(d) { return d.radius; })
.attr("cx", function(d, i) { return userNodesScaleX(d.x); })
.attr("cy", function(d, i) { return userNodesScaleY(d.y); })
.style("fill", function(d, i) { return 'gray'; });
Redraw:
userSvg.selectAll("circle")
.attr("class", "circle");
Of course this redraw doesn't work.
So how do I redraw this?
In the redraw function, you need to set all the attributes that are changed. For a line, this is basically only the d attribute, as it contains all the information that determines how the line is drawn. For a circle, this would be the position of the circle and the radius. That is, your redraw function would look something like this.
userSvg.selectAll("circle")
.attr("r", function(d) { return d.radius; })
.attr("cx", function(d, i) { return userNodesScaleX(d.x); })
.attr("cy", function(d, i) { return userNodesScaleY(d.y); });
Depending on what you're changing, you may have to set a different set of attributes. That is, if you're not changing the radius, then there's no need to set that, but if you're changing the fill color, you would need to set that as well.
I'm adapting the zoomable and clickable map found http://bl.ocks.org/mbostock/2206340 at to plot some points and do some other things. Right now, I'm trying to make it such that on the zoom and click actions, the plotted points also move / honor the zoom. I'm not sure what in the code here is wrong, since I seem to be calling the red.circle and blue.circle objects in the zoom + click -- can anyone identify the issue? Thanks! data.csv is formatted as follows:
lon_0,lat_0,lon_1,lat_1
-122.1430195,37.4418834,-122.415278,37.778643
-122.1430195,37.4418834,-122.40815,37.785034
-122.4194155,37.7749295,-122.4330827,37.7851673
-122.4194155,37.7749295,-122.4330827,37.7851673
-118.4911912,34.0194543,-118.3672828,33.9164666
-121.8374777,39.7284944,-121.8498415,39.7241178
-115.172816,36.114646,-115.078011,36.1586877
and here is the d3.js script.
.background {
fill: none;
pointer-events: all;
}
#states path {
fill: #aaa;
stroke: #fff;
stroke-width: 1.5px;
}
#states path:hover {
stroke: white;
}
</style>
<body>
<script>
var width = 1920/2,
height = 1000/2;
var projection = d3.geo.albersUsa()
.scale(width)
.translate([width / 2, height / 2]);
var path = d3.geo.path()
.projection(projection);
var zoom = d3.behavior.zoom()
.translate(projection.translate())
.scale(projection.scale())
.scaleExtent([height, 50 * height])
.on("zoom", zoom);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.attr("style", "stroke:black; stroke-width:2px");
var states = svg.append("g")
.attr("id", "states")
.call(zoom);
var dataset = [];
states.append("rect")
.attr("class", "background")
.attr("width", width)
.attr("height", height);
d3.json("us-states.json", function(json) {
states.selectAll("path")
.data(json.features)
.enter().append("path")
.attr("d", path)
.on("click", click);
d3.csv("data.csv", function(data) {
states.selectAll(".blue.circle")
.data(data)
.enter()
.append("circle")
.attr("cx", function(d) {
return projection([d["lon_0"], d["lat_0"] ])[0];
})
.attr("cy", function(d) {
return projection([d["lon_0"],d["lat_0"] ])[1];
})
.attr("r", 5)
.attr("class", "blue circle")
.style("fill", "blue");
states.selectAll(".red.circle")
.data(data)
.enter()
.append("circle")
.attr("cx", function(d) {
return projection([+d["lon_1"], +d["lat_1"] ])[0];
})
.attr("cy", function(d) {
return projection([+d["lon_1"],+d["lat_1"] ])[1];
})
.attr("r", 5)
.attr("class", "red circle")
.style("fill", "red");
});
});
function click(d) {
var centroid = path.centroid(d),
translate = projection.translate();
projection.translate([
translate[0] - centroid[0] + width / 2,
translate[1] - centroid[1] + height / 2
]);
zoom.translate(projection.translate());
states.selectAll("path").transition()
.duration(1000)
.attr("d", path);
states.selectAll("red.circle").transition()
.duration(1000)
.attr("d", circle);
states.selectAll("blue.circle").transition()
.duration(1000)
.attr("d", circle);
}
function zoom() {
projection.translate(d3.event.translate).scale(d3.event.scale);
states.selectAll("path").attr("d", path);
states.selectAll("red.circle").attr("d", path);
states.selectAll("blue.circle").attr("d",path);
}
</script>
you're setting the co-ordinates of the circles when you load the map, so when you click the zoom function, your circles are displayed but are not using the same co-ordinates - i think - it will help if you can create a http://bl.ocks.org to see this.
perhaps this could be of help http://bl.ocks.org/nkhine/3150901 only UK, US and Afganistan works, but i am basically re-projecting the secondary map to fit the new zoom level.