Implementing complex Processing pushMatrix/popMatrix logic with d3js - javascript

I want to reimplement the following Processing sketch with d3js.
Recursion Thing
This wonderful sketch recursively builds up the graph, using a complex pushMatrix, popMatrix hierarchy.
How could this be implemented in d3.js as we there always work on the DOM immediately when appending a shape or transformation. But in a logic like in this sketch the appending part seems to have to be held back for the respective popMatrix to come. It feels like I have to implement my own transform and shape stack to temporary remember the transformation and shapes to be added until the popMatrix comes but that seems so not d3.js.
Any suggestion highly appreciated
ps:
i dont want to use processing.js as i want to work with svg, not canvas.

Interesting problem! Here's my take on it: http://jsfiddle.net/Y48BL/
This is more a proof of concept; I didn't do all the different colours and such. Nevertheless, it demonstrates the general approach. The general idea is to use g elements instead of the matrices that processing uses. Both are used for local transformations of the coordinate system; in the case of the g elements by setting transform accordingly. New gs (matrices) are created inside the recursive function and then passed on to the next level of the recursion. This would correspond to pushMatrix(). Coming back up, we continue to use the original g, corresponding to popMatrix().
The translation of the drawing of the circles and lines is fairly straightforward -- I find the D3 code much easier to read.

So I came up with this helper "class" to get this done, maybe a bit of a overkill but I will have more use cases for this.
var TransformStack = (function () {
function TransformStack() {
this.stack = [];
}
TransformStack.prototype.getCurrentElement = function () {
return this.stack[this.stack.length - 1];
};
TransformStack.prototype.setCurrentElement = function (element) {
this.stack[this.stack.length - 1] = element;
};
TransformStack.prototype.push = function (transformElement) {
this.stack.push(transformElement);
};
TransformStack.prototype.pushAndTransform = function (transformAttr) {
this.push(this.getCurrentElement().append("g").attr("transform", transformAttr));
};
TransformStack.prototype.transform = function (transformAttr) {
this.setCurrentElement(this.getCurrentElement().append("g").attr("transform", transformAttr));
};
TransformStack.prototype.pop = function () {
return this.stack.pop();
};
return TransformStack;
})();
Basically a stack to push/pop g elements which replaces the matrices approach in processing as Lars already pointed out. With this the main routine looks something like
var svg = d3.select("body").append("svg").attr("width", width).attr("height", height)
.append("g").attr("transform", "translate(" + width / 2 + "," + height / 2 + ")scale(" + gScale + ")");
var tstack = new TransformStack();
tstack.push(svg);
doIt(nRecursions);
function doIt(n) {
// Circle
tstack.getCurrentElement()
.append("circle")
.attr("r", theSize)
.style("fill", "#fe6b0c")
.style("stroke", "0")
.style("stroke-width", "2")
.style("opacity", 0.3);
if (n != nRecursions) {
for (var i = 0; i < 4; i++) {
tstack.getCurrentElement().append("line")
.style("stroke", "red")
.style("opacity", 0.16)
.attr("x1", Math.random() * 4 - 2)
.attr("x2", Math.random() * 4 - 2)
.attr("y1", theSize / 2.0 + Math.random() * 4 - 2)
.attr("y2", distance - (theSize * theScale / 2.0) - 8.0 + Math.random() * 4 - 2);
}
}
var rot = 0;
tstack.pushAndTransform("scale(" + theScale + ")");
for (var i = 0; i < n; i++) {
if (n > 0) {
tstack.pushAndTransform("translate(0," + distance + ")");
doIt(n - 1);
tstack.pop();
rot = 360 / n;
tstack.transform('rotate(' + rot + ')');
}
}
tstack.pop();
}
}
Just wanted to share this, maybe of some use for some. The main point was given by Lars already.

Related

how to create three-level donut chart in d3.js

I'm using trying to create a multi-level donut chart in d3 version5
This image is drawn by d3 version3. it is working fine in version3. I decided to upgrade d3 to the latest version. now, donut chart is not drawn by d3(also no errors in the console)
D3 version 3 > version 5
Here is the sample dataset I used:
Hint: first value in the array is used storage and second is free storage
{
average: [30.012, 69.988],
minimum: [10, 90],
maximum: [40, 60]
}
Note: Above data is just a sample this is not exact data.
Here is the code I tried:
var width = 300;
var height = 300;
var radius = Math.floor((width / 6) - 2);
var classFn = function(a, b) {
return a === 0 ? classes[b] : 'default';
};
var pie = d3.layout.pie().sort(null);
var arc = d3.svg.arc();
var svg = d3.select(selector).append("svg");
svg.attr("width", width);
svg.attr("height", height);
svg = svg.append("g");
svg.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var gs = svg.selectAll("g").data(d3.values(dataset)).enter().append("g");
var path = gs.selectAll("path");
path = path.data(function(d) {
return pie(d);
});
path.enter().append("path");
path.attr("class", function(d, i, j) {
return classFn(i, j);
})
path.attr("d", function(d, i, j) {
return arc.innerRadius((j === 0 ? 0 : 2) + radius * j).outerRadius(radius * (j + 1))(d);
});
Note: This code is working fine in d3 version3.
2. Update:
I've updated the answer with a better solution. I didn't do this at first, because I didn't grasp you structure. I've updated it to being more D3 idiomatic. Plus it does away with the hack I made in my first update :)
var dataset = {
average: [0, 100],
minimum: [0, 100],
maximum: [0, 100]
}
var width = 300;
var height = 300;
var radius = Math.floor((width / 6) - 2);
var pie = d3.pie().sort(null);
var arc = d3.arc();
var svg = d3.select('body').append("svg");
svg.attr("width", width);
svg.attr("height", height);
svg = svg.append("g");
svg.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var gs = svg.selectAll("g").data(d3.values(dataset)).enter().append("g");
gs.each(function (d, j) {
d3.select(this).selectAll('path')
.data(pie(d)).enter()
.append('path')
.attr("class", function(d, i) {
// return classFn(i);
})
.attr('d', function (d) {
return arc
.innerRadius((j === 0 ? 0 : 2) + radius * j)
.outerRadius(radius * (j + 1))(d);
})
})
The updated code uses the index (here j) that is available when appending the g elements, which corresponds to you original j index. This makes it possible to calculate the radii in the original way.
To achieve this, the arc appending code is wrapped into a .each function that iterates over the g elements, making j available to us.
The class application should work as well, but I've commented it out, as the classFn function doesn't work, since the classes variable is not present.
1. Update:
Besides the original answer, when calculating the arc radii you rely on a j value that is different from D3 v3 and v5. I summise that j is used the index of the d3.values array, so I've cooked up a way to reverse look-up that index based on the input values.
First create a map for reverse mapping data values into their corresponding index:
var dataValueJoinChar = 'ยค'
var datasetValuesToIndex = d3.values(dataset).reduce((acc, curr, i) => {
acc[`0${dataValueJoinChar}${curr[0]}`] = i
acc[`1${dataValueJoinChar}${curr[1]}`] = i
return acc
}, {})
Then change the last part of your code to:
path = path.data(function(d) {
return pie(d);
}).enter().append("path");
path.attr("class", function(d, i, j) {
return classFn(i, j);
})
path.attr("d", function(d, i, j) {
var orgIndex = datasetValuesToIndex[`${i}${dataValueJoinChar}${d.data}`]
return arc
.innerRadius((orgIndex === 0 ? 0 : 2) + radius * orgIndex)
.outerRadius(radius * (orgIndex + 1))(d);
});
It might not be too pretty, but it's a simple adaption of your code that works.
------- Original answer --------
In D3 v5 pie and arc are found at d3.pie and d3.arc respectively. Therefore, try changing:
var pie = d3.layout.pie().sort(null);
var arc = d3.svg.arc();
To this instead:
var pie = d3.pie().sort(null);
var arc = d3.arc();
Pie API reference: https://github.com/d3/d3-shape/blob/v1.3.4/README.md#pie
Arc API reference: https://github.com/d3/d3-shape/blob/v1.3.4/README.md#arc
If you use a bundler to bundle sub-modules, both are part of the d3-shape module. If not they are both available in the full D3 library.
Hope this helps!

Amending D3 circle animation

This is a very simple question. Here is my jsfiddle, where I animate a circle between two nodes using D3 (original code is Mike Bostock's): http://jsfiddle.net/Guill84/uxy8d9vs/5/.
How do I stop the circle from returning to node A when it's reached node B? The relevant bit of code is as follows:
var path = d3.select("path#AB"),
startPoint = pathStartPoint(path);
marker.attr("r", 7)
.attr("transform", "translate(" + startPoint + ")");
transition();
//Get path start point for placing marker
function pathStartPoint(path) {
var d = path.attr("d"),
dsplitted = d.split(" ");
return dsplitted[1].split(",");
}
function transition() {
marker.transition()
.duration(2000)
.attrTween("transform", translateAlong(path.node()));
}
function translateAlong(path) {
var l = path.getTotalLength();
return function(i) {
return function(t) {
var p = path.getPointAtLength(t * l);
return "translate(" + p.x + "," + p.y + ")";//Move marker
}
}
}
var l = path.getTotalLength() / 2;
Because your path is two arcs, one from A to B, the other from B to A. Traversing the full path it's always going to return to A. Doing half the length of the path it stops at B.
On another point your fiddle ends up crashing chrome on my pc, I think it's to do with calling the transition from itself ('the infinite loop' bit)

D3 Mathematical Function Create

I would like to create a mathematical function for my D3 page.
Every node has got a "score" that I can get via d.score
I like to create a function that draws the radius of a circles based on that score.
I thought about following:
r <= 10, r >= 3
10 - (d.score / 50) = r
How could I do this in D3?
.attr('r', function(d) {
return node_size( * * ? ? ? * * );
});
Kind regards,
Marc
Please be advised, that this might be a bad question, due to it's "please give me code for this problem" character.
Anyway, i think this is what you want to try i guess:
.attr('r', function(d) {
var r = 10 - (d.score / 50);
if (r > 10) {r = 10;}
else if (r < 3) {r = 3;}
return r;
});
First you need to define a linear scale that would map the value of your node_size to an actual radius, as follows (example):
r = d3.scale.linear().range([3,10]);
Then specify as a domain, the values of your score parameter as follows (assuming DATA is your data structure)
// first define your radius according to your formula specified in your question.
DATA.forEach(function(d){
d.radius=10 - (d.score / 50);
})
//Then, assign is as a domain of your radius function
r.domain(d3.extent(DATA.map(function(d){return d.radius;})))
Finally, when plotting your data you should do something like this:
.attr('r', function(d) {return r(d.radius)});
You could merge or optimize these steps, but I think it is a good start.
Hope this helps.

d3 steady horizontal transition along an SVG path

I'm using a d3 attrTween to translate a circle over a path smoothly, similar to this example and as shown in the picture below:
The circle's transition is defined here:
function transition() {
circle.transition()
.duration(2051)
.ease("linear")
.attrTween("transform", translateAlong(path.node()))
}
And the attribute tween is shown here:
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 + ")";
};
};
}
This works well thanks to the SVG method getPointAtLength, which allows us to retrieve coordinates at different lengths of the path. However, I need a different kind of behavior and I've been unable to come up with a solution so far.
I need the circle to animate along the path, but at a steady horizontal speed. Meaning that the circle ought to take as much time to navigate this slice:
As it does with this slice:
Because both slices encompass the same width. On a low level, what I need is to be able to translate any X coordinate with its corresponding Y coordinate along the path. I've looked at all the SVG path methods and I haven't found anything particularly useful here. I'm hoping there's some way in D3 to feed an X coordinate to a d3 line and retrieve its corresponding Y coordinate.
Here's a JSFiddle working as described above. I'd really appreciate any help I can get on this. Thanks!
I ended up creating a lookup array for all my points along the line using getPointAtLength:
var lookup = [];
var granularity = 1000;
var l = path.node().getTotalLength();
for(var i = 1; i <= granularity; i++) {
var p = path.node().getPointAtLength(l * (i/granularity))
lookup.push({
x: p.x,
y: p.y
})
}
Once I had all those points in my lookup table, I used a bisector in my translate tween:
var xBisect = d3.bisector(function(d) { return d.x; }).left;
function translateAlong(path) {
var l = path.getTotalLength();
return function (d, i, a) {
return function (t) {
var index = xBisect(lookup, l * t);
var p = lookup[index];
return "translate(" + p.x + "," + p.y + ")";
};
};
}
And it works as expected! Yahoo!
Fiddle

Simultaneous cursors

I'm trying to emulate this graph: http://www.nytimes.com/interactive/2012/11/30/us/tax-burden.html
Here is the bare-bones rendition: http://jsfiddle.net/jd5Ym/6/
I can't get the different cursors to each follow the data for their own city. I can only do one at a time. My code depends on this function:
function mousemove() {
// p is the fraction of a graph traversed. decimalize strips integers.
var p=decimilize((x0.rangeBand()-d3.mouse(this)[0]+margin.left)/(x0.rangeBand()));
var u=data[Math.round(data.length-p*data.length)];
var v=cities[1].values[Math.round(data.length-p*data.length)];
cursor.data(data).attr("transform", "translate(" + (x1(u.date)) +","+y(v.temperature)+")");
}
Where it says v=cities[1], the index decides which city's data to follow. I want it to index each city itself, but when I try using the function (d,i) {...} setup, it doesn't work out, and I tried appending the mousemovefunction within a transform attribute in the declaration of city, and that didn't work either.
I am a beginning programmer so maybe this is easy. The data structure and parsing come out of Mike Bostock's examples.
You should use selectAll('.cities').each(...) to step over all the cities and update their cursors independently.
function mousemove() {
// the proportion of the way across any given graph that the mouse is
var mouseX = d3.mouse(this)[0]
var graph_width = x0.rangeBand()
// the corresponding data
var index = Math.floor( ( mouseX / graph_width ) * data.length );
d3.selectAll('.city')
.each(function(d, i){
var u = cities[i].values[index];
d3.select(this).select('.cursor')
.attr('transform', 'translate(' + x1(u.date) + ',' + y(u.temperature) + ')')
})
}
See here for the full working example: http://jsfiddle.net/jd5Ym/9/

Categories

Resources