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.
Related
I figured out how to change arc length of a pie chart with new values using d3 following Bostock's Pie Chart Update, but I'm having a hard time figuring out how to also animate the radius of the pic chart at the same time.
This a snippet of my current code:
pie_inner.value(function(d) {
return d.toward === toward || toward === "both" ? d.spent : 0;
});
path_inner
.data(function(d) {
return pie_inner(d.data).map(function(m) {
m.r = d.r;
return m;
})
})
.transition()
.duration(750)
.attrTween("d", function(d) {
var interpolate = d3.interpolate(this._current, d);
this._current = interpolate(0);
return function(t) {
arc_inner.innerRadius(d.r - thickness);
arc_inner.outerRadius(d.r);
return arc_inner(interpolate(t));
};
});
With this code the arc length animates to the new values nicely but the radius "jumps" to the new value.
I see that this code animates both the arc length and the radius but it does it in sequence as opposed to the same time.
I was setting the radius using the function outerRadius(), but I figured out if I just set the property, then it works:
pie_inner.value(function(d) {
return d.toward === toward || toward === "both" ? d.spent : 0;
});
path_inner
.data(function(d) {
return pie_inner(d.data).map(function(m) {
m.r = d.r;
return m;
})
})
.transition()
.duration(750)
.attrTween("d", function(d) {
d.innerRadius = d.r - thickness;
d.outerRadius = d.r;
var interpolate = d3.interpolate(this._current, d);
this._current = interpolate(0);
return function(t) {
return arc_inner(interpolate(t));
};
});
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";
})
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) + ')';
})
});
I am working on a d3 donut and am stuck on how to update the donut value where the value will flow back if for instance you change the value from 42 to 17.
I can remove the svg, but then it rewrites the new value (17) from the zero position.
I would like it to flow backwards from 42 say to 17.
var path = svg.selectAll("path")
.data(pie(dataset.lower))
.enter().append("path")
.attr("class", function(d, i) { return "color" + i })
.attr("d", arc)
.each(function(d) { this._current = d; }); // store the initial values
here is a link to my jsfidle http://jsfiddle.net/yr595n96/
and I would love any help you could offer.
Thanks
Heres you're new button click :
$("#next").click(function () {
percent = 17;
var progress = 0;
var timeout = setTimeout(function () {
clearTimeout(timeout);
var randNumber = Math.random() * (100 - 0 + 1) + 0;
//path = path.data(pie(calcPercent(17))); // update the data << change this
path = path.data(pie(calcPercent(randNumber)));
path.transition().duration(duration).attrTween("d", function (a) {
// Store the displayed angles in _current.
// Then, interpolate from _current to the new angles.
// During the transition, _current is updated in-place by d3.interpolate.
var i = d3.interpolate(this._current, a);
var i2 = d3.interpolate(progress, randNumber)
this._current = i(0);
return function(t) {
text.text( format(i2(t) / 100) );
return arc(i(t));
};
}); // redraw the arcs
}, 100);
});
Notice on line '8':
path = path.data(pie(calcPercent(randNumber)));
You need to pass new data for it to transition to. I have used a random number here just to show you any number works. I made a variable for this and passed it to both the 'path' and the text : 'i2' > d3.interpolate.
You can always just change it to 17 to suit what you asked for :)
Updated fiddle : http://jsfiddle.net/yr595n96/3/
I want to draw multiple barcharts into the same visualization.
When the first barchart is drawn it ḱnows nothing about the domain of the 2nd, 3rd, etc. At the time the 2nd barchart is drawn the domain of the y scale changes. 'Consequently the first barchart needs to be redrawn. The question is what is a good way to wire up the redraw since the scale/domain has no change notification mechanism.
http://bl.ocks.org/markfink/4d8f1c183e6cd9d6ea07
Here's one implementation: http://jsfiddle.net/tBHyD/2/
I only tried to address the setup in the question, not the full implementation noted in your comment. There are lots of ways to accomplish this; this one uses an event-driven model, using d3.dispatch:
var evt = d3.dispatch("change");
The key here is to update the scale extents globally, then fire an event if they've changed. Here I use a function, updateExtent, for this purpose:
var x0 = Infinity,
x1 = -Infinity,
y0 = Infinity,
y1 = -Infinity;
function updateExtent(data) {
var extx = d3.extent(data, function(d) { return d[0]; }),
exty = d3.extent(data, function(d) { return d[1]; }),
changed;
// update
if (extx[0] < x0) { x0 = extx[0]; changed = true; }
if (extx[1] > x1) { x1 = extx[1]; changed = true; }
if (exty[0] < y0) { y0 = exty[0]; changed = true; }
if (exty[1] > y1) { y1 = exty[1]; changed = true; }
// if changed, update scales and fire event
if (changed) {
// update scales
x.domain([x0, x1]);
y.domain([y1, y0]);
// update axes
vis.select(".x.axis").call(xAxis);
vis.select(".y.axis").call(yAxis);
// fire event
evt.change();
}
}
Then the redraw function sets a listener:
function redraw(selection, data, style) {
var bar = selection.selectAll(".bar")
.data(data);
// enter
bar.enter().append("rect")
.attr('class', "bar")
.attr("width", 5)
.style(style);
function updateBar() {
// update
bar
.attr("x", function(d) { return x(d[0]) - .5; })
.attr("y", function(d) { return y(d[1]); })
.attr("height", function(d) { return height - y(d[1]); });
}
// initial call
updateBar();
// handler call
evt.on("change", updateBar);
};
Note that now you don't need to set the extent explicitly:
var data1 = [[2,0.5], [4,0.8], [6,0.6], [8,0.7], [12,0.8]];
updateExtent(data1);