I am creating a bar graph and I would like to have a focus feature in it. So whenever I select, mouseover event, a particular bar, the width and height of bar increase and everything else remains the same making this bar more in focus. Something like this :-
Lets say if I hover mouse on 2nd bar, it should look like this :-
Is is possible to leverage focus and zoom functionality of d3.js?
Threw something together for you https://jsfiddle.net/guanzo/h1hdet8d/1/
It doesn't account for the axis/labels at all, but it should get you started. The idea is that on hover you increase the scale of the bar. Calculate how much more width it has, then divide that by 2 to get how much you should shift the other bars.
Important: Apply .style('transform-origin','bottom') to the bars so that they grow upward and to both sides evenly.
g.selectAll(".bar")
.data(data)
.enter().append("rect")
.attr("class", "bar")
.attr("x", function(d) { return x(d.letter); })
.attr("y", function(d) { return y(d.frequency); })
.attr("width", x.bandwidth())
.attr("height", function(d) { return height - y(d.frequency); })
.style('transform-origin','bottom')
.on('mouseover',mouseover)
.on('mouseout',mouseout)
function mouseover(data,index){
var bar = d3.select(this)
var width = bar.attr('width')
var height = bar.attr('height')
var scale = 1.5;
var newWidth = width* scale;
var newHeight = height*scale;
var shift = (newWidth - width)/2
bar.transition()
.style('transform','scale('+scale+')')
d3.selectAll('.bar')
.filter((d,i)=> i < index)
.transition()
.style('transform','translateX(-'+shift+'px)')
d3.selectAll('.bar')
.filter((d,i)=> i > index)
.transition()
.style('transform','translateX('+shift+'px)')
}
function mouseout(data,index){
d3.select(this).transition().style('transform','scale(1)')
d3.selectAll('.bar')
.filter(d=>d.letter !== data.letter)
.transition()
.style('transform','translateX(0)')
}
Related
I am using D3 v4 (most, if not all, of the examples out there are for v3). Back in v3, they had something called rangeBand() which would be able to dynamically position everything neatly on the x-axis for me.
Now, in v4, I am wondering how to do that.
I have a bar chart:
var barEnter = vis.selectAll("g")
.data(rawdata)
.enter()
.append('g')
.append("rect")
.attr("class", "bar")
.attr("x", function(d) { return x(d.key); })
.attr("y", function(d) { return y(d.value); })
.attr("height", function(d) { return height - y(d.value); })
.attr("width", canvas_width / rawdata.length);
It is the width of this bar this is messing me up. If I set it to canvas_width / rawdata.length, it nicely positions the bars centered around each tick on the x-axis. The problem is that all the bars are pressed together and there is no padding in between.
So, naturally, I tried to do x.paddingInner(.5) which does add some padding but now the bars are not centered around the tick marks. Doing anything with x.paddingOuter() messes things up even more.
After searching around, I found that rangeBand() is what I want but that's only for v3. In the v4 docs, there is nothing that quite looks like it. Is it rangeRound()? Is it align()? I'm not sure. If anyone can point me in the right direction, it'd be greatly appreciated.
Without seeing your code for the axis, I suppose you're using scaleOrdinal(). If that's the case, you can change for scaleBand(), in which it's very easy to center the bar around the tick.
All you need is:
band.paddingInner([padding]): Sets the inner padding of the bars
band.bandwidth(): Gives you the bandwidth of each bar.
Then, you set the x position using the corresponding variable in your data and the width using bandwidth().
This is a small snippet to show you how it works:
var w = 300, h = 100, padding = 20;
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
var data = [{name: "foo", value:50},
{name: "bar", value:80},
{name: "baz", value: 20}];
var xScale = d3.scaleBand()
.range([0,w])
.domain(data.map(function(d){ return d.name}))
.paddingInner(0.2)
.paddingOuter(0.2);
var xAxis = d3.axisBottom(xScale);
var bars = svg.selectAll(".bars")
.data(data)
.enter()
.append("rect");
bars.attr("x", function(d){ return xScale(d.name)})
.attr("width", xScale.bandwidth())
.attr("y", function(d){ return (h - padding) - d.value})
.attr("height", function(d){ return d.value})
.attr("fill", "teal");
var gX = svg.append("g")
.attr("transform", "translate(0," + (h - padding) + ")")
.call(xAxis);
<script src="https://d3js.org/d3.v4.min.js"></script>
I'm working with this d3.js example and am looking to change the charts entire orientation to go instead from right to left.
I was able to reverse the x-axis scale:
var x = d3.scale.linear().range([width, 0]);
and the placement of the y axis:
svg.append("g").attr("class", "y axis")
.attr("transform", "translate("+width+", 0)")
.append("line").attr("y1", "100%");
I believe I have to set the transform on each bar to the chart width - the bar width however applying ANY transform to the containing g has no effect.
function bar(d) {
var bar = svg.insert("g", ".y.axis")
.attr("class", "enter")
.attr("transform", "translate(0,5)")
.selectAll("g")
.data(d.children)
.enter().append("g");
//DOESNT WORK
bar.attr("transform", function(n){ return "translate("+ (width - x(n.value)) +", 0)"; })
.style("cursor", function(d) { return !d.children ? null : "pointer"; })
.on("click", down);
bar.append("text")....
bar.append("rect")....
return bar;
}
No transform is set on bar even if I just do a test with a fixed value, the result is translate(0,0) in the resulting page.
Why doesnt the transform not get applied here and is this even the correct way to make the bars right align? I also need the text to be on the right side of the bar instead of on the left and it seems that changing the order of appending makes no difference in this regard.
The problem with this particular example is that the code is a bit confusing -- the positions of the rect elements are set in various non-obvious places. In particular, the transform you are setting is overwritten immediately by other code.
I've modified the example to do what you want here. The key points are that I'm moving the g element containing all bars to the right
var bar = svg.insert("g", ".y.axis")
.attr("class", "enter")
.attr("transform", "translate("+width+",5)")
.selectAll("g")
// etc
and setting the x position of the rect elements to be their negative width
bar.append("rect")
.attr("x", function(d) { return width-x(d.value); })
.attr("width", function(d) { return x(d.value); })
.attr("height", barHeight);
The x position needs to be set in a few other places in the code in this way -- there're almost certainly more elegant ways to do this.
I'm putting together a D3 vertical bar chart and can't adjust the bar's height. When I adjust the barHeight variable from the following javascript, when rendered the bar's look exactly the same. Meaning if I took off the *50 in barHeight nothing would change. I'm probably overlooking something really really simple here, but how do I adjust the height?
var bars = svg.selectAll("rect")
.data(data_votes);
bars.enter()
.append("rect")
// .transition()
// .duration(1000);
bars.attr("x", function(d, i) {
return i * (w / data_votes.length);
})
.attr("y", function(d){
return h-d;
})
// Less bars == more width
.attr("width", w / data_votes.length - barPadding)
.attr("height", function(d){
var barHeight = d*50;
return barHeight;
})
.attr("fill", "teal");
bars.exit().remove();
Here is a working example with some corrected code:
And a very good example of why links to other sites that don't include the code are a bad idea. Tributary.io is not responding in December of 2019.
http://tributary.io/inlet/5767993
(You can click on any number and change it with the slider to see it's effect on the output)
I think the main issue was that the scale factor needed to be applied to both the "y" attribute and the height attribute.
You're right, I was adjusting the offset with the height so while the height pixel was actually increasing, it was offset proportionally and hidden off the edge of the svg canvas.
Here's how I adjusted it
.attr("y", function(d){
return h-(d*4);
})
// Less bars == more width
.attr("width", w / data_votes.length - barPadding)
.attr("height", function(d) {
return d * 4 ;
})
I am attempting to add labels/axis/titles/etc to a D3 bar graph. I can get something close to the right size, however I end up clipping off part of the last bar (so the last bar is skinnier than the others).
Here is the pertinent code:
var x = d3.scale.linear()
.domain([0, object.data.length])
.range([0, object.width]);
var y = d3.scale.linear()
.domain([object.min-(object.max-object.min)*.15, object.max (object.max-object.min)*.15])
.rangeRound([ object.height - 30, 0]);
var vis = d3.select(object.ele)
.append("svg:svg")
.attr("width", object.width)
.attr("height", object.height)
.append("svg:g");
vis.selectAll("g")
.data(object.data)
.enter().append("rect")
.attr("x", function(d, i) { return x(i); })
.attr("y", function(d) { return object.height - y(d.value); })
.attr("width", object.width/object.data.length - 1)
.attr("height", function(d) { return y(d.value); })
.attr("transform", "translate(30,-30)");
At the moment everything (labels, axis, and so on) is 30px. How do I correctly alter the graph to make room for whatever else I need?
You are cutting off your last bar because you use translate the x coordinate but your the range of your x is only to the width without the extra 30 pixels.
Also it may be easier to simplify your y domain to use .domain([object.min, object.max]) then have the "y" and "height" functions reversed. This way you start the rect at the y(d.value) and make it's height object.height - y(d.value).
I would create three groups initially, one for your y axis, one for x axis, and then another for the bars. Draw your bars inside the last group and then translate the whole group itself instead of each individual bar. Increase the size of your object.width and object.height to match the total space you want.
Is there a way to limit the size of a brush, even though the extent is larger?
I put together a brush with only an x-scale that can be moved and resized. I would like to be able to limit the extent to which it can be resized by the user (basically only up to a certain point).
In the following example, the brush function stops updating when the brush gets bigger than half the maximum extent. The brush itself, though, can still be extended. Is there a way to prevent this from happening? Or is there a better way of handling this?
Many thanks!
See this code in action here: http://bl.ocks.org/3691274 (EDIT: This demo now works)
bar = function(range) {
var x_range = d3.scale.linear()
.domain([0, range.length])
.range([0, width]);
svg.selectAll("rect.items").remove();
svg.selectAll("rect.items")
.data(range)
.enter().append("svg:rect")
.attr("class", "items")
.attr("x", function(d, i) {return x_range(i);})
.attr("y", 0)
.attr("width", width/range.length-2)
.attr("height", 100)
.attr("fill", function(d) {return d})
.attr("title", function(d) {return d});
}
var start = 21;
bar(data.slice(0, start), true);
var control_x_range = d3.scale.linear()
.domain([0, data.length])
.range([0, width]);
controlBar = svg.selectAll("rect.itemsControl")
.data(data)
.enter().append("svg:rect")
.attr("class", "itemsControl")
.attr("x", function(d, i) {return control_x_range(i);})
.attr("y", 110)
.attr("width", width/data.length-2)
.attr("height", 20)
.attr("fill", function(d) {return d});
svg.append("g")
.attr("class", "brush")
.call(d3.svg.brush().x(d3.scale.linear().range([0, width]))
.extent([0,1*start/data.length])
.on("brush", brush))
.selectAll("rect")
.attr("y", 110)
.attr("height", 20);
function brush() {
var s = d3.event.target.extent();
if (s[1]-s[0] < 0.5) {
var start = Math.round((data.length-1)*s[0]);
var end = Math.round((data.length-1)*s[1]);
bar(data.slice(start,end));
};
}
I eventually solved it by redrawing the brush to its maximum allowed size when the size is exceeded:
function brush() {
var s = d3.event.target.extent();
if (s[1]-s[0] < 0.5) {
var start = Math.round((data.length-1)*s[0]);
var end = Math.round((data.length-1)*s[1]);
bar(data.slice(start,end));
}
else {d3.event.target.extent([s[0],s[0]+0.5]); d3.event.target(d3.select(this));}
}
Demo: http://bl.ocks.org/3691274
I'm still interested in reading better solutions.
Here's another strategy using d3.v4 an ES6:
brush.on('end', () => {
if (d3.event.selection[1] - d3.event.selection[0] > maxSelectionSize) {
// selection is too large; animate back down to a more reasonable size
let brushCenter = d3.event.selection[0] + 0.5 * (d3.event.selection[1] - d3.event.selection[0]);
brushSel.transition()
.duration(400)
.call(brush.move, [
brushCenter - 0.49 * maxSelectionSize,
brushCenter + 0.49 * maxSelectionSize
]);
} else {
// valid selection, do stuff
}
});
If the brush selection size is too large when the user lets go of it, it animates back down to the specified maximum size (maxSelectionSize). At that point, the 'end' event will fire again, with an acceptable selection size.
Note the 0.49 scalar: this is required to prevent floating point / rounding errors that could cause an infinite loop if the brush is move()d to a size that is still too large.
Here is an example of limiting the brush's minimum width as 100px and maximum width as 200ps. I added a few lines into d3.v4.js, for setting the limitation of the brush width.
Added brush.limit([min, max]) for set the limitation:
var _limit = null;
brush.limit = function (l) {
_limit = l;
}
Break mouse move event in move() function:
if (_limit && e1 - w1 < _limit[0]) {
return;
}
(Demo) (Source Code)
Nice piece of code, but I found a little bug.
It does lock your brush whenever s[1]-s[0] < 0.5, but if you keep pressed the resize and bring all your brush to the oposite direction, it starts "moving" the brush without any action (i.e. it doesn't behave as it should).
I am pretty sure I can come up with a reasonable solution. If so, I'll re-post it here.
(sorry to post it here as an answer, but as you've already answered it once, I cannot post as a comment, I guess).