D3: Transition between polygons - javascript

Here is a D3 project I am working on. It updates a shape of a regular polygon from n to either n-1 or n+1 angles. Pressing the buttons a,b,c,d or e will trigger this. I followed the tutorial on general update patterns and have tried to follow patterns in many other samples I have found, most notably bl.ocks.org/mbostock/3081153.
Here is the drawing function
function draw(data) {
var v = vis.selectAll("polygon")
.data(data, function(d) { return d; });
// enter selection
v.enter().append("polygon");
// update selection
v.attr("class", "update")
.transition()
.duration(750)
.attr("points",function(d) {
return d.map(function(d) { return [d.x,d.y].join(","); }).join(" ");})
.attr("stroke","black")
.attr("stroke-width",2);
// exit selection
v.exit().attr("class", "exit")
.transition()
.duration(750).remove();
};
And the updating function
function update() {
if (this.classList.contains('on')) {
d3.select(this).classed('on',false)
letters.pop(this.id);
}
else {
d3.select(this).classed('on',true)
letters.push(this.id);
}
var N = letters.length;
draw([poly(N,N,[])]);
};
My question is how and where to call an interpolation to make this go smoothly from one polygon to the next?
Thanks

Thanks Lars for looking over my question. I had figured this much but I didn't know where this would come in. I think the problem was that I was creating new polygons instead of changing the old ones (which is what happens in the updating example, old out and new in). Most people link to the California-> circle example but this one actually helped much more with less code.
By combining what I had before with this example I got this. The update function now being
function update() {
if (this.classList.contains('on')) {
d3.select(this).classed('on',false)
letters.pop(this.id);
}
else {
d3.select(this).classed('on',true)
letters.push(this.id);
}
var N = letters.length;
d3.select(".big").transition().duration(500).attr("points", mapPoints(poly(N,N,[])));};
There are obviously still many faults with this but I am happy with the animations.

Related

With D3.js is it better to re-draw or "move" objects?

I've been experimenting with animation.
It's very simple to animate an object across the canvas by clearing the entire canvas and redrawing it in a new location every frame (or "tick"):
// Inside requestAnimationFrame(...) callback
// Clear canvas
canvas.selectAll('*').remove();
// ... calculate position of x and y
// x, y = ...
// Add object in new position
canvas.append('circle')
.attr('cx', x)
.attr('cy', y)
.attr('r', 10)
.attr('fill', '#ffffff');
Is this a bad practice or am I doing it right?
For instance, if you were making a screen full of objects moving around, is it better practice to animate them by updating their attributes (e.g., x, y coordinates) in each frame?
Or, perhaps there is some other method I'm entirely unaware of, no?
Note: my animation might include 100-200 objects in view at a time.
It is better to move them, because that is the only way you can animate without errors.
In d3.js the idea is that the objects are data-bound. Clearing and redrawing the 'canvas' is not the correct approach. Firstly its not a canvas, its a web page, and any clearing and redrawing is handled by the browser itself. You job is to bind data to SVG, basically.
You need to make use of the d3 events, enter, exit, update which handles how the SVG behaves when the databound underlying data is modified and let d3 handle the animations.
the most simple example is here: https://bost.ocks.org/mike/circles/
select your elements, and store the selction in a variable
var svg= d3.select("svg");
var circles = svg.selectAll('circle');
now we need to databind something to the circle.
var databoundCircles = circles.data([12,13,14,15,66]);
This data can be anything. Usually I would expect a list of object, but these are simple numbers.
handle how things 'are made' when data appears
databoundCircles.enter().append('circle');;
handle what happens to them when data is removed
databoundCircles.exit().remove()
handle what happens when the data is updated
databoundCircles.attr('r', function(d, i) { return d * 2; })
this will change the radius when the data changes.
And recap from that tutorial:
enter - incoming elements, entering the stage.
update - persistent elements, staying on stage.
exit - outgoing elements, exiting the stage.
so in conclusion: don't do it like you are. Make sure you are using those events specifically to handle the lifecycle of elements.
PRO TIP: if you're using a list of objects make sure you bind the data by id, or some unique identifier, or the animations might behave unusually over time. Remember you are binding data to SVG you are not just wiping and redrawing a canvas!
d3.selectAll('circle').data([{id:1},{id:2}], function(d) { return d.id; });
Make note the optional second argument, that tells us how to bind the data! very important!
var svg = d3.select("svg");
//the data looks like this.
var data = [{
id: 1,
r: 3,
x: 35,
y: 30
}, {
id: 2,
r: 5,
x: 30,
y: 35
}];
//data generator makes the list above
function newList() {
//just make a simple array full of the number 1
var items = new Array(randoNum(1, 10)).fill(1)
//make the pieces of data. ID is important!
return items.map(function(val, i) {
var r = randoNum(1, 16)
return {
id: i,
r: r,
x: randoNum(1, 200) + r,
y: randoNum(1, 100) + r
}
});
}
//im just making rando numbers with this.
function randoNum(from, to) {
return Math.floor(Math.random() * (to - from) + from);
}
function update(data) {
//1. get circles (there are none in the first pass!)
var circles = svg.selectAll('circle');
//2. bind data
var databoundCircles = circles.data(data, function(d) {
return d.id;
});
//3. enter
var enter = databoundCircles.enter()
.append('circle')
.attr('r', 0)
//4. exit
databoundCircles.exit()
.transition()
.attr('r', 0)
.remove();
//5. update
//(everything after transition is tweened)
databoundCircles
.attr('fill', function(d, i){
var h = parseInt(i.toString(16));
return '#' + [h,h,h].join('');
})
.transition()
.duration(1000)
.attr('r', function(d, i) {
return d.r * 4
})
.attr('cx', function(d, i) {
return d.x * 2;
})
.attr('cy', function(d, i){
return d.y * 2
})
;
}
//first time I run, I use my example data above
update(data);
//now i update every few seconds
//watch how d3 'keeps track' of each circle
setInterval(function() {
update(newList());
}, 2000);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<svg width="500" height="300">
</svg>
Is this a bad practice or am I doing it right?
Yes, it is a bad practice. In a normal circumstance I like to call it lazy coding: clearing the SVG (or whatever) and painting the dataviz again.
But, in your case, it's even worse: you will end up writing a huge amount of code (not exactly laziness, though), ignoring d3.transition(), which can easily do what you want. And that takes us to your second question:
Or, perhaps there is some other method I'm entirely unaware of, no?
Yes, as I just said, it's called transition(): https://github.com/d3/d3-transition
Then, at the end, you said:
Note: my animation might include 100-200 objects in view at a time.
First, modern browsers can handle that very well. Second, you still have to remove and repaint manually all that elements. If you benchmark the two approaches, maybe this is even worse.
Thus, just use d3.transition().
You can change the data (or the attributes) of the elements anytime you want, and "moving" (or transitioning) them to the new value calling a transition. For instance, to move this circle around, I don't have to remove it and painting it again:
var circle = d3.select("circle")
setInterval(() => {
circle.transition()
.duration(900)
.attr("cx", Math.random() * 300)
.attr("cy", Math.random() * 150)
.ease(d3.easeElastic);
}, 1000)
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg>
<circle r="10" cx="100" cy="50" fill="teal"></circle>
</svg>

dc.js: How to change the data distribution in pie chart using different input threshold?

I would like to use the score from input field to change the data distribution in pie chart.
For example, the distribution "71% High/ 28% Low" with scoreThreshold 0.5 will be changed to "42% High/ 57% Low" with scoreThreshold 0.7.
I made this example here, but the result was not satisfactory: when typing 0.5 in the input field and clicking "check!", the pie chart distribution does not change.
This decides the distribution:
//## pie chart
var coreCount = ndx.dimension(function (d) {
var maxNumber=80;
if (typeof scoreThreshold!='number') {scoreThreshold=0.5}
//console.log(scoreThreshold)
if (d.scores >maxNumber*scoreThreshold)
{return 'High';}
else {return 'Low';}
});
I would like to renew the coreCount function to reassign the distribution using input score threshold. But it does not work:
$('#scoreThresholdBt').click(function () {
scoreThreshold=document.getElementById('scoreThreshold').value
scoreThreshold=parseFloat(scoreThreshold);
console.log(scoreThreshold,typeof scoreThreshold);
function redo_coreCount () {
var coreCount = ndx.dimension(function (d) {
var maxNumber=80;
console.log(scoreThreshold);
if (d.count >maxNumber*scoreThreshold)
{return 'High';}
else {return 'Low';}
});
}; redo_coreCount();
coreCount.group();/**/
dc.renderAll();
dc.redrawAll();
});
How can I realize this function?
I really appreciate any help.
You are essentially putting the new dimension in a temporary variable which gets thrown away immediately. coreCount in the click function is unrelated to the variable with the same name at the global level.
Likewise, coreCount.group() is not an action; it's constructing a group object which will also get lost if you don't use it.
You need to assign the new dimension and group to your chart, since it doesn't track the value of the global or the local coreCount.
So let's change the function to return a new dimension based on reading the score threshold from the input:
function coreCount_from_threshold() {
var scoreThreshold=document.getElementById('scoreThreshold').value;
scoreThreshold=parseFloat(scoreThreshold);
console.log(scoreThreshold,typeof scoreThreshold);
if (isNaN(scoreThreshold)) {scoreThreshold=0.5}
return ndx.dimension(function (d) {
var maxNumber=80;
if (d.scores >maxNumber*scoreThreshold)
{return 'High';}
else {return 'Low';}
});
}
Note that we need to use isNaN here, because typeof NaN === 'number'
We can use this both at initialization time:
var coreCount = coreCount_from_threshold();
and on click:
$('#scoreThresholdBt').click(function () {
coreCount.dispose();
coreCount = coreCount_from_threshold();
coreCountGroup = coreCount.group();
coreYesNoPieChart
.dimension(coreCount)
.group(coreCountGroup);
dc.redrawAll();
});
Note that we are assigning to the same global variables coreCount and coreCountGroup (because we don't use var here). We first dispose the old dimension, because otherwise it would continue to filter and take up resources. Then we assign the new dimension and group to the chart (because otherwise it won't know about them).
We only need to do a redraw (not a render) here, because dc.js charts can just update when they get new data.
Here is a working fork of your fiddle.

D3.js method chaining command dosn't work when I split it, it works

I am new in D3.js,
when i use this code it doesn't work,(it is part of the redraw, when running for first time it works good when calling redraw again it works unexpextedly)
var rows=tbody.selectAll('tr').data(roster);
rows.enter().append('tr');
rows.exit().remove();
rows.selectAll('td').data(function(row) { return columns.map(function(col) {
return row[col];
});}).enter().append('td').text(function(d) {return d;} );
when I break the chain down into smaller it works.
var rows=tbody.selectAll('tr').data(roster);
rows.enter().append('tr');
rows.exit().remove();
var cells = rows.selectAll("td")
.data(function(row) { return columns.map(function(col) {
return row[col];
});});
cells.enter().append("td");
cells.text(function(d) { return d; });
any reason or any rule govern this.
In the first case you are only updating the text on the new cells, not the old ones. When you chain .enter() like that, all of the following methods chained apply to the object returned by .enter() and that is the enter selection : added cells in other words.
Read this

Dynamic returned values in append function

I'm using d3 library and I want to create different elements by checking some values. If I do:
elements.append("rect").attr(...);
What happens if I want to create different elements? I tried:
elements.append(function (d) {
if (d.foo) {
return "rect";
}
return "circle";
});
This seems not to work.
What's the alternative to this?
As you've already pointed out, .append() accepts a function that returns the DOM element to append. This looks as follows.
var data = ["circle", "rect"];
d3.select("svg").selectAll(".shape").data(data)
.enter()
.append(function(d) {
return document.createElementNS(d3.ns.prefix.svg, d);
});
Of course you also need to set all the attributes correctly, which is the crux of the method -- in principle you don't need lots of if/switch statements to handle each element type, but in practice you do because the attributes you need to set differ (unless you want to set everything for everything).
Complete demo here.
Even append function accepts a function, I'm not sure how that should work (maybe someone brings the light here). However the following solution is human readable and easy to implement:
elements.each(function (d) {
var el = d3.select(this);
var type = "circle";
if (el.foo) {
type = "rect";
}
var aType = el.append(type);
if (type === "rect") {
aType.attr("rx", 5)
.attr("ry", 5)
.attr("height", ...)
.attr("width", ...);
} else if (type === "circle") {
aType.attr("r", ...);
}
});

D3.js - Retrieving DOM subset given data subset

I'm using d3.js to create a large number of svg:ellipse elements (~5000). After the initial rendering some of the data items can be updated via the backend (I'll know which ones) and I want to change the color of those ellipses (for example).
Is there a fast way to recover the DOM element or elements associated with a data item or items? Other than the obvious technique if recomputing a join over the full set of DOM elements with the subset of data?
var myData = [{ id: 'item1'}, { id: 'item2' }, ... { id: 'item5000' }];
var create = d3.selectAll('ellipse).data(myData, function(d) { return d.id; });
create.enter().append('ellipse').each(function(d) {
// initialize ellipse
});
// later on
// this works, but it seems like it would have to iterate over all 5000 elements
var subset = myData.slice(1200, 1210); // just an example
var updateElements = d3.selectAll('ellipse').data(subset, function(d) { return d.id; });
updateElements.each(function(d) {
// this was O(5000) to do the join, I _think_
// change color or otherwise update
});
I'm rendering updates multiple times per second (as fast as possible, really) and it seems like O(5000) to update a handful of elements is a lot.
I was thinking of something like this:
create.enter().append('ellipse').each(function(d) {
d.__dom = this;
// continue with initialization
});
// later on
// pull the dom nodes back out
var subset = myData.slice(1200, 1210).map(function(d) { return d.__dom; });
d3.selectAll(subset).each(function(d) {
// now it should be O(subset.length)
});
This works. But it seems like this would be a common pattern, so I'm wondering if there is a standard way to solve this problem? I actually want to use my data in multiple renderings, so I would need to be more clever so they don't trip over each other.
Basically, I know that d3 provides a map from DOM -> data via domElement.__data__. Is there a fast and easy way to compute the reverse map, other than caching the values myself manually?
I need to get from data -> DOM.
As long as you keep the d3 selection reference alive (create in your example), D3 is using a map to map the data keys to DOM nodes in the update so it's actually O(log n).
We can do some testing with the D3 update /data operator method vs a loop method over the subset:
var d3UpdateMethod = function() {
svg.selectAll("ellipse").data(subset, keyFunc)
.attr("style", "fill:green");
}
var loopMethod = function() {
for (var i=0; i < subset.length; i++) {
svg.selectAll(".di" + i)
.attr("style", "fill:green");
}
}
var timedTest = function(f) {
var sumTime=0;
for (var i=0; i < 10; i++) {
var startTime = Date.now();
f();
sumTime += (Date.now() - startTime);
}
return sumTime / 10;
};
var nextY = 100;
var log = function(text) {
svg.append("text")
.attr("x", width/2)
.attr("y", nextY+=100)
.attr("text-anchor", "middle")
.attr("style", "fill:red")
.text(text);
};
log("d3UpdateMethod time:" + timedTest(d3UpdateMethod));
log("loopMethod time:" + timedTest(loopMethod));
I also created a fiddle to demonstrate what I understand you're trying to do here.
Another method to make it easy to track the nodes that are in your subset is by adding a CSS class to the subset. For example:
var ellipse = svg.selectAll("ellipse").data(data, keyFunc).enter()
.append("ellipse")
.attr("class", function (d) {
var cl = "di" + d.i;
if (d.i % 10 == 0)
cl+= " subset"; //<< add css class for those nodes to be updated later
return cl;
})
...
Note how the "subset" class would be added only to those nodes that you know are in your subset to be updated later. You can then select them later for an update with the following:
svg.selectAll("ellipse.subset").attr("style", "fill:yellow");
I updated the fiddle to include this test too and it's nearly as fast as the directMethod.

Categories

Resources