D3: move circle between two different g elements in force layout - javascript

I have two g elements each containing circles. Circles are organized using force.layout. The g elements are transitioning.
You can see here: demo. Reduced code:
var dots = svg.selectAll(".dots")
.data(data_groups)
.enter().append("g")
.attr("class", "dots")
.attr("id", function (d) {
return d.name;
})
...
.each(addCircles);
dots.transition()
.duration(30000)
.ease("linear")
.attr("transform", function (d, i) {
return "translate(" + (150 + i * 100) + ", " + 450 + ")";
});
function addCircles(d) {
d3.select(this).selectAll('circle')
.data(data_circles.filter(function (D) {
return D.name == d.name
}))
.enter()
.append('circle')
.attr("class", "dot")
.attr("id", function (d) {
return d.id;
})
...
.call(forcing);
}
function forcing(E) {
function move_towards(alpha) {
...
}
var force = d3.layout.force()
.nodes(E.data())
.gravity(-0.01)
.charge(-1.9)
.friction(0.9)
.on("tick", function (e) {
...
});
force.start();
}
I need to move circle (for example id=1) from the first g element to the second one using transition.
Any suggestions are appreciated.

It can be done.
What I did was:
1) Use jquery to append the point to the target group
2) Use a transformation (no transition) to move the point back to its original location
3) Transition the point to its new location
The jQuery was used for the appendTo method. It can be removed and replaced with some pure Javascript stuff, but it's quite convenient.
I've got a partially working fiddle here. The green points work right, but something is going wrong with the blue ones. Not sure why.

In my view, transitions work on a single element. If an element changes its position in the DOM tree, from below one g to another, I can't think of a way to make that as one smooth transition because it's basically a binary split: Now there's an element under one g, now it's gone but there's another one somewhere else.
What I'd do in order to achieve what I think you want to do: Group everything under the same ´g´, assign color and translation individually, then change color and translation for that single element you want to change.
But don't take that as a reliable statement that you can't do it the way you originally wanted.

Related

How to crossfilter histogram and scatterplot matrix in d3 v4?

I am using this kind of scatterplot matrix and a histogram as two views, in d3. Both of them get the data from the same csv file. This is how the histogram looks like (x axis):
To brush the histogram I use the code below, which is similar to this snippet:
svg.append("g")
.attr("class", "brush")
.call(d3.brushX()
.on("end", brushed));
function brushed() {
if (!d3.event.sourceEvent) return;
if (!d3.event.selection) return;
var d0 = d3.event.selection.map(x.invert),
d1 = [Math.floor(d0[0]*10)/10, Math.ceil(d0[1]*10)/10];
if (d1[0] >= d1[1]) {
d1[0] = Math.floor(d0[0]);
d1[1] = d1[0]+0.1;
}
d3.select(this).transition().call(d3.event.target.move, d1.map(x));
}
How can I link the two views, so that when I brush the histogram, the scatterplot matrix will show the brushed points as colored in red, and the other points as, lets say, grey?
This can get you started:
3 html files:
2 for the visuals (histogram.html and scatter.html)
1 to hold them in iframes (both.html):
Dependency:
jQuery (add to all 3 files)
Create table with 2 cells in both.html:
Add iframes to each cell:
<iframe id='histo_frame' width='100%' height='600px' src='histo.html'></iframe>
<iframe id='scatter_frame' width='100%' height='600px' src='scatter.html'></iframe>
I am using this histogram, and this scatterplot.
Add the linky_dink function to call the function inside your scatter.html (see below...):
function linky_dink(linked_data) {
document.getElementById('scatter_frame').contentWindow.color_by_value(linked_data);
}
In your scatter.html change your cell.selectAll function to this:
cell.selectAll("circle")
.data(data)
.enter().append("circle")
.attr("cx", function(d) { return x(d[p.x]); })
.attr("cy", function(d) { return y(d[p.y]); })
.attr("r", 4)
.attr('data-x', function(d) { return d.frequency }) // get x value being plotted
.attr('data-y', function(d) { return d.year }) // get y value being plotted
.attr("class", "all_circles") // add custom class
.style("fill", function(d) { return color(d.species); });
}
Note the added lines in bold:
Now our histogram circle elements retain the x and y values, along with a custom class we can use for targeting.
Create a color_by_value function:
function color_by_value(passed_value) {
$('.all_circles').each(function(d, val) {
if(Number($(this).attr('data-x')) == passed_value) {
$(this).css({ fill: "#ff0000" })
}
});
}
We know from above this function will be called from the linky_dink function of the parent html file. If the passed value matches that of the circle it will be recolored to #ff0000.
Finally, look for the brushend() function inside your histogram.html file. Find where it says: d3.selectAll("rect.bar").style("opacity", function(d, i) { .... and change to:
d3.selectAll("rect.bar").style("opacity", function(d, i) {
if(d.x >= localBrushYearStart && d.x <= localBrushYearEnd || brush.empty()) {
parent.linky_dink(d.y)
return(1)
} else {
return(.4)
}
});
Now, in addition to controlling the rect opacity on brushing, we are also calling our linky_dink function in our both.html file, thus passing any brushed histogram value onto the scatterplot matrix for recoloring.
Result:
Not the greatest solution for obvious reasons. It only recolors the scatterplot when the brushing ends. It targets circles by sweeping over all classes which is horribly inefficient. The colored circles are not uncolored when the brushing leaves those values since this overwhelms the linky_dink function. And I imagine you'd rather not use iframes, let alone 3 independent files. Finally, jQuery isn't really needed as D3 provides the needed functionality. But there was also no posted solution, so perhaps this will help you or someone else come up with a better answer.

d3.js (d.geometry.coordinates) issue when placing labels

I'm trying to place labels using the larskotthoff block page http://bl.ocks.org/larskotthoff/11406992
The issue I'm having is where the labels translate is worked out on the line.
.attr("transform", function (d) {
return "translate(" + projection(d.geometry.coordinates[0][0][0]) + ")";
})
So in his guide he is not doing d.geometry.coordinates[0][0][0] but just d.geometry.coordinates. This works for him because he is not working out the labels for multi-polygons because when I tried without the [0][0][0]
So my labels are not where they should be as you can see in the following image.
Although I've managed to get the labels drawing the structure of my feature (in this case d) is different and not a single array of coordinates. It is a 3 dimension array. So for this reason the placement (translate) is happening based on the first element [0][0][0] of each dimension. How do I overcome this? I want it to work out its placement from the whole set of polygons. Does d3 make something available for this?
UPDATE: Solution (thanks to Lars)
.data(json.features)
.enter().append("text")
.attr("class", "place-label")
.attr("transform", function (d) {
console.log("test");
return "translate(" + path.centroid(d) + ")";
})
.attr("x", function (d) {
return path.centroid(d)[0] > -1 ? 6 : -6;
})
.attr("dy", ".35em")
.style("text-anchor", function (d) {
return path.centroid(d)[0] > -1 ? "start" : "end";
})
As Lars Kotthoff confirms in comment. To get the centre location from a node/feature you can use path.centroid as described in the d3 documentation below.
Path.centroid

Filtering Data with on click function

I have two array objects that hold my d3.svg.symbol types which are circles, squares & triangles. Array #1 has multiple symbols which I plot across the canvas, whereas array #2 only holds three symbols aligned together.
My goal is to be able to click on array #2 to filter out all of the array #1 symbols that i dont want to see. e.g. Clicking a circle in array #2 would only mean circles are shown in array #1.
var array1 = svg.selectAll(a.array1)
.data(json).enter().append("a")
array1.transition().duration(1000)
.attr("transform", function(d,i) {return "translate("+d.x+","+d.y+")" ;})
array1.append('path')
.attr("d", d3.svg.symbol().type(function(d) {return shape [d.Country];}).size(120))
var array2 = svg.selectAll(g.array2)
.data(filt)
.enter().append("g")
.attr("transform", function(d,i) {return "translate("+d.x+","+d.y+")" ;})
array2.append("path")
.attr("d", d3.svg.symbol().type(function(d){return d.shape;}).size(200))
.attr("transform", "translate(-10, -5)")
So my query is how do I specify the click onto array#2 specific types as I have three. Therefore, I would like all to be clickable, but have a different outcome.
So far I have tried this just to try & select specific shapes in array#2
array2.on("click", function(){ alert('success') })
which just alerts when I click any of them, however when this is applied:
array2.on("click", function(){ if (d3.svg.symbol().type('circle') === true) { return alert('success') ;}; })
When I click the circle of array2 it doesnt alert at all.
It would be great if I could get some help - thanks. http://jsfiddle.net/Zc4z9/16/
The event listener gets the current datum and index as arguments, see the documentation. You can also access the DOM element through this. You could use this like follows.
.on("click", function(d) {
if(d.shape == "circle") { alert("success"); }
});

d3.js dynamic data reload possibly related to exit().remove()

I have implemented this d3 visualization http://bl.ocks.org/4745936 , to be loaded with dynamic data instead of a .tsv
in my case, once my server passes new information to the selector, a second chart gets rendered under the first one, instead of modifying the contents of the existing graph.
I believe it has to do with this append method.
var svg = d3.select(selector).append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
so I tried adding other exit().remove() methods to legend and cities variables right after they append('g'); but my javascript console says the exit() method does not exist at that point.
I feel I have the completely wrong approach, how do I update an existing graph like this? Having a second and third graph get generated alongside the previous ones is not the outcome I wanted at all
You're right the append method is adding a new svg element every time. To prevent the duplicate charts you need to check if the svg element exists already. So try something like this at the begining:
var svg = d3.select("#mycontainer > svg")
if (svg.empty())
svg = d3.select(selector).append("svg");
...
As stated in the exit() docs, This method is only defined on a selection returned by the data operator. So make sure that you're calling exit on a selection returned from .data(..).
scott's answer is one way of ensuring that the initialization happens only once.
However, I prefer a more d3-ic way of handling this:
var svg = d3.select(selector)
.selectAll('svg')
.data( [ dataFromTSV ] ); // 1 element array -> 1 svg element
// This will be empty if the `svg` element already exists.
var gEnter = svg.enter()
.append('svg')
.append('g');
gEnter.append( ... ); // Other elements to be appended only once like axis
svg.attr('width', ...)
.attr('height', ...);
// Finally, working with the elements which are surely in the DOM.
var g = svg.select("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
g.selectAll(...).attr(...);
This approach is exemplified in the reusable charts example's source code.
I prefer this approach because it keeps the code very declarative and true to the visualisation by hiding away the logic of initialisation and updates.
I would modify the original example: http://jsfiddle.net/8Axn7/5/ to http://jsfiddle.net/3Ztt8/
Both the legend and the graph are defined from svgElem with one single element of data:
var svgElem = d3.select("#multiLinegraph").selectAll('svg')
.data([cities]);
// ...
var svg = svgElem.select('g');
// ...
var city = svg.selectAll(".city")
.data(
function (d) { return d; },
function (d) { return d.name; } // Object consistency
);
// ...
var legend = svg.selectAll('g.legend')
.data(
function(d) { return d; },
function (d) { return d.name; } // Object consistency
);
Also, the static properties are set only once when the element is entered (or exited), while the update properties are set (transitioned) with each update:
gEnter.append("g")
.attr("class", "y multiLineaxis")
.append('text')
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("Requests (#)");
svg.select('g.y.multiLineaxis').transition().call(yAxis);
The code, in my opinion, follows the cycle of enter-update-exit cleanly.
I was able to solve this problem with some jQuery and CSS voodoo
basically since my d3 graph adds an svg element to an existing selector (a div in my case), I was able to check for the name of this dynamically
var svgtest = d3.select(selector+" > svg"); getting the svg subchild element of that div. then I could use jquery to remove that element from the dom completely, and then let d3 continue running and append svg's all it wants!
var svgtest = d3.select(selector+" > svg");
if(!svgtest.empty())
{
$(selector+" > svg").remove();
}
First of all you should remove old svg, after then you can add updated charts.
For that you should add only one line before you append svg.
And its working.
var flag=d3.select("selector svg").remove();
//----your old code would be start here-------
var svg = d3.select(selector).append("svg")

D3js sorting issue on transition applied

I have a dataset already binded to svg:g via a d.id
var categorized = g1.selectAll("g.node")
.data(dataset, function(d){return d.id})
.classed('filtered', false);
categorized.enter()
.append("g")
.attr("class", "node")
...
I use a function to order it from a data value like this:
var sorted = dataset
.filter(function(d) { return d.notation[3].value >=50 } )
.sort(function(a, b) { return d3.descending(a.notation[3].value,
b.notation[3].value) });
It returns the correct order when I console.log it
var filtered = g1.selectAll("g.node")
.data(sorted, function(d) {return d.id})
.classed('filtered', true);
Still in the right order if I console.log it,
but if I apply a delay it reverses the result order
scored.transition()
.delay(500).duration(1000)
.attr("id", function(d) {
console.log(d.id);
});
but keeps it well sorted if I remove the delay.
My question : am I doing something in a bad way?
I think you're observing that d3.js generally uses the "optimized" for loop that iterates in reverse (see Are loops really faster in reverse? among other references).
Would it work to simply reverse your selection? I'm not sure what you're transitioning such that you need the tween steps to be applied in a certain order.

Categories