Fading paths in d3.js - d3.js

Is there a way to create fading paths in d3.js, similar to the wind lines in the famous earth wind visualization:
http://earth.nullschool.net/
In principle, it seems like I could create a path with a lot of waypoints and transition each of the end segments toward transparency, but that seems hacky. Any better ideas?

To follow up on the example offered, here is a block on a reduced version. It is based on canvass as mentioned. The block illustrates the technique that I mentioned in my comments where the rendering engine is aggressively challenged which gives the blur effect for free. Click on the normal button to change to synchronous redraws and you can see that the blurring effect dissapears. This is achieved in the following code...
function run() {
paused = false;
then = Date.now();
now = then;
particles.forEach(function(p) {
p.t = now + (Math.random() - 1) * duration;
});
d3.timer(function(elapsed) {
var i = -1, n = particles.length;
var f = format(" >8.1f"), f3 = format(" >8.4f");
var normal = modeButton.text() == "normal", t;
if(normal) {
// clear the shadow context and copy the current context to it
offscreenContext.clearRect(0, 0, width, height);
offscreenContext.drawImage(canvas, 0, 0, width, height)
//clear the current context and try to jam the previous version
//back on top of the drawing activities
//lovely chaos ensues
context.clearRect(0, 0, width, height);
context.drawImage(offscreenCanvas, 0, 0, width, height);
}
else {
//allow the
context.clearRect(0, 0, width, height);
context.drawImage(offscreenCanvas, 0, 0, width, height);
offscreenContext.clearRect(0, 0, width, height);
}
now = elapsed + then;
//iterate the transition for each county
while(++i < n) {
var p = particles[i], t = (now - p.t) / duration;
if(t > 1) {
p.t += duration * Math.floor(t);
p.y = p.d.centroid[1] + (true ? (Math.random() - .5) * 2 : 0);
}
else if(t > 0) {
if(!normal){
offscreenContext.fillStyle = "rgba(" + p.r + "," + p.g + "," + p.b + "," + mirror(1 - t) + ")";
offscreenContext.fillRect(p.x + (t - .5) * p.v * 2 - .75, p.y - .75, 2.5, 2.5);
} else {
context.fillStyle = "rgba(" + p.r + "," + p.g + "," + p.b + "," + mirror(1 - t) + ")";
context.fillRect(p.x + (t - .5) * p.v * 2 - .75, p.y - .75, 1.5, 1.5);
}
}
}
return paused;
}, 0, now);
}
});

Here's a similar effect implemented in d3.js, though the code is compressed. You might find a way to reverse engineer it or find an uncompressed source.
It seems to me like each arcs is drawn only once it's the color that has a transition applied with a gradient that changes over time, and then is removed. I can't quite tell if all arcs share the same gradient and just start from different places, and appear at different times. That might be a great shortcut.
http://www.nytimes.com/interactive/2012/11/11/sunday-review/counties-moving.html
There's a presentation how this came to be, so perhaps they've talked about the tech elsewhere?
http://www.slideshare.net/openjournalism/amanda-cox-visualizing-data-at-the-new-york-times
(Quick note, I realize this isn't actually an answer, just needed more space than the comments section allows)

Related

D3JS v5 Zoom - The transform value changes between zoom on button and mouse wheel

I'm working on a project using D3js V5 that takes the input of a CSV file and returns a Workflow diagram, everything is working fine besides on it's own.
My problem is when I fit the workflow, through a Fit Button, and after that I use the mouse wheel to zoom in or out it just jumps to the previous zoom and position settings.
After some research I've found that after D3v4+ that "Zoom behaviours no longer store the active zoom transform (i.e., the visible region; the scale and translate) internally. The zoom transform is now stored on any elements to which the zoom behaviour has been applied.(link)"
That being said I cannot make it work and that's why I'm asking for your help.
The code is has follows:
document.getElementById('fitBTN').addEventListener('click', zoomFit);
function zoomFit() {
let bounds = document.getElementById('svgContent').getBBox();
let parent = document.getElementById('svgContent').parentElement;
let fullWidth = parent.clientWidth,
fullHeight = parent.clientHeight;
let width = bounds.width,
height = bounds.height;
let midX = bounds.x + width / 2,
midY = bounds.y + height / 2;
if (width == 0 || height == 0) return; // nothing to fit
let scale = 0.95 / Math.max(width / fullWidth, height / fullHeight);
let translate = [
fullWidth / 2 - scale * midX,
fullHeight / 2 - scale * midY
];
document
.getElementById('svgContent')
.setAttribute(
'transform',
'translate(' +
translate[0] +
',' +
translate[1] +
') scale(' +
scale +
')'
);
}
svg
.call(
zoom()
.on('zoom', () => {
document
.getElementById('svgContent')
.setAttribute(
'transform',
'translate(' +
currentEvent.transform.x * 0.125 +
',' +
currentEvent.transform.y * 0.125 +
') scale(' +
currentEvent.transform.k +
')'
);
})
.scaleExtent([0.1, 1])
)
.on('dblclick.zoom', false)
.on('wheel', function() {
currentEvent.preventDefault();
})
.on('wheel', false);
I believe that I have to had some variable that is able to read the state of the transform translate, but how?
So the answer provided by #AndrewReid has helped me resolve my issue and the corrected version of the code is here:
function zoomFit() {
let bounds = document.getElementById('svgContent').getBBox();
let parent = document.getElementById('svgContent').parentElement;
let fullWidth = parent.clientWidth,
fullHeight = parent.clientHeight;
let width = bounds.width,
height = bounds.height;
let midX = bounds.x + width / 2,
midY = bounds.y + height / 2;
if (width == 0 || height == 0) return; // nothing to fit
let scale = 0.95 / Math.max(width / fullWidth, height / fullHeight);
let translate = [
fullWidth / 2 - scale * midX,
fullHeight / 2 - scale * midY
];
document
.getElementById('svgContent')
.setAttribute(
'transform',
'translate(' +
translate[0] +
',' +
translate[1] +
') scale(' +
scale +
')'
);
// Create a zoom transform from d3.zoomIdentity
var transform = zoomIdentity
.scale(scale)
.translate(-translate[0], -translate[1]);
// Apply the zoom and trigger a zoom event:
svg.call(zoom().transform, transform);
}
var zooming = zoom()
.scaleExtent([0.1, 1])
.translateExtent([[0, 0], [840, 740]])
.on('zoom', zoomed);
svg.call(zooming);
function zoomed() {
svg
.attr('transform', currentEvent.transform)
.on('dblclick.zoom', false)
.on('wheel', function() {
currentEvent.preventDefault();
});
}
But another issue got in the way which is the panning function was lost.
I'll have to figure that one out and I'll update this answer has soon has the solutions is found.
Best regards,

Moving a line geometry with three.js using GLSL

I have a line geometry made with three.js, which I want to move as a spermatozoid (i.e. the head moves first, then all of the points along the tail move accordingly to the head) using GLSL.
Here is a visual representation of what I want:
So as you can see, point G eases to (follows) H, which in turn follows point E, which in turn follows the head.
I have a working examples, being animated on the CPU. Here is the code:
class Boid {
constructor (position) {
this.position = position
this.speed = 0.0009 + Math.random() * 0.0003
this.pointsNum = 12
this.points = []
this.line = null
this.angle = Math.random() * 360
for (let i = 0; i < this.pointsNum; i += 1) {
this.points.push(new THREE.Vector3(1, 1, 1))
}
this.angle = 0
}
update (target, time) {
if (time) {
this.line.geometry.verticesNeedUpdate = true
this.line.geometry.vertices.forEach((p, i) => {
let nextP = this.line.geometry.vertices[i + 1]
if (nextP) {
// if it's not the HEAD point, follow the next point in the geometry vertices
p.x += (nextP.x - p.x) * (time * 8.0)
p.y += (nextP.y - p.y) * (time * 8.0)
p.z += (nextP.z - p.z) * (time * 8.0)
} else {
// if the point is in fact the head, ease it according to some random moving point in our scene (target)
p.x += (target.x - p.x) * time
p.y += (target.y - p.y) * time
p.z += (target.z - p.z) * time
}
})
}
}
}
And here is a working example.
This technique is working, but would like to accomplish the same stuff with GLSL. My question is how should I approach it? Should I pass the next vertex position to the previous one and ease in my vertex shader? How should I keep track of the next's point position?
Any help is more then appreciated, I have been thinking about this a lot without any success.

Rotate every arc of pie chart 180 (like sun) with D3 JS. How to calculate translate parameters

I am working on pie chart with d3 js. I want to rotate every arc of my pie chart 180. I know that I am unable to explain completely show here is my fiddle link.
[fiddle]: https://jsfiddle.net/dsLonquL/
How can i get dynamic parameters for translate() function.
Basically you need to work out the centre point of the edge of each arc. I used this example for help : How to get coordinates of slices along the edge of a pie chart?
This works okay, but I needed to rotate the points to get them in the correct positions. As it is in radians the rotation is the following :
var rotationInRadians = 1.5708 * 1.5;
Now using the example before I used the data for the paths, so the start and end angle and got the center points like so :
var thisAngle = (d.startAngle + rotationInRadians + (d.endAngle + rotationInRadians - d.startAngle + rotationInRadians) / 2);
var x = centreOfPie[0] + radius * 2 * Math.cos(thisAngle)
var y = centreOfPie[1] + radius * 2 * Math.sin(thisAngle)
I created a function to show circles at these points to clarify :
function drawCircle(points, colour) {
svg.append('circle')
.attr('cx', points[0])
.attr('cy', points[1])
.attr('r', 5)
.attr('fill', colour);
}
Called it inside the current function like so :
drawCircle([x, y], color(d.data.label))
And then translated and rotated accordingly :
return 'translate(' + (x) + ',' + y + ') rotate(180)';
I added a transition so you can see it working. Here is the final fiddle :
https://jsfiddle.net/thatOneGuy/dsLonquL/7/
EDIT
In your comments you say you want the biggest segment to be kept in the middle. So we need to run through the segments and get the biggest. I have also taken care of duplicates, i.e if two or more segments are the same size.
Here is the added code :
var biggestSegment = {
angle: 0,
index: []
};
path.each(function(d, i) {
var thisAngle = (d.endAngle - d.startAngle).toFixed(6);//i had to round them as the numbers after around the 7th or 8th decimal point tend to differ tet theyre suppose to be the same value
if (i == 0) {
biggestSegment.angle = thisAngle
} else {
if (biggestSegment.angle < thisAngle) {
biggestSegment.angle = thisAngle;
biggestSegment.index = [i];
} else if (biggestSegment.angle == thisAngle) {
console.log('push')
biggestSegment.index.push(i);
}
}
})
Now this goes through each path checks if its bigger than the current value, if it is overwrite the biggest value and make note of the index. If its the same, add index to index array.
Now when translating the paths, you need to check the current index against the index array above to see if it needs rotating. Like so :
if (biggestSegment.index.indexOf(i) > -1) {
return 'translate(' + (centreOfPie[0]) + ',' + (centreOfPie[1]) + ')' // rotate(180)';
} else {
return 'translate(' + (x) + ',' + y + ') rotate(180)';
}
Updated fiddle : https://jsfiddle.net/thatOneGuy/dsLonquL/8/
I have editted 3 values to be different to the rest. Go ahead and change these, see what you think :)
This is a pure middle school geometry job.
CASE 1: The vertex of each sector rotation is on the outer line of the circle
fiddle
// ... previous code there
.attr('fill', function(d, i) {
return color(d.data.label);
})
.attr("transform", function(d, i) {
var a = (d.endAngle + d.startAngle) / 2, // angle of vertex
dx = 2 * radius * Math.sin(a), // shift/translate is two times of the vertex coordinate
dy = - 2 * radius * Math.cos(a); // the same
return ("translate(" + dx + " " + dy + ") rotate(180)"); // output
});
CASE 2: The vertex on the center of the chord
fiddle
// ... previous code there
.attr('fill', function(d, i) {
return color(d.data.label);
})
.attr("transform", function(d, i) {
var dx = radius * (Math.sin(d.endAngle) + Math.sin(d.startAngle)), // shift/translation as coordinate of vertex
dy = - radius * (Math.cos(d.endAngle) + Math.cos(d.startAngle)); // the same for Y
return ("translate(" + dx + " " + dy + ") rotate(180)"); // output
});

Compose an image with floating point layers in webgl

I have trying to render an image in the browser which is built like this:
A bunch of rectangles are each filled with a radial gradient (ideally Gaussian, but can be approximated with a few stopping points
Each rectangle is rotated and translated before being deposited on a drawing area
The image is flattened by summing all the intensities of the rectangles (and cropping to the drawing area's dimensions )
The intensity is rescaled so that the highest intensity is 255 and the lowest 0 (ideally I can apply some sort of gamma correction too)
Finally an image is drawn where the color of each pixel is taken from a palette of 256 colors.
The reason I cannot do this easily with a canvas object is that I need to be working in floating points or I'll lose precision. I do not know in advance what the maximum intensity and minimum intensity will be, so I cannot merely draw transparent rectangles and hope for the best.
Is there a way to do this in webgl? If so, how would I go about it?
You can use the regular canvas to perform this task :
1) check min/max of your rects, so you can build a mapping function double -> [0-255] out of that range.
2) draw the rects in 'lighter' mode == add the component values.
3) you might have a saturation when several rects overlaps : if so, double the mapping range and go to 2).
Now if you don't have saturation just adjust the range to use the full [0-255] range of the canvas, and you're done.
Since this algorithm makes use of getImageData, it might not reach 60 fps on all browsers/devices. But more than 10fps on desktop/Chrome seems perfectly possible.
Hopefully the code below will clarify my description :
//noprotect
// boilerplate
var cv = document.getElementById('cv');
var ctx = cv.getContext('2d');
// rectangle collection
var rectCount = 30;
var rects = buildRandRects(rectCount);
iterateToMax();
// --------------------------------------------
function iterateToMax() {
var limit = 10; // loop protection
// initialize min/max mapping based on rects min/max
updateMapping(rects);
//
while (true) {
// draw the scene using current mapping
drawScene();
// get the max int value from the canvas
var max = getMax();
if (max == 255) {
// saturation ?? double the min-max interval
globalMax = globalMin + 2 * (globalMax - globalMin);
} else {
// no sauration ? Just adjust the min-max interval
globalMax = globalMin + (max / 255) * (globalMax - globalMin);
drawScene();
return;
}
limit--;
if (limit <= 0) return;
}
}
// --------------------------------------------
// --------------------------------------------
// Oriented rectangle Class.
function Rect(x, y, w, h, rotation, min, max) {
this.min = min;
this.max = max;
this.draw = function () {
ctx.save();
ctx.fillStyle = createRadialGradient(min, max);
ctx.translate(x, y);
ctx.rotate(rotation);
ctx.scale(w, h);
ctx.fillRect(-1, -1, 2, 2);
ctx.restore();
};
var that = this;
function createRadialGradient(min, max) {
var gd = ctx.createRadialGradient(0, 0, 0, 0, 0, 1);
var start = map(that.min);
var end = map(that.max);
gd.addColorStop(0, 'rgb(' + start + ',' + start + ',' + start + ')');
gd.addColorStop(1, 'rgb(' + end + ',' + end + ',' + end + ')');
return gd;
}
}
// Mapping : float value -> 0-255 value
var globalMin = 0;
var globalMax = 0;
function map(value) {
return 0 | (255 * (value - globalMin) / (globalMax - globalMin));
}
// create initial mapping
function updateMapping(rects) {
globalMin = rects[0].min;
globalMax = rects[0].max;
for (var i = 1; i < rects.length; i++) {
var thisRect = rects[i];
if (thisRect.min < globalMin) globalMin = thisRect.min;
if (thisRect.max > globalMax) globalMax = thisRect.max;
}
}
// Random rect collection
function buildRandRects(rectCount) {
var rects = [];
for (var i = 0; i < rectCount; i++) {
var thisMin = Math.random() * 1000;
var newRect = new Rect(Math.random() * 400, Math.random() * 400, 10 + Math.random() * 50, 10 + Math.random() * 50, Math.random() * 2 * Math.PI, thisMin, thisMin + Math.random() * 1000);
rects.push(newRect);
}
return rects;
}
// draw all rects in 'lighter' mode (=sum values)
function drawScene() {
ctx.save();
ctx.globalCompositeOperation = 'source-over';
ctx.clearRect(0, 0, cv.width, cv.height);
ctx.globalCompositeOperation = 'lighter';
for (var i = 0; i < rectCount; i++) {
var thisRect = rects[i];
thisRect.draw();
}
ctx.restore();
}
// get maximum value for r for this canvas
// ( == max r, g, b value for a gray-only drawing. )
function getMax() {
var data = ctx.getImageData(0, 0, cv.width, cv.height).data;
var max = 0;
for (var i = 0; i < data.length; i += 4) {
if (data[i] > max) max = data[i];
if (max == 255) return 255;
}
return max;
}
<canvas id='cv' width = 400 height = 400></canvas>

How to repeat rotation using d3

I'm trying to figure out how to repeat a transition. I' m using world tour with my own tsv file. The tsv file s much smaller which ends the world tour quickly.
How can I repeat the rotation so its starts at beginning?
//Globe rotating
(function transition() {
d3.transition()
.duration(1500)
.each("start", function() {
title.text(countries[i = (i + 1) % n].name);
})
.style("color", "lightgreen")
.style("text-anchor", "middle")
.tween("rotate", function() {
var p = d3.geo.centroid(countries[i]),
r = d3.interpolate(projection.rotate(), [-p[0], -p[1]]);
return function(t) {
projection.rotate(r(t));
c.clearRect(0, 0, width, height); //clear the canvas for redrawing
c.fillStyle = "black", c.beginPath(), path(land), c.fill();
c.fillStyle = "lightgreen", c.beginPath(), path(countries[i]), c.fill();
c.strokeStyle = "green", c.lineWidth = .5, c.beginPath(), path(borders), c.stroke();
c.strokeStyle = "#000", c.lineWidth = 2, c.beginPath(), path(globe), c.stroke();
};
})
.transition()
.each("end", transition);
})();
}
One option would be to reset i to zero when it exceeds the number of countries in your list. Something like this:
.each("start", function() {
i = (i + 1) % n;
if(i >= names.length)
i = 0;
title.text(countries[i].name);
})
Edit: After looking at the World Tour example code, a simpler solution would be redefine n to be the length of your data (instead of the number of countries on the map):
n = names.length; // instead of countries.length
Then you can leave the rest of the code as is. The modulo in this expression - i = (i + 1) % n - will reset to zero once you reach the end of your list.

Resources