d3 force-directed layout expands on reheat - javascript

Consider any d3 force-directed animation, such as:
http://bl.ocks.org/mbostock/1062288
Load that graph and let the nodes settle down so that they are not moving. Hover your mouse over an orange node and let the mouse be still. Click and do nothing else. You will see all the nodes move inwards a little and then expand back to normal.
I do not like that unnecessary movement and I need to remove it.
I believe it has to do with the graph being "reheated" (see https://github.com/mbostock/d3/wiki/Force-Layout
), which sets the alpha parameter to 0.1. Specifically, I see the unnecessary movement occur immediately after any of the following.
a new node is added to the graph
a node is deleted from the graph
force.drag() is called.
What exactly causes this "unnecessary movement", and how can I disable it? (without disabling node dragging)

The behavior comes mainly from the alpha. The alpha function within d3:
force.alpha = function(x) {
if (!arguments.length) return alpha;
x = +x;
if (alpha) {
if (x > 0) alpha = x; else alpha = 0;
} else if (x > 0) {
event.start({
type: "start",
alpha: alpha = x
});
d3.timer(force.tick);
}
return force;
};
So when alpha is called, it starts the tick event if greater than 0
Within the tick event, at the very beginning, you can see the cut off that is causing the 'freeze' you mentioned:
if ((alpha *= .99) < .0005) {
event.end({
type: "end",
alpha: alpha = 0
});
return true;
}
Now, if we look at the dragmove, it calls the resume function:
function dragmove(d) {
d.px = d3.event.x, d.py = d3.event.y;
force.resume();
}
which calls the alpha function with a value of 0.1:
force.resume = function() {
return force.alpha(.1);
};
To stop the jump and also the freeze at 0.0005, I don't see myself any public function you can use.
I have an example fiddle that removed the code from the tick function that causes the freezing and also changed the resume function to start at a smaller alpha.
I believe this gives the result your looking for but of course, I wouldn't really advise customizing the library.
Maybe someone knows how to override the tick function without having to modify the source.

in
// Toggle children on click.
function click(d) {
if (!d3.event.defaultPrevented) {
if (d.children) {
d._children = d.children;
d.children = null;
} else {
d.children = d._children;
d._children = null;
}
update();
}
}
You could move the update to the children open only
// Toggle children on click.
function click(d) {
if (!d3.event.defaultPrevented) {
if (d.children) {
d._children = d.children;
d.children = null;
update();
} else {
d.children = d._children;
d._children = null;
}
}
}
so a node without children (orange) will not be updated on click.

Normally, when force.resume() is fired, the alpha value jumps from 0 to 0.1. By easing alpha up more slowly, we can cover up the "unnecessary movement".
I am still open to other ideas and solutions.
old function:
force.resume = function() {
return force.alpha(.1);
};
replacement function:
force.resume = function() {
var alpha = force.alpha()
if( alpha < 0.005 ){ alpha = 0.0055 }
else if( alpha < 0.11 ){ alpha += 0.0006 }
return force.alpha(alpha);
};

Related

How to create target lines in a rowchart in dc.js

i'm using a rowchart to show the total of sales by item of a salesman.
Already tried a composite chart unsuccessfully like many posts from the google, but none of the examples uses a rowchart.
I need to do like the image, creating the red lines to represent the sale value target for each item, but i dont know how, can you guys help me? Thanks!
Actually this is my code to plot the rowchart
spenderRowChart = dc.rowChart("#chart-row-spenders");
spenderRowChart
.width(450).height(200)
.dimension(itemDim)
.group(totalItemGroup)
.elasticX(true);
Obviously you need a source for the target data, which could be a global map, or a field in your data.
I've created an example which pulls the data from a global, but it would also take from the data if your group reduction provides a field called target.
Then, it adds a new path element to each row. Conveniently the rows are already SVG g group elements, so anything put in there will already be offset to the top left corner of the row rect.
The only coordinate we are missing is the height of the rect, which we can get by reading it from one of the existing bars:
var height = chart.select('g.row rect').attr('height');
Then we select the gs and use the general update pattern to add a path.target to each one if it doesn't have one. We'll make it red, make it visible only if we have data for that row, and start it at X 0 so that it will animate from the left like the row rects do:
var target = chart.selectAll('g.row')
.selectAll('path.target').data(function(d) { return [d]; });
target = target.enter().append('path')
.attr('class', 'target')
.attr('stroke', 'red')
.attr('visibility', function(d) {
return (d.value.target !== undefined || _targets[d.key] !== undefined) ? 'visible' : 'hidden';
})
.attr('d', function(d) {
return 'M0,0 v' + height;
}).merge(target);
The final .merge(target) merges this selection into the main selection.
Now we can now animate all target lines into position:
target.transition().duration(chart.transitionDuration())
.attr('visibility', function(d) {
return (d.value.target !== undefined || _targets[d.key] !== undefined) ? 'visible' : 'hidden';
})
.attr('d', function(d) {
return 'M' + (chart.x()(d.value.target || _targets[d.key] || 0)+0.5) + ',0 v' + height;
});
The example doesn't show it, but this will also allow the targets to move dynamically if they change or the scale changes. Likewise targets may also become visible or invisible if data is added/removed.
thank you, due the long time to have an answer i've developed a solution already, but, really thank you and its so nice beacause its pretty much the same ideia, so i think its nice to share the code here too.
The difference its in my code i use other logic to clear the strokes and use the filter value of some other chart to make it dynamic.
.renderlet(function(chart) {
dc.events.trigger(function() {
filter1 = yearRingChart.filters();
filter2 = spenderRowChart.filters();
});
})
.on('pretransition', function(chart) {
if (aux_path.length > 0){
for (i = 0; i < aux_path.length; i++){
aux_path[i].remove();
}
};
aux_data = JSON.parse(JSON.stringify(data2));
aux_data = aux_data.filter(venda => filter1.indexOf(venda.Nome) > -1);
meta_subgrupo = [];
aux_data.forEach(function(o) {
var existing = meta_subgrupo.filter(function(i) { return i.SubGrupo === o.SubGrupo })[0];
if (!existing)
meta_subgrupo.push(o);
else
existing.Meta += o.Meta;
});
if (filter1.length > 0) {
for (i = 0; (i < Object.keys(subGrupos).length); i++){
var x_vert = meta_subgrupo[i].Meta;
var extra_data = [
{x: chart.x()(x_vert), y: 0},
{x: chart.x()(x_vert), y: chart.effectiveHeight()}
];
var line = d3.line()
.x(function(d) { return d.x; })
.y(function(d) { return d.y; })
.curve(d3.curveLinear);
var chartBody = chart.select('g');
var path = chartBody.selectAll('path.extra').data([extra_data]);
path = path.enter()
.append('path')
.attr('class', 'oeExtra')
.attr('stroke', subGruposColors[i].Color)
.attr('id', 'ids')
.attr("stroke-width", 2)
.style("stroke-dasharray", ("10,3"))
.merge(path)
path.attr('d', line);
aux_path.push(path);
}
}
})
And that's how it looks

d3 scaleLinear update and zoom conflict

i have a problem combining manual update and zoom functionality in d3.
Here is a small demo code cut off out of a larger module which creates only a linear scale.
https://jsfiddle.net/superkamil/x3v2yc7j/2
class LinearScale {
constructor(element, options) {
this.element = d3.select(element);
this.options = options;
this.scale = this._createScale();
this.axis = this._createAxis();
this.linearscale = this._create();
}
update(options) {
this.options = Object.assign(this.options, options);
this.scale = this._createScale();
this.axis = this._createAxis();
this.linearscale.call(this.axis);
}
_create() {
const scale = this.element
.append('g')
.attr('class', 'linearscale')
.call(this.axis);
this.zoom = d3.zoom().on('zoom', () => this._zoomed());
this.element.append('rect')
.style('visibility', 'hidden')
.style('width', this.options.width)
.style('height', this.options.height)
.attr('pointer-events', 'all')
.call(this.zoom);
return scale;
}
_createScale() {
let range = this.options.width;
this.scale = this.scale || d3.scaleLinear();
this.scale.domain([
this.options.from,
this.options.to
]).range([0, range]);
return this.scale;
}
_createAxis() {
if (this.axis) {
this.axis.scale(this.scale);
return this.axis;
}
return d3.axisBottom(this.scale);
}
_zoomed() {
this.linearscale
.call(this.axis.scale(d3.event.transform.rescaleX(this.scale)));
let domain = this.axis.scale().domain();
this.element.dispatch('zoomed', {
detail: {
from: domain[0],
to: domain[1],
},
});
}
}
const scale = new LinearScale(document.getElementById('axis'), {
from: 0,
to: 600,
width: 600,
height: 100
});
document.getElementById('set').addEventListener('click', () => {
scale.update({
from: 0,
to: 100
});
});
Zoom x axis out to 0 - 10000 (random numbers)
Click on the "set" button
X axis sets the domain from 0 - 100
Start zooming out again
-> Expected: Zoom starts from the domain 0 - 100
-> Result: Zoom jumps back to the previous zoom level 0 - 10000
I know, that d3 is working with a scale copy and i'm updating the original scale but i don't find a way how to combine them or how to set the zoom level to the original scale.
https://github.com/d3/d3-zoom/blob/master/README.md#transform_rescaleX
Thanks!
You either recreate the zoom behaviour or just reset the current zoom transforms.
You should be able to reset each parameter( zoom and translate) or just reset them all. The following example also triggers the zoom specific events, but as you already have the domain set it should not be a visual problem.
Ex:
Add
this.element.select("rect").call(this.zoom.transform, d3.zoomIdentity);
in your update function.
More information regarding the zoom api in https://github.com/d3/d3-zoom/blob/master/README.md#zoom_transform . Also I found an example using a transition animation at https://bl.ocks.org/mbostock/db6b4335bf1662b413e7968910104f0f .
I also updated the fiddle:
update(options) {
this.options = Object.assign(this.options, options);
this.scale = this._createScale();
this.axis = this._createAxis();
this.linearscale.call(this.axis);
this.element.select("rect").call(this.zoom.transform, d3.zoomIdentity);
}
https://jsfiddle.net/x3v2yc7j/8/

How to animate drawing a sequence of line segments

I would like to draw a point and after 1 sec or so I would like to draw the next point. Is this somehow possible:
I already tried:
function simulate(i) {
setTimeout(function() { drawPoint(vis,i,i); }, 1000);
}
for (var i = 1; i <= 200; ++i)
simulate(i);
function drawPoint(vis,x,y){
var svg = vis.append("circle")
.attr("cx", function(d) {
console.log(x);
return 700/2+x;
})
.attr("cy", function(d) {
return 700/2+y;
})
.attr("r", function(d) {
console.log(x);
return 6;
});
}
Unfortunately, this is not working. It just draws the whole line immediately.
This will not work, because the for loop will immediately run to the end, the setTimeouts will be scheduled simultaneously and all the functions will fire at the same time.
Instead of that, do this:
var i = 1;
(function loop(){
if(i++ > 200) return;
setTimeout(function(){
drawPoint(vis,i,i);
loop()
}, 1000)
})();
Explanation:
This IIFE will run for the first time with i = 1. Then, the if increases i (doing i++) and checks if it is bigger than 200. If it is, the function loop returns. If it's not, a setTimeout is scheduled, which calls drawnPoint and the very function loop again.

How to bind a changing datasource to a set of svg?

I would like to visualize a circle with a random colour at a random x and y coordinate, then add an extra colourful circle at a random location every second.
I am using d3.timer to run a function that appends x and y coordinates to my dataset object, which is bound to all of the circle objects. When I print the dataset object, I can see that my function does in fact append the new x and y coordinates to my dataset object. However, the visualization does not update with the new circles. How can I add a new circle every second?
Relevant functions below:
var reshuffleData = function(){
for (var i=0; i<5; i++){
console.log('Reshuffling')
dataset.push({x: randomXPosition(), y: randomYPosition()})
}
console.log(dataset)
return true
}
d3.timer(reshuffleData, 10);
Full jsfiddle here: http://jsfiddle.net/d74Le5xk/
It wasn't not working because d3.timer is used incorrectly. As d3.timer just takes a function to paint next animation frame. We do not control when the this function will be called but it will be called most likely (1/ frames per second seconds). Where FPS may change every second.
If you want to do something periodically use setInterval also you need to redraw the circles once the dataset size is changed.
Following is the jsfiddle link for the working code.
http://jsfiddle.net/d74Le5xk/3/
Also attaching the code here for reference.
HTML
<svg class='canvas'></svg>
Javascript
(function () {
var width = 420, height = 200;
var randomXPosition = function(d){
return Math.random() * width;
}
var randomYPosition = function(d){
return Math.random() * height;
}
var dataset = [];
var circleBatchSize = 5;
var maxCircleCount = 100;
for (var i=0; i < circleBatchSize; i++){
dataset.push({x: randomXPosition(), y: randomYPosition()})
}
var testInterval = null;
var reshuffleData = function(){
for (var i=0; i<circleBatchSize; i++){
dataset.push({x: randomXPosition(), y: randomYPosition()})
//return true;
}
console.log('Reshuffled ' + dataset.length)
console.log(dataset)
if(dataset.length > maxCircleCount) {
clearInterval(testInterval);
}
}
console.log(dataset);
var colours = ['#FDBB30', '#EE3124', '#EC008C', '#F47521', '#7AC143', '#00B0DD'];
var randomColour = function() {
return colours[Math.floor(Math.random() * colours.length)];
}
//d3.timer(reshuffleData, 0, 5000);
testInterval = window.setInterval(reshuffleData, 2000);
var canvas = d3.select('.canvas')
.attr('width', width)
.attr('height', height)
.style('background-color', 'black');
var datasetOldLength = 0;
function drawCircles() {
if(datasetOldLength === dataset.length ) {
return;
}
datasetOldLength = dataset.length;
var circles = canvas.selectAll('circle')
.data(dataset)
.enter()
.append('circle')
.style('r', 20)
.style('fill', randomColour)
.style('cx', function(d) { return d.x} )
.style('cy', function(d) { return d.y} );
if(dataset.length > maxCircleCount) {
return true;
}
}
d3.timer(drawCircles, 1000);
})();
d3.timer usage explanation
# d3.timer(function[, delay[, time]])
[function] argument is called at every frame rendering by d3. It's called until it returns true.
optional [delay] in milliseconds to delay the first invocation of the [function]. Delay is taken since the [time] passed in third argument. If [time] is not passed delay starts from new Date().getTime().
optional [time] is the epoch time from when the delay is considered.
Reference https://github.com/mbostock/d3/wiki/Transitions#timers

animation glitch in sunburst

I started with the default Sunburst example and tried to add an animation on click - clicking on a wedge makes that wedge the center element. This works great except in a few cases - it looks like if the depth goes above 2 (0 based) then some wedges don't animate correctly; they seem to start the animation in the wrong place, but end up in the correct place.
Here is what I think is the relevant code, and I hope it's useful to someone else:
animate = function (d) {
var oldDs = [], topD, includeds;
if (animating)
return;
animating = true;
topD = (d == selected && d.parent) ? d.parent : d;
if (selected == topD) {
animating = false;
return;
}
selected = topD;
svg.selectAll("path").filter(function (f) {
return (!isDescendant(f, topD))
}).transition().duration(1000).style("opacity", "0").attr("pointer-events","none");
includeds = svg.datum(topD).selectAll("path").filter(function (f) {
return isDescendant(f, topD);
});
// step 1 - save the current arc settings
includeds.data().each(function (d) {
oldDs[d.ac_id] = {x: d.x, dx: d.dx, y: d.y, dy: d.dy};
});
// step 2 - recalculate arc settings and animate
includeds.data(partition.nodes) // recalculates
.transition().duration(1000).attrTween("d", function (d) {
var oldD = oldDs[d.ac_id],
interpolator = d3.interpolateObject(oldD, d);
return function (t) {
return arc(interpolator(t));
}
}).style("opacity", "1").attr("pointer-events","all");
setTimeout(function(){ animating = false; }, 1000);
};
Because my code doesn't have anything to do with the depth, I expect the issue is somewhere else, but I haven't been able to find it. The glitch seems to affect all browsers (at least Chrome, Firefox, and IE).
So, my question: what's the issue and how can I fix it?

Categories

Resources