I'm new to D3 and I'm using this template (http://bl.ocks.org/mbostock/1153292) to visualize my graph. However, my nodes' labels are shore and I want to place them inside circles not on their right hand-side.
Can anybody help me with that?
Thanks a lot
Append circle and text inside a group element and use text-achor property to align the text to the middle.
var nodes = svg.selectAll(".node")
.data(force.nodes())
.enter()
.append("g")
.attr("class","node")
.call(force.drag);
var circles = nodes.append("circle")
.attr("r", 6);
var texts = nodes.append("text")
.attr("text-anchor", "middle")
.text(function(d) {
return d.name;
});
Change tick function as shown below.
function tick() {
path.attr("d", linkArc);
nodes.attr("transform", transform);
}
Update
JSFiddle 1 for aligning text to the center.
If you would like enclose the text label within the circle, increase the radius of circle using the length of name property. Note that, you will have to update the link target positions this time, to adjust the marker positions relative to the circle radius.
var circles = nodes.append("circle")
.attr("r", function(d){ d.radius = d.name.length*3; return d.radius; }
);
function linkArc(d) {
var tX = d.target.x-d.target.radius,
dx = tX - d.source.x,
dy = d.target.y - d.source.y,
dr = Math.sqrt(dx * dx + dy * dy);
return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + tX + "," + d.target.y;
}
JSFiddle 2
Related
(My code is at the end)
My goal is to display a country map (provided in a topojson file) which automatically scale and translate to fit into an area and then display few dots on it, representing some cities (given their lat/long coordinates).
First part was easy. I found (don't remember if it was on SO or on bl.ocks.org) that we can use bounds to compute scale and translate. That works perfectly and my country adapt to its parent area.
First Question: Why the country doesn't behave the same if I scale/translate it with its transform attribute or with projection.scale().translate() ? I mean, when I use transform attribute the country adapts perfectly whereas projection.scale().translate() displays a small country in a corner.
Second part is displaying some cities on my map. My cities has coordinates (which are real ones) :
var cities = {
features: [
{
'type':'Feature',
'geometry':{
'type':'Polygon',
'coordinates': [2.351828, 48.856578] // Longitude, Latitude
},
'properties':{}
},
{
'type':'Feature',
'geometry':{
'type':'Polygon',
'coordinates': [5.726945, 45.187778] // Longitude, Latitude
},
'properties':{}
},
};
When I try to apply scale and translate parameters (to adapt with my country which has been scaled and translated) either with projection.scale().translate() or with transform attribute my cities are far far away from where they should be.
Second Question: Why I cannot use same scale/translate parameters on country and cities ? How can I properly display my cities where they should be ?
function computeAutoFitParameters(bounds, width, height) {
var dx = bounds[1][0] - bounds[0][0];
var dy = bounds[1][1] - bounds[0][1];
var x = (bounds[0][0] + bounds[1][0]) / 2;
var y = (bounds[0][1] + bounds[1][1]) / 2;
var scale = 0.9 / Math.max(dx / width, dy / height);
var translate = [width / 2 - scale * x, height / 2 - scale * y];
return {
scale : scale,
translate: translate
};
}
// element is the HTML area where the country has to fit.
var height = element.height();
var width = element.width();
var projection = d3.geo.miller();
var path = d3.geo.path().projection(projection);
// data is my country (a topojson file with BBox)
var topojsonCountry = topojson.feature(data, data.objects[country.id]).features;
var bounds = path.bounds(topojsonCountry[0]);
var params = computeAutoFitParameters(bounds, width, height);
var scale = params.scale;
var translate = params.translate;
var svg = d3.select(element[0]).append('svg')
.attr('width', width + 'px')
.attr('height', height + 'px');
svg.append('g')
.selectAll('path')
.data(topojsonCountry)
.enter()
.append('path')
.attr('d', path)
.attr('transform', 'translate(' + translate + ')scale(' + scale + ')');
svg.selectAll('circle')
.data(cities.features) // city is defined in the code above
.enter()
.append('circle')
.attr('transform', function(d) {
return 'translate(' + projection(d.geometry.coordinates) + ')';
)
.attr('r', '6px');
EDIT: I had removed too much code to simplify it. It's fixed now. The difference is that I have an array of cities to display rather than just one.
Thanks in advance.
I found out that I had to add null parameters to my projection. To sum up :
Create a minimal projection (and a path)
Apply null scale and translate parameters to the projection : projection.scale(1).translate([0, 0])
Compute real scale and translate parameters according to the bounding box
Display the country's map as before (no changes here)
Set computed scale and translate parameters to the projection : projection.scale(params.scale).translate(params.translate);
Draw the cities dots.
`
// element is the HTML area where the country has to fit.
var height = element.height();
var width = element.width();
var projection = d3.geo.miller();
var path = d3.geo.path().projection(projection);
projection.scale(1).translate([0, 0]) // This is new
// data is my country (a topojson file with BBox)
var topojsonCountry = topojson.feature(data, data.objects[country.id]).features;
var bounds = path.bounds(topojsonCountry[0]);
var params = computeAutoFitParameters(bounds, width, height);
var svg = d3.select(element[0]).append('svg')
.attr('width', width + 'px')
.attr('height', height + 'px');
svg.append('g')
.selectAll('path')
.data(topojsonCountry)
.enter()
.append('path')
.attr('d', path)
.attr('transform', 'translate(' + params.translate + ')scale(' + params.scale + ')');
projection.scale(params.scale).translate(params.translate); // This is new
svg.selectAll('circle')
.data(cities.features)
.enter()
.append('circle')
.attr('transform', function(d) {
return 'translate(' + projection(d.geometry.coordinates) + ')';
})
.attr('r', '6px')
.attr('fill', 'red');
I'm trying to draw vertical labels for the heatmap that I'm working. I'm using the example from http://bl.ocks.org/tjdecke/5558084. Here is the part of the code that I've changed:
var timeLabels = svg.selectAll(".timeLabel")
.data(ife_nr)
.enter().append("text")
.text(function(d) {
return d;
})
.attr("x", function(d, i) {
return (i * gridSize);
})
.attr("y", 0)
//.style("text-anchor", "middle")
//.attr("transform", "translate(" + gridSize / 2 + '-5' + ")")
.attr("transform", "translate(" + gridSize/2 + '-8' + "), rotate(-90)")
.attr("class", function(d, i) {
return ((i >= 0) ? "timeLabel mono axis axis-worktime" : "timeLabel mono axis");
});
But it appears the labels seems to be stacked on top one another on top of the first grid. How can I edit this code to get the labels correctly displayed?
Two problems: first, the translate should have a comma separating the values:
"translate(" + gridSize/2 + ",-8), rotate(-90)")
Assuming that -8 is the y value for the translate. If you don't have a comma, the value inside the parenthesis should be just the x translation (If y is not provided, it is assumed to be zero). But even if there is actually no comma and all that gridSize/2 + '-8' is just the x value you still have a problem, because number plus string is a string. You'll have to clarify this point.
Besides that, for rotating the texts over their centres, you'll have to set the cx and cy of the rotate. Have a look at this demo:
var svg = d3.select("body")
.append("svg")
.attr("width", 400)
.attr("height", 100);
var texts = svg.selectAll(".texts")
.data(["foo", "bar", "baz"])
.enter()
.append("text");
texts.attr("y", 50)
.attr("x", function(d,i){ return 50 + 80*i})
.text(function(d){ return d});
texts.attr("transform", function(d,i){
return "rotate(-90 " + (50 + 80*i) + " 50)";
});
<script src="https://d3js.org/d3.v4.js"></script>
I am attempting to use d3.js to move a point along an arc from 0 to PI, say, without the point moving back along the innerRadius as seen here http://bl.ocks.org/mbostock/1705868.
I removed innerRadius hoping (unsuccessfully) that would work (http://jsfiddle.net/klin/23c5476v/). I had also tried setting the innerRadius with the same value as outerRadius.
Fragment I changed (changes marked with //) ...
var path = svg.append("svg:path")
.datum({endAngle: Math.PI}) //
.attr("d", d3.svg.arc()
// .innerRadius(h / 4) // Hoping removal would prevent inner transition
.outerRadius(h / 3)
.startAngle(0)
);//.endAngle(Math.PI));
Entire code ...
var svg = d3.select("body").append("svg:svg")
.attr("width", w)
.attr("height", h)
.append("svg:g")
.attr("transform", "translate(" + w / 2 + "," + h / 2 + ")");
var path = svg.append("svg:path")
.datum({endAngle: Math.PI}) //
.attr("d", d3.svg.arc()
// .innerRadius(h / 4) // Hoping removal would prevent inner transition
.outerRadius(h / 3)
.startAngle(0)
);//.endAngle(Math.PI));
var circle = svg.append("svg:circle")
.attr("r", 6.5)
.attr("transform", "translate(0," + -h / 3 + ")");
function transition() {
circle.transition()
.duration(5000)
.attrTween("transform", translateAlong(path.node()))
.each("end", transition);
}
transition();
// Returns an attrTween for translating along the specified path element.
function translateAlong(path) {
var l = path.getTotalLength();
return function(d, i, a) {
return function(t) {
var p = path.getPointAtLength(t * l);
return "translate(" + p.x + "," + p.y + ")";
};
};
}
The problem I think is that the arc shape has area, so the path must be closed, while the line shape does not. Eventually I'd like to be able to separately animate object movement along a series of consecutive arcs similar to the answer to Interpolating along consecutive paths with D3.js, but first I need to avoid the loop back movement.
Is a simple solution maybe to not use d3's arc generator, but instead use another where the end point actually is the terminus of the path?
Paul is right.
You can do next
var arc = d3.svg.arc(); //plus params
$path.attr('d',function(){
var d = arc();
return d.split('L')[0]; //will return half of arc without lines
});
I'm using d3.geo.tile() and have used it successfully before but this time the tile layer doesn't seem to draw at the same scale and translate as the point layer. The below code creates a map that pans and draws just fine, but draws the circles, which should be in the Mediterranean, in Africa. If I zoom in, it scales the tiles and circles just fine, it's as if my xy coordinates are off, but they aren't.
I get the feeling that it's actually drawing the base layer without offsetting and scaling it properly because it should be centering on the coordinates 12,42, but it's a great big mystery to me since this exact same code works fine in a different application.
If someone can spot some problem, or just a hint, that would help.
function createNewMap(){
width = 1200, height = 800;
var tile = d3.geo.tile()
.size([1200, 800]);
var projection = d3.geo.mercator()
.scale((1 << 12) / 2 / Math.PI)
.translate([width / 2, height / 2]);
var center = projection([12, 42]);
var zoom = d3.behavior.zoom()
.scale(projection.scale() * 2 * Math.PI)
.scaleExtent([1 << 10, 1 << 17])
.translate([width - center[0], height - center[1]])
.on("zoom", zoomed);
projection
.scale(1 / 2 / Math.PI)
.translate([0, 0]);
var svg = d3.select("#newMapId").append("svg")
.attr("width", width)
.attr("height", height)
.call(zoom);
var raster = svg.append("g");
var vector = svg.append("g");
vector.selectAll("g").data(dataModule.polisData).enter().append("g")
.attr("class", "sites")
.attr("transform", function(d) {return "translate(" + (projection([d.xcoord,d.ycoord])[0]) + "," + (projection([d.xcoord,d.ycoord])[1]) + ")scale("+(projection.scale())+")"})
.append("circle")
.attr("class", "sitecirc");
zoomed();
function zoomed() {
var tiles = tile
.scale(zoom.scale())
.translate(zoom.translate())
();
var image = raster
.attr("transform", "scale(" + tiles.scale + ")translate(" + tiles.translate + ")")
.selectAll("image")
.data(tiles, function(d) { return d; });
image.exit()
.remove();
image.enter().append("image")
.attr("xlink:href", function(d) { return "http://" + ["a", "b", "c", "d"][Math.random() * 4 | 0] + ".tiles.mapbox.com/v3/elijahmeeks.map-zm593ocx/" + d[2] + "/" + d[0] + "/" + d[1] + ".png"; })
.attr("width", 1)
.attr("height", 1)
.attr("x", function(d) { return d[0]; })
.attr("y", function(d) { return d[1]; });
vector
.attr("transform", "translate(" + zoom.translate() + ")scale(" + zoom.scale() + ")");
d3.selectAll(".sitecirc")
.attr("r", 10 / zoom.scale());
}
Your code appears to be based on my example that changes the SVG transform on zoom. Changing the transform is a nice technique when you have complex geometry that you just want to scale and translate when you pan or zoom — it’s typically faster than reprojecting everything — but it’s also more complex than changing the projection on zoom.
The code doesn’t change very much if you want to change the projection on zoom. In essence:
projection
.scale(zoom.scale() / 2 / Math.PI)
.translate(zoom.translate());
And then re-run your d3.geo.path to re-render. As shown in bl.ocks.org/9535021:
Also, fixing the projection and changing the transform can cause precision problems if you zoom in a lot. Another reason to only use that technique when it offers substantial performance gains by avoid reprojection. And here reprojecting is super-cheap because it’s just a handful of points.
I'm using the force layout to represent a directed unweighted network. My inspiration comes from the following example: http://bl.ocks.org/mbostock/1153292
I tried to make nodes of different sizes, but I have a little problem.
The marker used to draw the arrow on each link points to the center of the circle. If the circle is too big it covers completely the arrow.
How can I handle this?
If you will use a <line> instead of <path>, the following should work for you, I have it working in my current solution. It's based on #ɭɘ ɖɵʊɒɼɖ 江戸 solution:
In your tick event listener:
linkElements.attr("x1", function(d) { return d.source.x; })
.attr("y1", function(d) { return d.source.y; })
.attr("x2", function(d) {
return getTargetNodeCircumferencePoint(d)[0];
})
.attr("y2", function(d) {
return getTargetNodeCircumferencePoint(d)[1];
});
function getTargetNodeCircumferencePoint(d){
var t_radius = d.target.nodeWidth/2; // nodeWidth is just a custom attribute I calculate during the creation of the nodes depending on the node width
var dx = d.target.x - d.source.x;
var dy = d.target.y - d.source.y;
var gamma = Math.atan2(dy,dx); // Math.atan2 returns the angle in the correct quadrant as opposed to Math.atan
var tx = d.target.x - (Math.cos(gamma) * t_radius);
var ty = d.target.y - (Math.sin(gamma) * t_radius);
return [tx,ty];
}
I am sure this solution can be modified to accomodate <path> elements, however I haven't tried it.
You can offset the target of the link by the radius of the node, i.e. adjust the code
path.attr("d", function(d) {
var dx = d.target.x - d.source.x,
dy = d.target.y - d.source.y,
dr = Math.sqrt(dx * dx + dy * dy);
return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y;
});
by changing the values of d.target.x and d.target.y to take the radius (which would need to be part of the data, something like d.target.radius) into account. That is, offset the end of the arrow by the circle radius.
At the end I've decided to create a marker for each link (instead of one per class).
This solution has the advantage of defining the offset of each marker, depending on the target node which, in my own case, is refX.
// One marker for link...
svg.append("svg:defs").selectAll("marker")
.data(force.links())
.enter().append("svg:marker")
.attr("id", function(link, idx){ return 'marker-' + idx})
.attr("viewBox", "0 -5 10 10")
.attr("refX", function(link, idx){
return 10 + link.target.size;
})
.attr("refY", 0)
.attr("markerWidth", 6)
.attr("markerHeight", 6)
.attr("orient", "auto")
.append("svg:path")
.attr("d", "M0,-5L10,0L0,5")
.attr("fill", function(link){
if(link.type == 'in')
return "green";
return "blue";
});
Now there is one little problem with the fact that the line is curve. This means that the marker/arrow should be translated not only on the X axis, but also on the Y axis, of a value which probably depends on the ray of the curve...
A bit late to answer, but combining all previous answers, I have come up with a comprehensive solution that works for me in d3 v4, written in TypeScript because Angular (in case you find the lack of global variables curious). Below is a snippet containing the key components to include (because my entire production code is way too long and under NDA). Key ideas are annotated as code comments. The end result looks like this:
First of all, since you have tried to make nodes of different sizes, I will assume you have a radius property inside your nodes data. Let's say it is an array of objects like this:
{
id: input.name,
type: input.type,
radius: input.radius
}
Then markers are appended. Note that the size of each arrow (or marker) is 10, and half of it is 5. You can assign it as a variable like #ɭɘ-ɖɵʊɒɼɖ-江戸 did in his answer, but I am just too lazy.
let marker = svg.append("defs")
.attr("class", "defs")
.selectAll("marker")
// Assign a marker per link, instead of one per class.
.data(links, function (d) { return d.source.id + "-" + d.target.id; });
// Update and exit are omitted.
// Enter
marker = marker
.enter()
.append("marker")
.style("fill", "#000")
// Markers are IDed by link source and target's name.
// Spaces stripped because id can't have spaces.
.attr("id", function (d) { return (d.source.id + "-" + d.target.id).replace(/\s+/g, ''); })
// Since each marker is using the same data as each path, its attributes can similarly be modified.
// Assuming you have a "value" property in each link object, you can manipulate the opacity of a marker just like a path.
.style("opacity", function (d) { return Math.min(d.value, 1); })
.attr("viewBox", "0 -5 10 10")
// refX and refY are set to 0 since we will use the radius property of the target node later on, not here.
.attr("refX", 0)
.attr("refY", 0)
.attr("markerWidth", 5)
.attr("markerHeight", 5)
.attr("orient", "auto")
.append("path")
.attr("d", "M0,-5L10,0L0,5")
.merge(marker);
Then, the path can reference each individual marker with its ID:
let path = svg.append("g")
.attr("class", "paths")
.selectAll("path")
.data(links, function (d) { return d.source.id + "-" + d.target.id; });
// Update and exit are omitted.
// Enter
path = path
.enter()
.append("path")
.attr("class", "enter")
.style("fill", "none")
.style("stroke", "#000")
.style("stroke-opacity", function (d) { return Math.min(d.value, 1); })
// This is how to connect each path to its respective marker
.attr("marker-end", function(d) { return "url(#" + (d.source.id + "-" + d.target.id).replace(/\s+/g, '') + ")"; })
.merge(path);
One optional thing to modify if you want more features: Allow your .on("tick", ticked) listener to receive more variables to test for boundaries. For example, the width and height of the svg.
.on("tick", function () { ticked(node, path, width, height) })
And here is your new ticked function, based on the answer of #ɭɘ-ɖɵʊɒɼɖ-江戸 :
ticked(node, path, width, height) {
node
.attr("transform", function(d){return "translate(" + Math.max(d.radius, Math.min(width - d.radius, d.x)) + "," + Math.max(d.radius, Math.min(height - d.radius, d.y)) + ")"});
path
.attr("d", d => {
let dx = d.target.x - d.source.x,
dy = d.target.y - d.source.y,
dr = Math.sqrt(dx * dx + dy * dy),
gamma = Math.atan2(dy, dx), // Math.atan2 returns the angle in the correct quadrant as opposed to Math.atan
sx = Math.max(d.source.radius, Math.min(width - d.source.radius, d.source.x + (Math.cos(gamma) * d.source.radius) )),
sy = Math.max(d.source.radius, Math.min(height - d.source.radius, d.source.y + (Math.sin(gamma) * d.source.radius) )),
// Recall that 10 is the size of the arrow
tx = Math.max(d.target.radius, Math.min(width - d.target.radius, d.target.x - (Math.cos(gamma) * (d.target.radius + 10)) )),
ty = Math.max(d.target.radius, Math.min(height - d.target.radius, d.target.y - (Math.sin(gamma) * (d.target.radius + 10)) ));
// If you like a tighter curve, you may recalculate dx dy dr:
//dx = tx - sx;
//dy = ty - sy;
//dr = Math.sqrt(dx * dx + dy * dy);
return "M" + sx + "," + sy + "A" + dr + "," + dr + " 0 0,1 " + tx + "," + ty;
});
}
As mentioned by #joshua-comeau, it should be a plus sign when calculating sx and sy.
Here my solution :
First I calculate the angle with horizontal axes of the path (gamma). Then I get the X component (Math.cos(gamma) * radius) and Y component (Math.sin(gamma) * radius) of the radius. Then offset the ends of the path by those components.
function linkArc(d) {
var t_radius = calcRadius(d.target.size);
var s_radius = calcRadius(d.source.size);
var dx = d.target.x - d.source.x;
var dy = d.target.y - d.source.y;
var gamma = Math.atan(dy / dx);
var tx = d.target.x - (Math.cos(gamma) * t_radius);
var ty = d.target.y - (Math.sin(gamma) * t_radius);
var sx = d.source.x - (Math.cos(gamma) * s_radius);
var sy = d.source.y - (Math.sin(gamma) * s_radius);
return "M" + sx + "," + sy + "L" + tx + "," + ty;
}
First you will notice I am not using arcs but the principle should be the same.
Also my nodes have a size property from which I calculate the diameter of the circle.
Finally my marker is defined as is:
var arrowsize = 10;
var asHalf = arrowsize / 2;
svg.append("defs").selectAll("marker")
.data(["arrowhead"])
.enter().append("marker")
.attr("id", function (d) {
return d;
})
.attr("viewBox", "0 -5 " + arrowsize + " " + arrowsize)
.attr("refX", arrowsize)
.attr("refY", 0)
.attr("markerWidth", 9)
.attr("markerHeight", 9)
.attr("orient", "auto")
.attr("class", "arrowhead-light")
.append("path")
.attr("d", "M 0," + (asHalf * -1) + " L " + arrowsize + ",0 L 0," + asHalf);
I haven't found a way to control every single copy of the marker.