D3 bar chart by date with context brushing - javascript

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.

Related

How can I make the D3 Collide force apply only on X

I would like to use the Collide force in D3 to prevent overlaps between nodes in a force layout, but my y-axis is time-based. I would like to only use the force on the nodes' x positions.
I have tried to combine the collide force with a forceY but if I increase the collide radius I can see that nodes get pushed off frame so the Y position is not preserved.
var simulation = d3.forceSimulation(data.nodes)
.force('links', d3.forceLink(data.links))
.force('x', d3.forceX(width/2))
.force('collision', d3.forceCollide().radius(5))
.force('y', d3.forceY( function(d) {
var date = moment(d.properties.date, "YYYY-MM-DD");
var timepos = y_timescale(date)
return timepos; }));
My hunch is that I could modify the source for forceCollide() and remove y but I am just using D3 with <script src="https://d3js.org/d3.v5.min.js"></script> and I'm not sure how to start making a custom version of the force.
Edit: I have added more context in response to the answer below:
- full code sample here
- screenshot here
Not quite enough code in the question to guarantee this is what you need, but making some assumptions:
Often when using a force layout you would allow the forces to calculate the positions and then reposition the node to a given [x,y] co-ordinate on tick e.g.
function ticked() {
nodeSelection.attr('cx', d => d.x).attr('cy', d => d.y);
}
Since you don't want the forces to affect the y co-ordinate just remove it from here i.e.
nodeSelection.attr('cx', d => d.x);
And set the y position on, say, enter:
nodeSelection = nodeSelection
.enter()
.append('circle')
.attr('class', 'node')
.attr('r', 2)
.attr('cy', d => {
// Set y position based on scale here
})
.merge(nodeSelection);

A few bars in the wrong place in a bar graph after sorting in ascending order

I have some javascript inside a Flask application that uses d3 to render a bar graph from some randomly generated data in a specific range. It allows sorting by label or value, in ascending or descending order. Sorting works fine in every situation except specifically when sorting in ascending order by value.Most of the time, at least 2 of the bars are not in the right order.
From inspecting the code, the problem seems to be that the 'x' d3 attribute for these bars is not set correctly, so they're displayed in the wrong spot on the x axis. I figure it must be a javascript issue and not a Flask issue since the rest works but I can't seem to figure it out.
Data generation(done in Python in the original code but I generated it in js with jiphy here):
function data_bar_graph(num=32) {
data_min = 0.0;
data_max = 100.0;
output = [];
for (i in range(int(num))) {
output.push(dict(label=str(uuid.uuid4())[:2], value=random.uniform(data_min, data_max)));
return output;
}
}
The js file in question: https://jsfiddle.net/turtles_/bk3svowo/ (it doesn't work properly since I'm not sure how to pass the data or generate the graph in jsfiddle outside of flask, but you can see the code. I'll try to figure out the data passing if it's too abstract like this).
EDIT: I'm about 95% sure the error is somewhere in this following segment(only the x is sometimes wrong,rest of the values are sorted correctly so I'm thinking the .attr("x" has something weird. Does the following seem correct?
data = data.sort(sortComparator);
// Map x-axis labels
xScale.domain(
data.map(function(d)
{
return d.label;
}
));
// Map y-axis values
yScale.domain([0, d3.max(data,
function(d)
{
return d.value;
}
)]);
// Draw the axes
drawAxisLines(chart.g, xAxis, yAxis, computedHeight, 0, 0, 0);
var bars = chart.g.append("g")
.attr("class", "bars")
.selectAll(".bar")
.data(data)
.enter()
.append("rect")
.attr("x",
function(d)
{
return xScale(d.label);
}
)
.attr("y",
function(d)
{
return yScale(d.value);
}
)
.attr("fill",
function(d)
{
return colours(d.label);
}
)
.attr("height",
function(d)
{
return computedHeight - yScale(d.value);
}
)
.attr("width", xScale.rangeBand())
.style(cssStyling.bar);
}
sortComparator is the d3.ascending/d3.descending that was mentioned.
Since you don't have a working example, it's difficult to know exactly what the issue is, but there is commonly pitfall here: you're most likely comparing the numbers as strings (e.g. "12" < "2", whereas 12 > 2).
To make sure you're comparing numbers, you can do the following:
data = data.sort(function(a,b) { return d3.ascending(+a.value, +b.value); })
By preceding the fields with a +, you're coaxing those fields to numbers (which your Python code may not do automatically).

d3.js Strange dragging behaviour when using dynamic scaling

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]);

D3: Passing object array data to a barchart on mouseover

Working with two charts in D3. I have a pie chat displaying parent data regarding a budget. When the user mouses over a pie slice, I am trying to push that slice's array data to a bar chart.
My data is setup like so:
{"Department":"Judiciary",
"Funds1415":317432,
"Fundsb":"317.4",
"annual": [ 282,288,307,276,276,298,309,317,317 ]
},
I'm trying to use this to pass the annual array to the barchart:
path.on('mouseover', function(d) {
...
bars.selectAll('rect').transition().attr("y", function(d) { return h - d.data.annual /125; });
bars.selectAll('rect').transition().attr("height", function(d) { return d.data.annual / 125; });
});
And here's the barchart I'm trying to send it to:
var bars = svg.selectAll("rect")
.data(budget)
.enter()
.append("rect")
.attr("class", "barchart")
.attr("transform", "translate(26,109)")
.attr("fill", function(d, i) {
return color2(i);
})
.attr('class', 'barchart')
.attr("x", function(d, i) {
return i * 14;
})
.attr("width", 12)
.attr("y", 100)
.attr("height", 100);
Link to full code here:
http://jsbin.com/zayopecuto/1/edit?html,js,output
Everything 'seems' to be working, except the data either isn't passing or it isn't updating the bar chart.
I've been banging my head up against this for a couple of days, to no avail. Originally I was thinking of placing the annual data in separate arrays and just transitioning from data source to data source on mouseover, but that seems backward and unnecessary.
First, your selector is wrong. bars is already a collection of rects, so you can't re-select the rects. Second, you haven't bound "updated" data to those rects. So, with this in mind, it becomes:
bars
.data(d.data.annual)
.transition()
.attr("height", function(d) {
return d / 125;
})
.attr("y", function(d) {
return h - d /125;
});
Here's an updated example.
What I understand from your code and comment is that, you have data points for your donut chart and each data object contains a property called 'annual' which you want to use as a input data for the bar chart.
You should be calling a separate function to plot your bar chart passing it the annual data array.
Clear the existing bar chart on 'mouseout' event, so that a new bar chart can be plotted on the next 'mouseover' event. You can use jQuery empty() function for clearing out the chart container.

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