d3.js Strange dragging behaviour when using dynamic scaling - javascript

I am trying to build a d3 graph (currently only unconnected nodes) to handle relatively small node values. For this reason I am using a linear scale that reacts dynamically on the input values and scales them accordingly:
var xScale = d3.scale.linear().range([0, width]);
var yScale = d3.scale.linear().range([height, 0]);
[...]
xScale.domain(d3.extent(layout, function (d){ return d.x; }));
yScale.domain(d3.extent(layout, function (d){ return d.y; }));
In the current version (you can find it here: https://jsfiddle.net/mn1wmbe3/6/) the dragging and value changing of single nodes works perfectly fine. This is the code I am using to move a node:
node.filter(function(d) { return d.selected; })
.each(function(d) {
d.x = xScale.invert(d3.event.x);
d.y = yScale.invert(d3.event.y);
})
.attr("cx", xScale(d.x))
.attr("cy", yScale(d.y));
As seen in other examples, to be able to move several nodes simultaneously I would need to add the relative coordinates of the drag movement (d3.event.dx) to each selected node value. Like this:
node.filter(function(d) { return d.selected; })
.each(function(d) {
d.x += xScale.invert(d3.event.dx);
d.y += yScale.invert(d3.event.dy);
})
.attr("cx", xScale(d.x))
.attr("cy", yScale(d.y));
However, this behaves not as expected and moves the nodes way to far. You can see it in action here.
Do you have any suggestions why that is? Is there a possibility to work around that and move several nodes in a different way?

You should only need a few minor changes to make things work properly. First, change your drag event from:
d.x += xScale.invert(d3.event.dx);
d.y += yScale.invert(d3.event.dy);
To:
d.x = xScale.invert(d3.event.x);
d.y = yScale.invert(d3.event.y);
And things will no longer jump around like before. However, if you increase the size of the radius you will notice that when you first start dragging, the object will jump to center around the mouse position. This is because you now need to specify an origin function. Change this code:
d3.behavior.drag()
.on("drag", function(d) {
To this:
.call(d3.behavior.drag()
.origin(function(d){return {x:xScale(d.x), y:yScale(d.y)};})
.on("drag", function(d) {
With this, when you drag something it will remember the offset. And now things will behave nicely.
Now to move many nodes relatively you can write your own dx,dy function. Simply log the coordinates of the object on dragstart and then calculate dx,dy on drag, and add these values to the other nodes to be moved.

I figured it out. Basically, the error was using dynamic scaling:
xScale.domain(d3.extent(layout, function (d){ return d.x; }));
yScale.domain(d3.extent(layout, function (d){ return d.y; }));
Because of my test data, this lead to the domain beeing:
xScale.domain(-2, 1);
yScale.domain(-1, 0);
Which behaves perfectly fine, if you moove your nodes this way:
d.x = xScale.invert(d3.event.x);
d.y = yScale.invert(d3.event.y);
However, when trying to move nodes by adding the delta distances (d3.event.dx) to them, the invertion of the scale lead to unexpected results. The reason for this is the fact that xScale.invert(1) returned -1.994, which when added to the original value, gave unexpected results.
Solution
The workaround I used calculates the distance between the min and max value of the data and sets the domain from zero to this value:
extentX = Math.abs(d3.max(graph.layout, function(d) { return d.x;}) - d3.min(graph.layout, function(d) { return d.x;}));
extentY = Math.abs(d3.max(graph.layout, function(d) { return d.y;}) - d3.min(graph.layout, function(d) { return d.y;}));
xScale.domain([0, extentX]);
yScale.domain([0, extentY]);

Related

Getting D3 Force to work in Update pattern

I'm hoping someone can spot where I'm going wrong here as I've looked at it for over 24 hours and can't see the issue.
I have a fairly complex dataviz working nicely in D3 but the final step is to 'adjust' any overlapping points - primarily so that their individual tooltips are accessible (on hover) (e.g. I don't want to consider alternatives like 'growing' the points to show their combined status).
So, imagine a pinboard where every pin is a point with hit_x and hit_y coordinates. Everything is working perfectly (filtering, updating, etc) in what I understand to be a fairly standard D3 'update' pattern. Sometimes two pins might have the same coordinates.
I thought I'd use D3 forces (for the first time) to recognise the 'colliding' pins and then adjust their positions accordingly. However, whilst I can get a simple version working on Blockbuilder. I can't get the same thing working when applied to my dataviz, even when I simplify it considerably.
I think perhaps I don't 100% understand the simulation process when using from an update pattern. My (simplified) code is pasted below, and here's in effect what I think it should do:
Appropriately loaded/formatted data is passed to update()
Prepare points ready to attach svg objects (same as 'standard' update pattern).
Prepare a simulation (and initially run it via ticked()).
Visualise the data.
Rerun the simulation so that the collisions are detected...
...during which, ticked() should notice the collisions and adjust the points by adjusting d.x and d.y accordingly until there are no overlaps.
I'm sure there's something obvious I'm missing - possibly related to whether I should pass the 'points' to the simulation or the original data. If anyone can spot it then I'd be very grateful. ¯\(ツ)/¯
function ticked() {
console.log("Ticking...");
points
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
});
}
// Update visualisation
function update(data) {
// Animation transitions
var t1 = d3.transition().duration(3000);
// Add svg group for handling/styling points later
graph.select("g.points").remove();
var pointsG = graph.append("g")
.attr('class', 'points');
// Data: Points
// Join new data with old elements
points = pointsG.selectAll("circle.point")
.data(data, function(d) {
return d;
});
// Forces
collisionSimulation = d3.forceSimulation(points)
.force('charge', d3.forceManyBody().strength(10))
.force('x', d3.forceX(function(d) {
return xScale(d.point.hit_x);
}).strength(0.5))
.force('y', d3.forceY(function(d) {
return yScale(d.point.hit_y);
}).strength(0.5))
.alphaTarget(1)
.on('tick', ticked);
console.log(collisionSimulation);
// Remove old elements not present in new data
points.exit()
.transition(t1)
.attr('class', 'exit')
.remove();
// Append new elements
points.enter()
.append('circle')
.attr('class', 'point')
.attr('cx', function(d) {
return xScale(d.point.hit_x);
})
.attr('cy', function(d) {
return yScale(d.point.hit_y);
})
.attr('r', 5)
.merge(points);
collisionSimulation.nodes(points)
.force("collide", d3.forceCollide().strength(0.5).radius(function() {
return 5;
}));
collisionSimulation.alpha(0.5).restart();
}
OK, now resolved - though I would appreciate further input on the ticked() implementation.
I imagine someone will go through the same process as me at some point, so following is what I did to get things working. The crux of it is that I had been getting confused joining the forces with the visual objects. By passing the same original data to both, the forces were then able to act on the visual objects, rather than both existing as one.
I re-read the core information about data joins. I've read this a number of times over the past few months working with D3 but for some reason, reading it again made my forces / update pattern finally make (un)sense in my head.
Similarly, despite having used a few excellent Blocks examples for reference, I came across this Medium article which made things click for me. It doesn't mean the Blocks examples aren't great - just that for some reason this article helped me.
Thereafter, I updated my code and was able to get things working. I feel like I'm still missing a little bit of 'data join' magic in my ticked() function though as it seems odd to need to 'search' for the relevant items to act on rather than use a previously built reference. I'm sure I can optimise that, but if anyone else can input then great.
Hope that helps someone else out.
function ticked() {
console.log("Ticking...");
d3.select('g.points').selectAll('circle.point')
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
}
// Update visualisation
function update(data) {
// Animation transitions
var t1 = d3.transition().duration(3000);
// Add svg groups for organising data elements
graph.select("g.points").remove();
var pointsG = graph.append("g")
.attr('class', 'points');
// Points
// Join new data with old elements
points = pointsG.selectAll("circle.point")
.data(data, function(d){ return d; });
// Remove old elements not present in new data
points.exit()
.transition(t1)
.attr('class', 'exit')
.remove();
// Append new elements
points.enter()
.append('circle')
.attr('class', 'point')
.attr('cx', function(d){ return xScale(d.point.hit_x); })
.attr('cy', function(d){ return yScale(d.point.hit_y); })
.attr('r', 5)
.merge(points);
// Forces
collisionSimulation = d3.forceSimulation()
.nodes(data)
.force('charge', d3.forceManyBody().strength(2))
.force('x', d3.forceX(function(d) { return xScale(d.point.hit_x); }).strength(0.5))
.force('y', d3.forceY(function(d) { return yScale(d.point.hit_y); }).strength(0.5))
.force("collide", d3.forceCollide().strength(0.5).radius(function() { return 5; }))
.on('tick', ticked);
collisionSimulation.alpha(0.5).restart();
}

d3.js: Unusual Error

I have this d3.js project donut chart. For some reason, I am not able to access the data with in the onmousemove. The i value become zero is all the functions I pass within that event. I want to access the data of the particular slice where the mouse has moved.
How do I resolve this? Someone pls hlp!
Here is my code so far:
piesvg.selectAll("path")
.data(pie(data))
.enter().append("g")
.attr('class', 'slice')
var slice = d3.selectAll('g.slice')
.append('path')
.each(function(d) {
d.outerRadius = outerRadius - 20;
})
.attr("d", arc)
.attr('fill', function(d, i) {
return colorspie(i)
})
.on("mouseover", arcTween(outerRadius, 0))
.on("mouseout", arcTween(outerRadius - 20, 150))
.on("mousemove", function(data){
piesvg.select(".text-tooltip")
.attr("fill", function(d,i){return colorspie(i)})
.text(function(d, i){return d[i].domain + ":" + parseInt(d[i].value * 20)}); //Considers i as 0, so no matter whichever slice the mouse is on, the data of only first one is shown
});
Here is the full code:
https://jsfiddle.net/QuikProBro/xveyLfyd/1/
I dont know how to add external files in js fiddle so it doesn't work....
Here is the .tsv that is missing:
value domain
1.3038675 Cloud
2.2541437 Networking
0.15469614 Security
0.8287293 Storage
0.7292818 Analytics
0.61878455 Intelligence
1.7016574 Infra
0.4088398 Platform
Your piesvg.select is bound to be zero-indexed for i and in all probability undefined for d as it takes those values from a single tooltip element, not the slices. Hard to be 100% sure from the snippet, but I suspect you're wanting to access and use the 'data' and 'i' from the original selectAll on the slices.
.on("mousemove", function(d, i){
piesvg.select(".text-tooltip")
.attr("fill", colorspie(i))
.text(d.data.domain + ":" + parseInt(d.data.value * 20));
});
Edited as pie slices store original data in d.data property ^^^

D3 bar chart by date with context brushing

I have a D3 bar chart with date/time on the X axis. This is working well now, but I'd like to add a smaller brushable chart below it and am having trouble due to some of the date manipulations I had to do to make the bars center over the X axis tick lines - see this post for details on that.
The brush does not seem to be calculating the X axis date range correctly.
Here is the x domain definition and brush function. JSFiddle for the full code.
main_x.domain([
d3.min(data.result, function(d) { return d.date.setDate(d.date.getDate() - 1); }),
d3.max(data.result, function(d) { return d.date.setDate(d.date.getDate() + 2); })
]);
function brushed() {
// I thought that re-defining the x domain without the date manipulations might work, but I
// was getting some odd results
//main_x.domain(d3.extent(data.result, function(d) { return d.date; }));
main_x.domain(brush.empty() ? main_x.domain() : brush.extent());
console.log(brush.extent());
bar.selectAll("rect")
.attr("width", function(d) { return main_width/len; })
.attr("x", function(d) { return main_x(d.date) - (main_width/len)/2; });
main.select(".x.axis").call(main_xAxis);
}
The problem is that you're using the same scale for the focus and the context chart. As soon as you change that, the range of the selection changes (same size, but different underlying scale). To fix, use two different scales.
I've done this here, introducing mini_x as the x scale for the context chart.

Conditionally build group in d3

I am trying to build a graph using the force layout in D3. I would like to build different looking nodes depending on the data. Currently all nodes have a category and a name. So I draw an svg:g consisting of two rect and two text elements.
My code currently looks something like this:
// nodes are in the form: { group: 'string', name: 'string2' }
this.node = this.node.data(this.node, function(d) { return d.id; });
var g = this.node.enter().
append('svg:g').
attr('transform', function(d) { return 'translate('+ d.x +','+ d.y +')'; });
g.append('svg:rect').attr('h', 20).attr('w', 100);
g.append('svg:rect').attr('y', 20).attr('h', 20).attr('w', 100);
g.append('svg:text').text(function(d) { d.group; });
g.append('svg:text').attr('y', 20).text(function(d) { d.name; });
If the node doesn't have a name, however, I'd like to supress the creation of the second rect and text. Logically, if it wasn't for the implicit iterator in d3 I'd be doing something like:
var g = this.node.enter().
append('svg:g').
attr('transform', function(d) { return 'translate('+ d.x +','+ d.y +')'; });
g.append('svg:rect').attr('h', 20).attr('w', 100);
g.append('svg:text').text(function(d) { d.group; });
// unfortunately 'd' isn't defined out here.
// EDIT: based on comment from the answer below; the conditional should
// be for the text and the related rectangle.
if(d.name) {
g.append('svg:rect').attr('y', 20).attr('h', 20).attr('w', 100);
g.append('svg:text').attr('y', 20).text(function(d) { d.name; });
}
You could use an each call on your g selection to decide whether or not to add the label.
g.each(function(d) {
if (d.name){
var thisGroup = d3.select(this);
thisGroup.append("text")
.text(d.group);
thisGroup.append("text")
.attr("y", 20)
.text(d.name);
});
However, be aware that this structure could get confusing if you're going to be updating the data.
If you want to be able to update neatly, I would recommend doing a nested selection:
var labels = g.selectAll("text")
.data(function(d){ d.name? [d.group, d.name]:[]; });
labels.enter().append("text");
labels.exit().remove();
labels.text(function(d){return d;})
.attr("y", function(d,i){return i*20;});
The data-join function tests the parent's data object, and based on it either passes an array containing the two values you want to use for the label text, or an empty array. If it passes the empty array, then no labels are created; otherwise, each label has it's text set by the value in the array and it's vertical position set by the index.

Bar chart with d3.js and an associative array

I give up, I can't figure it out.
I was trying to create a bar chart with 3d.js but I can't get it working. Probably I don't understand it enough to deal with my complicate associative array.
My array has the following structure:
{"January"=>{"fail"=>13, "success"=>6},
"February"=>{"success"=>10, "fail"=>4},
"March"=>{"success"=>9, "fail"=>13},
"April"=>{"success"=>16, "fail"=>5},
"May"=>{"fail"=>52, "success"=>23},
"June"=>{"fail"=>7, "success"=>2},
"July"=>{},
"August"=>{"fail"=>6, "success"=>3},
"September"=>{"success"=>54, "fail"=>59},
"October"=>{"success"=>48, "fail"=>78},
"November"=>{"fail"=>4, "success"=>6},
"December"=>{"fail"=>1, "success"=>0}}`
I got the displaying of the axis working:
The code looks really ugly because I converted the names to a "normal" array:
monthsNames = new Array();
i = 0;
for (key in data) {
monthsNames[i] = key;
i++;
}
x.domain(monthsNames);
y.domain([0, 100]);
But I can't figure it out how to deal with the data.
I tried things like, svg.selectAll(".bar").data(d3.entries(data))
What is a good beginning I guess but I can't get the connection to the axis working.
What I want to create is a bar-chart that has the months as x-axis and every month has two bars (respectively one bar with two colours) - one for success and one for fail.
Can anybody please help me how to handle the data? Thanks in advance!
EDIT:
I cannot figure out how to scale x and y. If I use this code:
var x = d3.scale.ordinal()
.domain(monthsNames)
.range([0, width]);
var y = d3.scale.linear()
.domain([0,100])
.range([0, height]);
nothing is shown up then. If I print out the values that evaluate after using e.g. x(d.key) or x(d.value.fail) they are really strange numbers, sometimes even NaN.
EDIT:
d3.selectAll(".barsuccess")
.on('mouseover', function(d){
svg.append('text')
.attr("x", x(d.key))
.attr("y", y(d.value.success))
.text(d.value.success+"%")
.attr('class','success')
.style("font-size","0.7em")
})
.on('mouseout', function(d){
d3.selectAll(".success").remove()
});
d3.selectAll(".barfail")
.on('mouseover', function(d){
svg.append('text')
.attr("x", x(d.key)+x.rangeBand()/2)
.attr("y", y(d.value.fail))
.text(d.value.fail+"%")
.attr('class','fail')
.style("font-size","0.7em")
})
.on('mouseout', function(d){
d3.selectAll(".fail").remove()
});
Be sure to check out the bar chart tutorials here and here. You have basically all you need already. The connection between the axes and the data are the functions that map input values (e.g. "March") to output coordinates (e.g. 125). You (presumably) created these functions using d3.scale.*. Now all you need to do is use the same functions to map your data to coordinates in the SVG.
The basic structure of the code you need to add looks like
svg.selectAll(".barfail").data(d3.entries(data))
.enter().append("rect")
.attr("class", "barfail")
.attr("x", function(d) { x(d.key) })
.attr("width", 10)
.attr("y", function(d) { y(d.value.fail) })
.attr("height", function(d) { y(d.value.fail) });
and similar for success. If you use the same scale for the x axis for both types of bar, add a constant offset to one of them so the bars don't overlap. Colours etc can be set in the CSS classes.

Categories

Resources