How to position labels on dc.js pie chart? - dc.js

I'd like to show labels for tiny slices (chart.minAngleForLabel(0.05)) avoiding text overlap.
I added a renderlet that shifts labels toward outer edge:
.on('renderlet', function(chart) {
chart.selectAll('text').attr('transform', function(d, i) {
var old = this.getAttribute('transform');
if (d.endAngle-d.startAngle > 0.3) { return old; }
var xy = old.slice(10,-1).split(',');
var m = 1.25 + (i%3) * 0.25;
return 'translate(' + (+xy[0]*m) + ',' + (+xy[1]*m) + ')';
})
})
and i'm rather happy with it (the second image is after renderlet):
but it makes annoying transitions -- labels move toward centroid and then jump back. Is there a workaround for this?

My solution is a bit excessive, but I wanted to know if it's now possible to replaced transitioned positions, now that we have the pretransition event in dc.js 2.0 beta 11.
In fact, it is. The impractical part is that your code relies on already having the final positions, which we're not going to have if we replace the transitions. Instead, we have to calculate the positions from scratch, which means copying a bunch of code out of the pie chart.
I wasn't able to get your code to work, so I'm just testing this by offsetting all label positions by -25, -25. But it's the same idea, we use the original code to get the centroid, and then modify that position:
// copied from pieChart
function buildArcs(chart) {
return d3.svg.arc().outerRadius(chart.radius()).innerRadius(chart.innerRadius());
}
function labelPosition(d, arc) {
var centroid = arc.centroid(d);
if (isNaN(centroid[0]) || isNaN(centroid[1])) {
return [0,0];
} else {
return centroid;
}
}
//
.on('pretransition', function(chart) {
chart.selectAll('text.pie-slice').transition().duration(chart.transitionDuration())
.attr('transform', function(d, i) {
var arc = buildArcs(chart);
var xy = labelPosition(d, arc);
return 'translate(' + (+xy[0] - 25) + ',' + (+xy[1] - 25) + ')';
})
});
The big idea here is that if you specify a new transition for an element, it will replace the transition that was already active. So we are completely removing the original position and transition, and replacing it with our own. No "jump"!

Not really solving your problem, but might look better with a transition on the position?
chart.selectAll('text')
.transition()
.delay(800)
.attr("transform", ...

I have a solution for this problem. Try this once , this will works to avoid overlapping of label names in pie charts.
function buildArcs(chart) {
return
d3.svg.arc().outerRadius(chart.radius()).innerRadius(chart.innerRadius());
}
function labelPosition(d, arc) {
var centroid = arc.centroid(d);
if (isNaN(centroid[0]) || isNaN(centroid[1])) {
return [0,0];
} else {
return centroid;
}
}
.on('pretransition', function(chart) {
chart.selectAll('text.pieslice').transition()
.duration(chart.transitionDuration())
.attr('transform', function(d, i) {
var j = 0;
var arc = buildArcs(chart);
var xy = labelPosition(d, arc);
if (xy[1] < 0) {
j = -(10 * (i + 1));
}
else {
j = 10 * (i + 1);
}
return 'translate(' + (+xy[0] - 25) + ',' + (j) + ')';
})
});

Related

d3: how to drag line elements independently of background

I have developed an applet that shows a d3 diagonal tree. The graph is navigatable by dragging the background.
It is based on the code found at the following link:
https://bl.ocks.org/adamfeuer/042bfa0dde0059e2b288
I am trying to have vertical lines across the page to further annotate the tree/ graph (based on the following link: https://bl.ocks.org/dimitardanailov/99950eee511375b97de749b597147d19).
See below:
See here: https://jsfiddle.net/chrisclarkson100/opfq6ve8/28/
I append the lines to the graph as follows:
var data_line = [
{
'x1': 300,
'y1': 700,
'x2': 300,
'y2': 700
},
////....
];
// Generating the svg lines attributes
var lineAttributes = {
....
'x1': function(d) {
return d.x1;
},
'y1': function(d) {
return screen.availHeight;
},
'x2': function(d) {
return d.x2;
},
'y2': function(d) {
return 0;
}
};
var drag_line = d3.behavior.drag()
.origin(function(d) { return d; })
.on('drag', dragged_line);
// Pointer to the d3 lines
var svg = d3.select('body').select('svg');
var lines = svg
.selectAll('line')
.data(data_line)
.enter().append('g')
.attr('class', 'link');
links_lines=lines.append('line')
.attr(lineAttributes)
.call(drag_line);
lines.append('text')
.attr('class','link_text')
.attr("x", d => d.x1)
.attr("y", d => 350)
.style('fill', 'darkOrange')
.style("font-size", "30px")
function dragged_line() {
var x = d3.event.dx;
var y = d3.event.dy;
var line = d3.select(this);
// Update the line properties
var attributes = {
x1: parseInt(line.attr('x1')) + x,
y1: parseInt(line.attr('y1')) + y,
x2: parseInt(line.attr('x2')) + x,
y2: parseInt(line.attr('y2')) + y,
};
line.attr(attributes);
}
The lines display as I wanted and are draggable. However, when I drag them, the background/ tree network moves with them... I want the different draggable elements to be independent of eachother.
Can anybody spot how I'm failing to make the dragging interactivity of each element independent of eachother?
Here's a JSFiddle that seems to do what you want– you can move the vertical lines without moving the tree; and move the tree without moving the vertical lines:
https://jsfiddle.net/adamfeuer/gd4ouvez/125/
(That JSFiddle uses a much smaller dataset of tree nodes; the one you linked to was too big to easily iterate and debug.)
The issue with the code you posted is that the zoom (pan) function for the tree is active at the same time the zoom() for the lines is active, so the tree and the active line drag at the same time.
I added a simple mechanism to separate the two – a boolean called lineDragActive. The code then checks for that in the tree zoom(), sets it true when a line drag starts, and false when the line drag ends:
// Define the zoom function for the zoomable tree
// flag indicates if line dragging is active...
// if so, we don't want to drag the tree
var lineDragActive = false;
function zoom() {
if (lineDragActive == false) {
// not line dragging, so we can zaoom (drag) the tree
svgGroup.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
}
}
[...]
function drag_linestarted() {
// tell others not to zoom while we are zooming (dragging)
lineDragActive = true;
d3.select(this).classed(activeClassName, true);
}
function drag_lineended() {
// tell others that zooming (dragging) is allowed
lineDragActive = false;
d3.select(this).classed(activeClassName, false);
label = baseSvg.selectAll('.link_text').attr("transform", "translate(" + String(Number(this.x1.baseVal.value) - 400) + "," + 0 + ")");
}

possible to tween d3.geo.circle().angle()

I'm trying to recreate the "pulse" effect from this example:
https://anthonyskelton.com/2016/d3-js-earthquake-visualizations/
on a rotating globe... so I have to use d3.geo.circle() to generate paths (rather than svg circles) to manage the clipping properly.
I can transition() other attributes but am guessing I'll need to tween the path for each circle... I just don't know where to start and can't find any examples... there are very few examples using d3.geo.circle() and they are all static.
Thanks for any pointers!
The solution to this question came by way of pursuing a related question:
D3: Accessing bound data after using .datum()
The first step was understanding d3.geo.path().pointRadius() and creating a function to pass into .attr('d', f(d){})
The parameter i is unused but serves as a placeholder so that the radius r can be passed.
The pointPath() function is used elsewhere in update() and reDraw() functions, so it looks for a radius attribute that may already be present in bound data.
geoPath = d3.geo.path().projection(projection);
// var pointPath = function(d, i, data, r) { // d3v4 adds extra param
var pointPath = function(d, i, r) {
if (d.properties && d.properties.radius != undefined) {
r = r || d.properties.radius;
}
r = r || 1.5;
var coords = [d.geometry.coordinates[0], d.geometry.coordinates[1]];
var pr = geoPath.pointRadius(globe.scale() / 100 * r);
return pr({ type: "Point", coordinates: coords })
}
The animation was then possible using an .attrTween()
pulse = function() {
surface.selectAll('.pulse-circle')
.attr("d", function(d) { return pointPath(d, 0, 0); })
.style("fill-opacity", 1 )
.transition()
.delay(function(d, i) { return i * 200; })
.duration(3000)
.style("fill-opacity", 0 )
.attrTween("d", function(d) {
rinterp = d3.interpolate(0, 10);
var fn = function(t) {
d.r = rinterp(t);
return pointPath(d, 0, d.r) || 'M0,0';
};
return fn;
});
}
Because this occurs on a rotating globe I had to add return 'M0,0' if pointPath() returned undefined... to avoid console errors.

How to detect whether circle is translated in clockwise or not in d3

I am using globe functionality using d3 and d3 geo zoom library.
Almost all things are done, but having one issue.
I have draw circle with element g, it is rotating along with path when we rotate globe, but main problem is that element g should be hide when path goes behind, right now points are showing when path goes behind while rotating, so what would the best solution to hide points on globe when map path is behind.
var zoom = d3.geo.zoom()
.projection(projection)
.scaleExtent([minScale, maxScale])
.on("zoomstart", function() {
// TODO inertial drag
if (d3.event.sourceEvent) svg.selectAll("path").classed("focus", false);
})
.on("zoom",function() {
if (d3.event.sourceEvent) {
d3.event.sourceEvent.preventDefault();
}
d3.select(this).call(redraw);
suppressClick = true;
})
.on("zoomend", function() {
svg.classed("zooming", false);
});
function redraw(svg) {
svg.selectAll('path')
.attr('d', function (d) {
var g = d3.select(this);
return path.pointRadius(g.classed('focus') || g.classed('focus-hover') ? 9.5 : 7.5)(d);
});
var cluster = svg.selectAll('g.cluster')
.each(function (d) {
d.projected = null;
d3.geo.stream(d, projection.stream({point: function (x, y) {
d.projected = [x, y];
}}));
var circle = d3.select(this).select('circle.cluster');
circle.attr('r', circle.classed('focus-hover') ? 9.5 : 8.5);
})
.attr('transform', function (d) {
return 'translate(' + (d.projected || 0) + ')';
});
g.selectAll('g.cluster').attr("transform", function(d) {return "translate(" + projection([d[1],d[0]]) + ")";});
/*.style('display', function (d) {
return d.projected ? null : 'none';
})
.attr('transform', function (d) {
return 'translate(' + (d.projected || 0) + ')';
});*/
var displayLocation = projection.scale() > maxScale - 0.1;
svg.classed('zoomed', displayLocation);
if (displayLocation) {
cluster.selectAll('.location')
.style('display', null)
.attr('r', function (d) {
var circle = d3.select(this);
return circle.classed('focus') || circle.classed('focus-hover') ? 9.5 : 7.5;
});
cluster.selectAll('.label').style('display', null);
} else {
cluster.selectAll('.location').style('display', 'none');
cluster.selectAll('.label').style('display', 'none');
}
}
One way to do it is to hide them. Set CSS display to none when they appear behind the globe, that is, when any of their positions is not in the range [-90,90] for latitude or longitude).
You can add the yaw and pitch angles obtained from projection.rotate() (which are, respectively, equivalent to longitude and latitude) to each corresponding longitude/latitude coordinate of the circle. If the result is out of range hide it (display: none), otherwise show it (display:block). You can start with like this:
d3.selectAll("circle").style("display", function(d) {
return d[0] + projection.rotate()[0] < 90
&& d[0] + projection.rotate()[0] > -90
&& d[1] + projection.rotate()[1] < 90
&& d[1] + projection.rotate()[1] > -90 ? "block" : "none";
})

D3 Multiple Pie Chart Updates

I am quite new to D3 but have been working through some mbostocks examples but hitting an issue when trying to update multiple pie charts. I can generate these fine from my data array but when I want to update them I run into an issue.
The issue is quite simple but I am a little stuck on how to fix this. I have run up my code in js fiddle that can be found here. You will see that in my example I build three pies, then wait 3 seconds and update these to new data. The issue I have is that all pies always seem to get updated with the same data.
I believe this is due to the way I am making the path selection in order to update the pie. it looks like I am updating each all the paths each time with each data array so they all end up being updated with the last dataset in my array.
If anyone knows how I can update this in order to correctly build the pies I would be very grateful of any help, pointers or comments.
var data = [
[3, 4, 5, 9],
[1, 7, 3, 4],
[4, 3, 2, 1],
];
function getData() {
// Generate some random data to update the pie with
tdata = []
for(i in data) {
rdata = []
for(c in data[i]) {
rdata.push(Math.floor((Math.random() * 10) + 1) )
}
tdata.push(rdata)
}
return tdata
}
// ------------
var m = 10,
r = 100
var mycolors = ["red","#FF7F00","#F5CC11","#D61687","#1E93C1","#64B72D","#999999"]
var arc = d3.svg.arc()
.innerRadius(r / 2)
.outerRadius(r)
var pie = d3.layout.pie()
.value(function(d) { return d; })
.sort(null);
var svg = d3.select("body").selectAll("svg")
.data(data)
.enter()
.append("svg")
.attr("width", (r + m) * 2)
.attr("height", (r + m) * 2)
.attr("id", function(d,i) {return 'pie'+i;})
.append("svg:g")
.attr("transform", "translate(" + (r + m) + "," + (r + m) + ")");
var path = svg.selectAll("path")
.data(pie)
.enter()
.append("svg:path")
.attr("d", arc)
.style("fill", function(d, i) { return mycolors[i]; })
.each(function(d) { this._current = d; }); // store the initial angles
var titles = svg.append("svg:text")
.attr("class", "title")
.text(function(d,i) {return i;})
.attr("dy", "5px")
.attr("text-anchor", "middle")
// -- Do the updates
//------------------------
setInterval(function() {
change()
}, 3000);
function change() {
// Update the Pie charts with random data
piedata = getData()
svg.each(function(d,i) {
path = path.data(pie(piedata[i]))
path.transition().duration(1000).attrTween("d", arcTween);
})
// temp, print new array to screen
tdata = ""
for(x in piedata) {
tdata += "<strong>"+x+":</strong> "+piedata[x]+"<br>"
}
$('#pieData').html(tdata)
}
function arcTween(a) {
var i = d3.interpolate(this._current, a);
this._current = i(0);
return function(t) {
return arc(i(t));
};
}
Right, I finally got this working and am posting the working solution incase others are trying to do the same thing.
I expect this might not be the best nor most efficient way of doing it but this is going to be fine for what I need (at this point). But if anyone still has any better solutions it would be good to hear from you.
I ended up selecting the paths based on a unique id that I gave the individual SVG elements which I created, then just updated these paths only. Sounds simple now when I say it like this but did have me stumped for a while.
function change() {
// Update the Pie charts with random data
var newdata = getData()
for(x in newdata) {
var npath = d3.select("#pie"+x).selectAll("path").data(pie(newdata[x]))
npath.transition().duration(1000).attrTween("d", arcTween); // redraw the arcs
}
}
Full working copy can be found at http://jsfiddle.net/THT75/nskwwbnf/

How do I adjust zoom size for a point in D3?

This could be a classic case of "you're doing it wrong", but all of my searching to date hasn't warranted any help.
Here's my scenario:
I'm using an albersUSA map projection in conjunction with the national and county GeoJson files to draw everything.
I also have a self created "cities" file that contains major cities for each state. The coordinates are accurate and everything looks good.
When a user clicks on a given state, I hide all state shapes and then calculate the transform needed to get the county shapes for that state to fit within my viewport. I then apply that transform to all the necessary county shapes in order to get the "zoomed" view. My code is as follows:
function CalculateTransform(objectPath)
{
var results = '';
// Define bounds/points of viewport
var mapDimensions = getMapViewportDimensions();
var baseWidth = mapDimensions[0];
var baseHeight = mapDimensions[1];
var centerX = baseWidth / 2;
var centerY = baseHeight / 2;
// Get bounding box of object path and calculate centroid and zoom factor
// based on viewport.
var bbox = objectPath.getBBox();
var centroid = [bbox.x + bbox.width / 2, bbox.y + bbox.height / 2];
var zoomScaleFactor = baseHeight / bbox.height;
var zoomX = -centroid[0];
var zoomY = -centroid[1];
// If the width of the state is greater than the height, scale by
// that property instead so that state will still fit in viewport.
if (bbox.width > bbox.height) {
zoomScaleFactor = baseHeight / bbox.width;
}
// Calculate how far to move the object path from it's current position to
// the center of the viewport.
var augmentX = -(centroid[0] - centerX);
var augmentY = -(centroid[1] - centerY);
// Our transform logic consists of:
// 1. Move the state to the center of the screen.
// 2. Move the state based on our anticipated scale.
// 3. Scale the state.
// 4. Move the state back to accomodate for the scaling.
var transform = "translate(" + (augmentX) + "," + (augmentY) + ")" +
"translate(" + (-zoomX) + "," + (-zoomY) + ")" +
"scale(" + zoomScaleFactor + ")" +
"translate(" + (zoomX) + "," + (zoomY) + ")";
return results;
}
...and the binding function
// Load county data for the state specified.
d3.json(jsonUrl, function (json) {
if (json === undefined || json == null || json.features.length == 0)
{
logging.error("Failed to retrieve county structure data.");
showMapErrorMessage("Unable to retrieve county structure data.");
return false;
}
else
{
counties.selectAll("path")
.data(json.features)
.enter()
.append("path")
.attr("id", function (d, i) {
return "county_" + d.properties.GEO_ID
})
.attr("data-id", function (d, i) { return d.properties.GEO_ID })
.attr("data-name", function (d, i) { return countyLookup[d.properties.GEO_ID] })
.attr("data-stateid", function (d, i) { return d.properties.STATE })
.attr("d", path);
// Show all counties for state specified and apply zoom transform.
d3.selectAll(countySelector).attr("visibility", "visible");
d3.selectAll(countySelector).attr("transform", stateTransform);
// Show all cities for the state specified and apply zoom transform
d3.selectAll(citySelector).attr("visibility", "visible");
d3.selectAll(citySelector).attr("transform", stateTransform);
}
});
This works fine here, except for really small states, the zoom factor is much larger, and the circles get distored.
Is there a way to force the size of the points to be a fixed size (say a 15px radius) even after the transform occurs?
For things you don't want to scale, just make them divided by 'scale' . In my case,
var zoom = d3.behavior.zoom()
.on("zoom",function() {
g.attr("transform","translate("+d3.event.translate.join(",")+")scale("+d3.event.scale+")");
g.selectAll(".mapmarker")
.attr("r",6/d3.event.scale)
.attr("stroke-width",1/d3.event.scale);
});
This is happening because you are setting a scale transform instead of scaling the positions. You can see the difference here Basically it is the difference between:
// Thick lines because they are scaled too
var bottom = svg.append('g').attr('transform', 'scale('+scale+','+scale+')');
bottom.selectAll('circle')
.data(data)
.enter().append('circle')
.attr('cx', function(d) { return d.x; })
.attr('cy', function(d) { return d.y; });
and
// line thicknesses are nice and thin
var top = svg.append('g');
top.selectAll('circle')
.data(data)
.enter().append('circle')
.attr('cx', function(d) { return d.x * scale; })
.attr('cy', function(d) { return d.y * scale; });
With mapping probably you best solution is to compute your offset and scale as you do and then add them into your projection function - you want to directly modify the post-projection x and y values. If you update your projection function properly you should not have to do anything else to apply the appropriate zoom to your map.

Resources