So I'm working through some d3 tutorials and learning a bit about why things work the way they do, and I've run into a peculiar instance of selection not behaving as expected. So I'm wondering if anyone can explain this to me.
var sales = [
{ product: 'Hoodie', count: 7 },
{ product: 'Jacket', count: 6 },
{ product: 'Snuggie', count: 9 },
];
var rects = svg.selectAll('rect')
.data(sales).enter();
var maxCount = d3.max(sales, function(d, i) {
return d.count;
});
var x = d3.scaleLinear()
.range([0, 300])
.domain([0, maxCount]);
var y = d3.scaleBand()
.rangeRound([0, 75])
.domain(sales.map(function(d, i) {
return d.product;
}));
rects.append('rect')
.attr('x', x(0))
.attr('y', function(d, i) {
return y(d.product);
})
.attr('height', y.bandwidth())
.attr('width', function(d, i) {
return x(d.count);
});
This all works good and fine, generates 3 horizontal bars that correspond to the data in sales, but here's where I'm seeing the ambiguity:
sales.pop();
rects.data(sales).exit().remove();
The last line is supposed to remove the bar that was popped, from the visual but it doesn't work. I think that there must be something going on with the d3 selection that I'm missing, because this does work:
d3.selectAll('rect').data(sales).exit().remove();
Also when I break out the first one that doesn't work, it does appear to be selecting the correct element on exit, but just doesn't seem to be removing it. Anyway if anyone can explain what's going on here that would be very helpful, thanks!
Note: using d3 v4
This is your rects selection:
var rects = svg.selectAll('rect')
.data(sales).enter();
So, when you do this:
rects.data(sales).exit().remove();
You are effectively doing this:
svg.selectAll('rect')
.data(sales)
.enter()
.data(sales)
.exit()
.remove();
Thus, you are binding data, calling enter, binding data again and calling exit on top of all that! Wow!
Solution: just create a regular, old-fashioned "update" selection:
var rects = svg.selectAll('rect')
.data(sales);
Here is a basic demo with your code, calling exit after 2 seconds:
var svg = d3.select("svg")
var sales = [{
product: 'Hoodie',
count: 7
}, {
product: 'Jacket',
count: 6
}, {
product: 'Snuggie',
count: 9
}, ];
draw();
function draw() {
var rects = svg.selectAll('rect')
.data(sales);
var maxCount = d3.max(sales, function(d, i) {
return d.count;
});
var x = d3.scaleLinear()
.range([0, 300])
.domain([0, maxCount]);
var y = d3.scaleBand()
.rangeRound([0, 75])
.domain(sales.map(function(d, i) {
return d.product;
}))
.padding(.2);
var rectsEnter = rects.enter().append('rect')
.attr('x', x(0))
.attr('y', function(d, i) {
return y(d.product);
})
.attr('height', y.bandwidth())
.attr('width', function(d, i) {
return x(d.count);
});
rects.exit().remove();
}
d3.timeout(function() {
sales.pop();
draw()
}, 2000)
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg></svg>
rects is already a d3 selection, so you only need rects.exit().remove()
Related
I just started learning javascript and d3.js by taking a couple of lynda.com courses. My objective is to create a function that takes an array of numbers and a cutoff and produces a plot like this one:
I was able to write javascript code that generates this:
Alas, I'm having troubles figuring out a way to tell d3.js that the area to the left of -opts.threshold should be read, the area in between -opts.threshold and opts.threshold blue, and the rest green.
This is my javascript code:
HTMLWidgets.widget({
name: 'IMposterior',
type: 'output',
factory: function(el, width, height) {
// TODO: define shared variables for this instance
return {
renderValue: function(opts) {
console.log("MME: ", opts.MME);
console.log("threshold: ", opts.threshold);
console.log("prob: ", opts.prob);
console.log("colors: ", opts.colors);
var margin = {left:50,right:50,top:40,bottom:0};
var xMax = opts.x.reduce(function(a, b) {
return Math.max(a, b);
});
var yMax = opts.y.reduce(function(a, b) {
return Math.max(a, b);
});
var xMin = opts.x.reduce(function(a, b) {
return Math.min(a, b);
});
var yMin = opts.y.reduce(function(a, b) {
return Math.min(a, b);
});
var y = d3.scaleLinear()
.domain([0,yMax])
.range([height,0]);
var x = d3.scaleLinear()
.domain([xMin,xMax])
.range([0,width]);
var yAxis = d3.axisLeft(y);
var xAxis = d3.axisBottom(x);
var area = d3.area()
.x(function(d,i){ return x(opts.x[i]) ;})
.y0(height)
.y1(function(d){ return y(d); });
var svg = d3.select(el).append('svg').attr("height","100%").attr("width","100%");
var chartGroup = svg.append("g").attr("transform","translate("+margin.left+","+margin.top+")");
chartGroup.append("path")
.attr("d", area(opts.y));
chartGroup.append("g")
.attr("class","axis x")
.attr("transform","translate(0,"+height+")")
.call(xAxis);
},
resize: function(width, height) {
// TODO: code to re-render the widget with a new size
}
};
}
});
In case this is helpful, I saved all my code on a public github repo.
There are two proposed solutions in this answer, using gradients or using multiple areas. I will propose an alternate solution: Use the area as a clip path for three rectangles that together cover the entire plot area.
Make rectangles by creating a data array that holds the left and right edges of each rectangle. Rectangle height and y attributes can be set to svg height and zero respectively when appending rectangles, and therefore do not need to be included in the array.
The first rectangle will have a left edge at xScale.range()[0], the last rectangle will have an right edge of xScale.range()[1]. Intermediate coordinates can be placed with xScale(1), xScale(-1) etc.
Such an array might look like (using your proposed configuration and x scale name):
var rects = [
[x.range()[0],x(-1)],
[x(-1),x(1)],
[x(1),x.range()[1]]
]
Then place them:
.enter()
.append("rect")
.attr("x", function(d) { return d[0]; })
.attr("width", function(d) { return d[1] - d[0]; })
.attr("y", 0)
.attr("height",height)
Don't forget to set a clip-path attribute for the rectangles:
.attr("clip-path","url(#areaID)"), and to set fill to three different colors.
Now all you have to do is set your area's fill and stroke to none, and append your area to a clip path with the specified id:
svg.append("clipPath)
.attr("id","area")
.append("path")
.attr( // area attributes
...
Here's the concept in action (albeit using v3, which shouldn't affect the rectangles or text paths.
Thanks to #andrew-reid suggestion, I was able to implement the solution that uses multiple areas.
HTMLWidgets.widget({
name: 'IMposterior',
type: 'output',
factory: function(el, width, height) {
// TODO: define shared variables for this instance
return {
renderValue: function(opts) {
console.log("MME: ", opts.MME);
console.log("threshold: ", opts.threshold);
console.log("prob: ", opts.prob);
console.log("colors: ", opts.colors);
console.log("data: ", opts.data);
var margin = {left:50,right:50,top:40,bottom:0};
xMax = d3.max(opts.data, function(d) { return d.x ; });
yMax = d3.max(opts.data, function(d) { return d.y ; });
xMin = d3.min(opts.data, function(d) { return d.x ; });
yMin = d3.min(opts.data, function(d) { return d.y ; });
var y = d3.scaleLinear()
.domain([0,yMax])
.range([height,0]);
var x = d3.scaleLinear()
.domain([xMin,xMax])
.range([0,width]);
var yAxis = d3.axisLeft(y);
var xAxis = d3.axisBottom(x);
var area = d3.area()
.x(function(d){ return x(d.x) ;})
.y0(height)
.y1(function(d){ return y(d.y); });
var svg = d3.select(el).append('svg').attr("height","100%").attr("width","100%");
var chartGroup = svg.append("g").attr("transform","translate("+margin.left+","+margin.top+")");
chartGroup.append("path")
.attr("d", area(opts.data.filter(function(d){ return d.x< -opts.MME ;})))
.style("fill", opts.colors[0]);
chartGroup.append("path")
.attr("d", area(opts.data.filter(function(d){ return d.x > opts.MME ;})))
.style("fill", opts.colors[2]);
if(opts.MME !==0){
chartGroup.append("path")
.attr("d", area(opts.data.filter(function(d){ return (d.x < opts.MME & d.x > -opts.MME) ;})))
.style("fill", opts.colors[1]);
}
chartGroup.append("g")
.attr("class","axis x")
.attr("transform","translate(0,"+height+")")
.call(xAxis);
},
resize: function(width, height) {
// TODO: code to re-render the widget with a new size
}
};
}
});
I have an external JSON file structured like so:
{
data: [
{
0: 'cat',
1: 232,
2: 45
},
{
0: 'dog',
1: 21,
2: 9
},
{
0: 'lion',
1: 32,
2: 5
},
{
0: 'elephant',
1: 9,
2: 4
},
]
}
Using d3.js I want to access the data with key 2 for the height in a bar chart, so I have set this up:
d3.select('#chart').append('svg')
.selectAll('rect')
.data(data)
.enter()
.append('rect')
.attr('width', barWidth)
.attr('height', function(d) {
return d.data['2'];
});
I can see the SVG canvas but I can't get the rectangles for the bar chart height, any ideas anyone?
Thanks
I changed a few things to make the code function how a default bar chart would. Your main problem was using d.data instead of d. After that, you have to set the x and y coordinates and an alternating color (or a padding with x) so that the rectangles don't overlap each other.
var width = 30;
var padding = 5;
var chartheight = 100;
d3.select('#chart').append('svg')
.selectAll('rect')
.data(data)
.enter()
.append('rect')
.attr('width', width)
.attr('height', function(d) {
return d[2];
})
.attr('x', function(d, i) {
return i*(width + padding);
})
.attr('y', function(d, i) {
return chartheight - d[2];
});
d3.selectAll('rect')
.style('fill', function(d, i) {
return i % 2 == 0 ? "#0f0" : "#00f";
});
The d variable sent to your function is already your data, and its called for each item in your array:
.attr('height', function(d) {
return d['2'];
});
I'm trying to draw what I think amounts to a force graph in d3, but in a single flat line. I would like around 4-5 points of varying size depending on their magnitude, spaced evenly between them (not by center, but the distance between the sides of the circles should be constant), and lines to join them. So in ASCII format, something like:
o---O---o---O
I was trying to avoid a complicated calculation to figure out the center coordinates and start and end of each line, so it seemed like the force layout might do the trick. Unfortunately, when I put it together, I can't seem to get it to work very well. Often times points end up behind other points, so for a 4 node graph like above, it comes out looking something more like:
O---O
Is there any way to get the force layout to work in 1 dimension instead of 2? Or am I stuck doing all of the spacing calculations myself? The code I'm working with is below:
var width = 500;
var height = 200;
var svg = d3.select($el[0])
.append('svg')
.attr('width', width)
.attr('height', height);
var data_nodes = [
{ x: width / 2, y: height / 2, count: 5 },
{ x: width / 2, y: height / 2, count: 0 },
{ x: width / 2, y: height / 2, count: 1 },
{ x: width / 2, y: height / 2, count: 10 },
];
var data_links = [
{ source: 0, target: 1 },
{ source: 1, target: 2 },
{ source: 2, target: 3 },
];
var force = d3.layout.force()
.nodes(data_nodes)
.links(data_links)
.linkDistance(150)
.linkStrength(0.5)
.gravity(0.7)
.friction(0.3)
.size([width, height])
.charge(-300);
var links = svg.selectAll('line')
.data(data_links)
.enter()
.append('line')
.attr('stroke', '#65759E')
.attr('stroke-width', 4)
.attr('x1', function (d) { return data_nodes[d.source].x; })
.attr('y1', function (d) { return data_nodes[d.source].y; })
.attr('x2', function (d) { return data_nodes[d.target].x; })
.attr('y2', function (d) { return data_nodes[d.target].y; });
var nodes = svg.selectAll('circle')
.data(data_nodes)
.enter()
.append('circle')
.attr('fill', '#65759E')
.attr('r', function (d) { return 10 + Math.sqrt(d.count) * 4; })
.attr('cx', function (d, i) { return d.x + i * 10; })
.attr('cy', function (d, i) { return d.y; });
force.on('tick', function () {
nodes.attr('cx', function (d) { return d.x; });
links.attr('x1', function (d) { return d.source.x; })
.attr('x2', function (d) { return d.target.x; });
});
force.start();
The difficulty you're having stems from the fact that there is no way for the nodes to get around each other without leaving the 1D line you're forcing them into. The repulsive forces prevent a node from passing over top of another node to get to the other side, so they become trapped in these sub-optimal arrangements.
By default, d3 force layout initializes nodes in a random position. However, you can initialize them yourself by setting the x and y properties of the data nodes objects before starting the layout. If you initialize the graph with the nodes ordered in a row, according to the order of their connections, then the force layout can handle the spacing for you.
I'm displaying data on a line graph with monotone interpolation. But in some cases little loops appear.
Is there away to fix this while keeping the same look as monotone (so not just using linear) and having the line hit all the points?
JSFiddle showing the issue here: http://jsfiddle.net/WkvMx/3/
Code to replicate:
var data =
[
{date: '2013-08-01', value: 234},
{date: '2013-08-02', value: 244},
{date: '2013-08-04', value: 1034},
{date: '2013-08-06', value: 1004},
{date: '2013-08-28', value: 234},
{date: '2013-08-29', value: 233}
]
var width = 500;
var height = 250;
var parse = d3.time.format("%Y-%m-%d").parse;
var x = d3.time.scale()
.range([0, width])
.domain([parse('2013-08-01'), parse('2013-09-01')]);
var max = d3.max(data, function (v) { return v.value; });
var min = d3.min(data, function (v) { return v.value; });
var y = d3.scale.linear()
.range([height, 0])
.domain([min, max]).nice();
var line = d3.svg.line()
.interpolate("monotone")
.x(function(d) { return x(parse(d.date)); })
.y(function(d) { return y(d.value); });
var svg = d3.select('body').append("svg")
.attr("width", width)
.attr("height", height)
svg.append("path")
.datum(data)
.attr("class", "line")
.attr("d", line)
.attr("stroke", 'red')
var circlegroup = svg.append("g")
circlegroup.selectAll(".dot")
.data(data)
.enter().append("circle")
.attr("r", 3)
.attr("cx", function(d) { return x(parse(d.date)); })
.attr("cy", function(d) { return y(d.value); })
The monotone interpolation is intended to interpolate monotone datasets, that's it, datasets whose y-coordinate increases, and even in that case, the monotonicity of the interpolation function is not guaranteed, although it can be achieved by modifying the tangents of the curve. The tangents can be controled globally (kind of) using the tension method, but it can't be set point by point.
As #Lars pointed out, you can try other interpolation methods better suited for your data, and tweak the tension to get a smooth line that passes by all the input points.
I have this array:
var bucket_contents = [8228, 21868, 12361, 15521, 3037, 2656];
I am trying to alter the width of a series of rects based on these values using range/domain. This is my code:
var bucket_width = d3.scale.linear()
.domain([d3.min(bucket_contents), d3.max(bucket_contents)])
.range([0, 1000]);
bucket.forEach(function(d, i) {
d.x = (width / bucket.length) * i;
d.y = (height / 2) - 25;
d.w = bucket_width(i);
console.log(bucket_width(i));
});
d3.select("#plot").selectAll(".bucket")
.data(bucket)
.enter()
.append("rect")
.attr("class", "bucket")
.attr("x", function(d, i) { return d.x; })
.attr("y", function(d, i) { return d.y; })
.attr('width', function(d, i) { return d.w; })
.attr('height', '50')
.style("fill", '#000');
Currently the console log bucket_width(i) inside the forEach loop outputs the following:
-138.24692900270665
-138.1948782011243
-138.14282739954194
-138.09077659795963
-138.03872579637726
-137.98667499479492
Console log on d3.min(..) is 2656 and d3.max(..) is 21868 as expected.
What am I doing wrong? I thought range 'normalizes' any values within that range, i.e. 21868 would return 1000 and 2656 would return 0.
Your problem is that you are calling bucket_width(i) instead of bucket_width(d). Hence, you are passing in values of 0, 1, etc. which are less than the min value of 2656 by quite some.
Demo: http://jsfiddle.net/2tq8S/
var contents = [8228, 21868, 12361, 15521, 3037, 2656];
var bucket_width = d3.scale.linear().domain(d3.extent(contents)).range([0, 1000]);
contents(function(d, i){
console.log( d, i, bucket_width(i), bucket_width(d) );
});
Output:
8228 0 -138.24 290.03
21868 1 -138.19 1000.00
12361 2 -138.14 505.15
15521 3 -138.09 669.63
3037 4 -138.03 19.83
2656 5 -137.98 0.00