I want to use d3.js to make a chart with vertical zooming of histrogram bars. I am doing something wrong, because result is not what I want.
This is my zoom:
svg.selectAll('g.info-group').each(function (d, i) {
var el = d3.select(this);
svg.select('.bars').attr('transform', 'translate(0,' + d3.event.translate[1] + ')');
el
.selectAll('.bar')
.call(function (s) {
barSetPosition(s, d.ib, i);
});
});
svg.select('.y.axis').call(yAxis);
Here is a jsFiddle
What is not working:
The y axis may have negative value and very positive where values do not exist.
If I do scroll the bars and y axis do not conform to each other.
How can I correct this?
Update (05.19.2015):
I found the solution for this questions and here it is - jsFiddle
var zoom = d3.behavior.zoom()
.y(yScale)
.scaleExtent([1, 2])
.on("zoom", function () {
var t = zoom.translate(),
tx = t[0],
ty = t[1],
scale = zoom.scale();
ty = Math.min(ty, 0);
ty = Math.max(ty, canvasH + margin.top - (canvasH + margin.top) * scale);
zoom.translate([tx, ty]);
svg
.select('.bars')
.attr("transform", "translate(0," + ty + ")");
svg.selectAll('.bar')
.attr('y', function (d) {
return (canvasH + margin.top) * scale - (yScale(0) - yScale(d.ib));
})
.attr('height', function (d) {
return yScale(0) - yScale(d.ib);
})
.attr('width', barScale.rangeBand());
svg.select('.y.axis').call(yAxis);
});
Now zooming work fine.
But now there two more questions.
When I am doing zoom and pan to the down the bar go under X axis and the numbers on axis become hidden.
When I am hovering bars and zooming, then pan to the down the bars is trembled.
How to fix this? Thanks.
I fixed all problems, the final code is - jsFiddle
This is the main part:
var zoom = d3.behavior.zoom()
.y(yScale)
.scaleExtent([1, 2])
.on("zoom", function () {
var t = zoom.translate(),
tx = t[0],
ty = t[1],
scale = zoom.scale();
ty = Math.min(ty, 0);
ty = Math.max(ty, canvasH + margin.top - (canvasH + margin.top) * scale);
zoom.translate([tx, ty]);
svg
.select('.bars')
.attr("transform", "translate(0," + ty + ")");
svg.selectAll('.bar')
.attr('y', function (d) {
return (canvasH + margin.top) * scale - (yScale(0) - yScale(d.ib));
})
.attr('height', function (d) {
var height = (yScale(0) - yScale(d.ib)) - (canvasH + margin.top) * (scale - 1) - ty;
if (height < 0) {
height = 0;
}
return height;
})
.attr('width', barScale.rangeBand());
svg.select('.y.axis').call(yAxis);
});
svg.call(zoom);
Related
So, implementing a brush behaviour inspired from M Bostock example I came across something I did not quite understand.
If set a callback for the 'end' event of the brush, this gets called as expected whenever you're interacting directly with the brush.
But whenever I recenter the brush, it seems that the end event is fired twice.
Why is that the case? Or, is it something I'm doing wrong here?
<!DOCTYPE html>
<style>
.selected {
fill: red;
stroke: brown;
}
</style>
<svg width="960" height="150"></svg>
<div>Event fired <span id="test"></span></div>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
var fired=0;
var randomX = d3.randomUniform(0, 10),
randomY = d3.randomNormal(0.5, 0.12),
data = d3.range(800).map(function() { return [randomX(), randomY()]; });
var svg = d3.select("svg"),
margin = {top: 10, right: 50, bottom: 30, left: 50},
width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom,
g = svg.append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var x = d3.scaleLinear()
.domain([0, 10])
.range([0, width]);
var y = d3.scaleLinear()
.range([height, 0]);
var brush = d3.brushX()
.extent([[0, 0], [width, height]])
.on("start brush", brushed)
.on("end", brushend);
var dot = g.append("g")
.attr("fill-opacity", 0.2)
.selectAll("circle")
.data(data)
.enter().append("circle")
.attr("transform", function(d) { return "translate(" + x(d[0]) + "," + y(d[1]) + ")"; })
.attr("r", 3.5);
g.append("g")
.call(brush)
.call(brush.move, [3, 5].map(x))
.selectAll(".overlay")
.each(function(d) { d.type = "selection"; }) // Treat overlay interaction as move.
.on("mousedown touchstart", brushcentered); // Recenter before brushing.
g.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
function brushcentered() {
var dx = x(1) - x(0), // Use a fixed width when recentering.
cx = d3.mouse(this)[0],
x0 = cx - dx / 2,
x1 = cx + dx / 2;
d3.select(this.parentNode).call(brush.move, x1 > width ? [width - dx, width] : x0 < 0 ? [0, dx] : [x0, x1]);
}
function brushed() {
var extent = d3.event.selection.map(x.invert, x);
dot.classed("selected", function(d) { return extent[0] <= d[0] && d[0] <= extent[1]; });
}
function brushend() {
document.getElementById('test').innerHTML = ++fired;
// console.log('end fired - ' + (++fired));
}
</script>
Whenever you want to stop an event from triggering multiple layers of actions, you can use:
d3.event.stopPropagation();
Here you can include it at the end of the brushcentered function:
function brushcentered() {
var dx = x(1) - x(0), // Use a fixed width when recentering.
cx = d3.mouse(this)[0],
x0 = cx - dx / 2,
x1 = cx + dx / 2;
d3.select(this.parentNode).call(brush.move, x1 > width ? [width - dx, width] : x0 < 0 ? [0, dx] : [x0, x1]);
d3.event.stopPropagation();
}
And the demo:
<style>
.selected {
fill: red;
stroke: brown;
}
</style>
<svg width="960" height="150"></svg>
<div>Event fired <span id="test"></span></div>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
var fired=0;
var randomX = d3.randomUniform(0, 10),
randomY = d3.randomNormal(0.5, 0.12),
data = d3.range(800).map(function() { return [randomX(), randomY()]; });
var svg = d3.select("svg"),
margin = {top: 10, right: 50, bottom: 30, left: 50},
width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom,
g = svg.append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var x = d3.scaleLinear()
.domain([0, 10])
.range([0, width]);
var y = d3.scaleLinear()
.range([height, 0]);
var brush = d3.brushX()
.extent([[0, 0], [width, height]])
.on("start brush", brushed)
.on("end", brushend);
var dot = g.append("g")
.attr("fill-opacity", 0.2)
.selectAll("circle")
.data(data)
.enter().append("circle")
.attr("transform", function(d) { return "translate(" + x(d[0]) + "," + y(d[1]) + ")"; })
.attr("r", 3.5);
g.append("g")
.call(brush)
.call(brush.move, [3, 5].map(x))
.selectAll(".overlay")
.each(function(d) { d.type = "selection"; }) // Treat overlay interaction as move.
.on("mousedown touchstart", brushcentered); // Recenter before brushing.
g.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
function brushcentered() {
var dx = x(1) - x(0), // Use a fixed width when recentering.
cx = d3.mouse(this)[0],
x0 = cx - dx / 2,
x1 = cx + dx / 2;
d3.select(this.parentNode).call(brush.move, x1 > width ? [width - dx, width] : x0 < 0 ? [0, dx] : [x0, x1]);
d3.event.stopPropagation();
}
function brushed() {
var extent = d3.event.selection.map(x.invert, x);
dot.classed("selected", function(d) { return extent[0] <= d[0] && d[0] <= extent[1]; });
}
function brushend() {
document.getElementById('test').innerHTML = ++fired;
}
</script>
-UPDATE-
For the purpose of this snippet, I can use a boolean flag to stop the first event and let the second go through. This means that I am still able to drag the brush after recentering, all in one go.
<!DOCTYPE html>
<style>
.selected {
fill: red;
stroke: brown;
}
</style>
<svg width="960" height="150"></svg>
<div>Event fired <span id="test"></span></div>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
var fired=0;
var justcentered = false;
var randomX = d3.randomUniform(0, 10),
randomY = d3.randomNormal(0.5, 0.12),
data = d3.range(800).map(function() {
return [randomX(), randomY()];
});
var svg = d3.select("svg"),
margin = { top: 10, right: 50, bottom: 30, left: 50 },
width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom,
g = svg.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var x = d3.scaleLinear()
.domain([0, 10])
.range([0, width]);
var y = d3.scaleLinear()
.range([height, 0]);
var brush = d3.brushX()
.extent([[0, 0], [width, height]])
.on("start brush", brushed)
.on("end", brushend);
var dot = g.append("g")
.attr("fill-opacity", 0.2)
.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("transform", function(d) {
return "translate(" + x(d[0]) + "," + y(d[1]) + ")";
})
.attr("r", 3.5);
g.append("g")
.call(brush)
.call(brush.move, [3, 5].map(x))
.selectAll(".overlay")
.each(function(d) { d.type = "selection"; }) // Treat overlay interaction as move.
.on("mousedown touchstart", brushcentered); // Recenter before brushing.
g.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
function brushcentered() {
var dx = x(1) - x(0), // Use a fixed width when recentering.
cx = d3.mouse(this)[0],
x0 = cx - dx / 2,
x1 = cx + dx / 2;
justcentered = true;
d3.select(this.parentNode)
.call(brush.move, x1 > width ? [width - dx, width] : x0 < 0 ? [0, dx] : [x0, x1]);
}
function brushed() {
var extent = d3.event.selection.map(x.invert, x);
dot.classed("selected", function(d) { return extent[0] <= d[0] && d[0] <= extent[1]; });
}
function brushend() {
if(justcentered) {
justcentered = false;
return;
}
document.getElementById('test').innerHTML = ++fired;
}
</script>
I am implementing the d3 tree version 4 with pan and zoom. To center a node with version 3, I found the following from a version 3 example.
// Function to center node when clicked/dropped so node doesn't get lost when collapsing/moving with large amount of children.
function centerNode(source) {
scale = zoomListener.scale();
x = -source.y0;
y = -source.x0;
x = x * scale + viewerWidth / 2;
y = y * scale + viewerHeight / 2;
d3.select('g').transition()
.duration(duration)
.attr("transform", "translate(" + x + "," + y + ")scale(" + scale + ")");
zoomListener.scale(scale);
zoomListener.translate([x, y]);
}
I have the following in version 4
_treeChanged: function() {
console.log(this.localName + '#' + this.id + ' tree data has changed');
// Assigns parent, children, height, depth
var aa = this.$.polymerTree.getBoundingClientRect();
var svg_height = aa.height - 20 - 30;
var svg_width = aa.width - 90 - 90;
this.root = d3.hierarchy(this.tree, function(d) { return d.children; });
this.root.x0 = svg_height / 2;
this.root.y0 = 0;
if (g) {
g.remove();
}
zoomListener = d3.zoom()
.scaleExtent([.5, 10])
.on("zoom", function () {
g.attr("transform", d3.event.transform);
}
);
g = d3.select(this.$.polymerTree)
.call(zoomListener)
.on("dblclick.zoom", null)
.append("g");
if (this.treemap) {
this.update(this.root);
this.centerNode(this.root);
}
},
How should I be implementing the center node to work with version 4?
My working d3-tree is at https://github.com/powerxhub/emh-d3-tree
Thank you!
I found my own fix:
centerNode: function(source) {
var aa = d3Polymer.$.polymerTree.getBoundingClientRect();
var svg_height = aa.height - 20 - 30;
var svg_width = aa.width - 90 - 90;
t = d3.zoomTransform(d3Polymer.$.polymerTree);
/*
To keep the node to be centered at the same x coordinate use
x = t.x;
To position the node at a certain x coordinate, use
x = source.y0;
x = -x *t.k + 50;
*/
x = t.x;
y = source.x0;
y = -y *t.k + svg_height / 2;
g.transition()
.duration(750)
.attr("transform", "translate(" + x + "," + y + ")scale(" + t.k + ")")
.on("end", function(){ g.call(zoomListener.transform, d3.zoomIdentity.translate(x,y).scale(t.k))});
},
I got it in a related posting at d3.js rewriting zoom example in version4.
You get the t from calling zoomTransform on the element that has the zoom added. In my case, it is directly on the SVG. g is the element with the bounding SVG g component. Here is the code that sets up the zoom:
zoomListener = d3.zoom()
.scaleExtent([.5, 10])
.on("zoom", function () {
g.attr("transform", d3.event.transform);
}
);
g = d3.select(this.$.polymerTree)
.call(zoomListener)
.on("dblclick.zoom", null)
.append("g");
Where this.$.polymerTree is the SVG element.
In the following code, I would like to put the donut legends outside the donut, on its right:
http://bl.ocks.org/juan-cb/1984c7f2b446fffeedde
Which line code should I change to do it?
var legend = svg.selectAll('.legend')
.data(color.domain())
.enter()
.append('g')
.attr('class', 'legend')
.attr('transform', function(d, i) {
var height = legendRectSize + legendSpacing;
var offset = height * color.domain().length / 2;
var horz = -3 * legendRectSize;
var vert = i * height - offset;
return 'translate(' + horz + ',' + vert + ')';
});
legend.append('rect')
.attr('width', legendRectSize)
.attr('height', legendRectSize)
.style('fill', color)
.style('stroke', color);
legend.append('text')
.attr('x', legendRectSize + legendSpacing)
.attr('y', legendRectSize - legendSpacing)
.text(function(d) { return d; });
Something similar to this one:
http://www.jqueryscript.net/demo/jQuery-Plugin-To-Convert-Tabular-Data-Into-Donut-Charts-Chart-js/
Edit: the solution was just a matter of changing horizontal and vertical coordinates. No need of complicated stuff.
These 2 variables:
var horz = -3 * legendRectSize; // X-axis translation.
var vert = i * height - offset; // Y-axis translation.
You could modify horz and vert formulas for translating. Like this:
var horz = 30 * legendRectSize; // Will be right side of the donut chart.
var vert = i * height - offset; // Still at the middle of the donut chart vertically.
I'm building a set of bar charts that will be updated dynamically with json data.
There will be occasions where the x.domain value is equal to zero or null so in those cases I don't want to draw the rectangle, and would like the overall height of my chart to adjust. However, the bars are being drawn based on data.length which may contain 9 array values, but some of those values are zeros, but render a white space within the graph.
I've attached an image of what is happening. Basically there are 9 data entries and only one of those actually contains the positive value, but the bars are still being drawn for all 9 points.
Here is my code:
d3.json("data/sample.json", function(json) {
var data = json.cand;
var margin = {
top: 10,
right: 20,
bottom: 30,
left: 0
}, width = parseInt(d3.select('#graphic').style('width'), 10),
width = width - margin.left - margin.right -50,
bar_height = 55,
num_bars = (data.length),
bar_gap = 18,
height = ((bar_height + bar_gap) * num_bars);
var x = d3.scale.linear()
.range([0, width]);
var y = d3.scale.ordinal()
.range([height, 0]);
var svg = d3.select('#graphic').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'] + ')');
var dx = width;
var dy = (height / data.length) + 8;
// set y domain
x.domain([0, d3.max(data, function(d) {
return d.receipts;
})]);
y.domain(0, d3.max(data.length))
.rangeBands([0, data.length * bar_height ]);
var xAxis = d3.svg.axis().scale(x).ticks(2)
.tickFormat(function(d) {
return '$' + formatValue(d);
});
var yAxis = d3.svg.axis()
.scale(y)
.orient('left')
.ticks(0)
.tickFormat(function(d) {
return '';
});
// set height based on data
height = y.rangeExtent()[1] +12;
d3.select(svg.node().parentNode)
.style('height', (height + margin.top + margin.bottom) + 'px');
// bars
var bars = svg.selectAll(".bar")
.data (data, function (d) {
if (d.receipts >= 1) {
return ".bar";
} else {
return null;
}
})
.enter().append('g');
bars.append('rect')
.attr("x", function(d, i) {
return 0;
})
.attr("y", function(d, i) {
if (d.receipts >= 1) {
return i * (bar_height + bar_gap - 4);
}else {
return null;
}
})
.attr("width", function(d, i) {
if (d.receipts >= 1) {
return dx * d.receipts;
} else {
return 0;
}
});
//y and x axis
svg.append('g')
.attr('class', 'x axis bottom')
.attr('transform', 'translate(0,' + height + ')')
.call(xAxis.orient('bottom'));
svg.append('g')
.call(yAxis.orient('left'));
});
I have several graphs set up to zoom on the container and it works great. However, on the initial load, the zoom level is way too close. Is there a method of setting the initial zoom level to avoid having to first zoom out? I am familiar with the .scale() method but have not had any luck implementing it. Is this the way to go or is there something I am missing?
Here is what I have thus far as pertaining to zoom:
var margin = {top: 20, right: 120, bottom: 20, left: 120},
width = 50000 - margin.right - margin.left,
height = 120000 - margin.top - margin.bottom;
var x = d3.scale.linear()
.domain([0, width])
.range([0, width]);
var y = d3.scale.linear()
.domain([0, height])
.range([height, 0]);
var tree = d3.layout.tree()
.size([height, width])
.separation(function(a, b) { return (a.parent == b.parent ? 1 : 2) / a.depth; });
var diagonal = d3.svg.diagonal()
.projection(function(d) { return [d.x, d.y]; });
function zoom(d) {
svg.attr("transform",
"translate(" + d3.event.translate + ")"+ " scale(" + d3.event.scale + ")");
}
var svg = d3.select("body").append("svg")
.attr("width", width + margin.right + margin.left)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
.attr("pointer-events", "all")
.call(d3.behavior.zoom()
.x(x)
.y(y)
.scaleExtent([0,8])
.on("zoom", zoom))
.append('g');
svg.append('rect')
.attr('width', width*5)
.attr('height', height)
.attr('border-radius', '20')
.attr('fill', 'sienna');
D3v4 answer
If you are here looking for the same but with D3 v4,
var zoom = d3.zoom().on("zoom", function(){
svg.attr("transform", d3.event.transform);
});
vis = svg.append("svg:svg")
.attr("width", width)
.attr("height", height)
.call(zoom) // here
.call(zoom.transform, d3.zoomIdentity.translate(100, 50).scale(0.5))
.append("svg:g")
.attr("transform","translate(100,50) scale(.5,.5)");
I finally got this to work by setting both the initial transform and the zoom behavior to the same value.
var zoom = d3.behavior.zoom().translate([100,50]).scale(.5);
vis = svg.append("svg:svg")
.attr("width", width)
.attr("height", height)
.call(zoom.on("zoom",zooming))
.append("svg:g")
.attr("transform","translate(100,50)scale(.5,.5)");
Applies to d3.js v4
This is similar to davcs86's answer, but it reuses an initial transform and implements the zoom function.
// Initial transform to apply
var transform = d3.zoomIdentity.translate(200, 0).scale(1);
var zoom = d3.zoom().on("zoom", handleZoom);
var svg = d3.select("body")
.append('svg')
.attr('width', 800)
.attr('height', 300)
.style("background", "red")
.call(zoom) // Adds zoom functionality
.call(zoom.transform, transform); // Calls/inits handleZoom
var zoomable = svg
.append("g")
.attr("class", "zoomable")
.attr("transform", transform); // Applies initial transform
var circles = zoomable.append('circle')
.attr("id", "circles")
.attr("cx", 100)
.attr("cy", 100)
.attr('r', 20);
function handleZoom(){
if (zoomable) {
zoomable.attr("transform", d3.event.transform);
}
};
See it in action: jsbin link
Adding this answer as an addendum to the accepted answer in case anyone is still having issues:
The thing that made this really easy to understand was looking here
That being said, I set three variables:
scale, zoomWidth and zoomHeight
scale is the initial scale you want the zoom to be, and then
zoomWidth and zoomHeight are defined as follows:
zoomWidth = (width-scale*width)/2
zoomHeight = (height-scale*height)/2
where width and height are the width and height of the "vis" svg element
the translate above is then amended to be:
.attr("transform", "translate("+zoomWidth+","+zoomHeight+") scale("+scale+")")
as well as the zoom function:
d3.behavior.zoom().translate([zoomWidth,zoomHeight]).scale(scale)
What this does is effectively ensures that your element is zoomed and centered when your visualization is loaded.
Let me know if this helps you! Cheers.
D3JS 6 answer
Let's say that you want your initial position and scale to be x, y, scale respectively.
const zoom = d3.zoom();
const svg = d3.select("#containerId")
.append("svg")
.attr("width", width)
.attr("height", height)
.call(zoom.transform, d3.zoomIdentity.translate(x, y).scale(scale)
.call(zoom.on('zoom', (event) => {
svg.attr('transform', event.transform);
}))
.append("g")
.attr('transform', `translate(${x}, ${y})scale(${k})`);
.call(zoom.transform, d3.zoomIdentity.translate(x, y).scale(scale) makes sure that when the zoom event is fired, the event.transform variable takes into account the translation and the scale. The line right after it handles the zoom while the last one is used to apply the translation and the scale only once on "startup".
I was using d3 with react and was very frustrated about the initial zoom not working.
I tried the solutions here and none of them worked, what worked instead was using an initial scale factor and positions and then updating the zoom function on the basis of those scale factor and positions
const initialScale = 3;
const initialTranslate = [
width * (1 - initialScale) / 2,
height * (1 - initialScale) / 2,
];
const container = svg
.append('g')
.attr(
'transform',
`translate(${initialTranslate[0]}, ${initialTranslate[1]})scale(${initialScale})`
);
The zoom function would look something like this
svg.call(
zoom().on('zoom', () => {
const transformation = getEvent().transform;
let {x, y, k} = transformation;
x += initialTranslate[0];
y += initialTranslate[1];
k *= initialScale;
container.attr('transform', `translate(${x}, ${y})scale(${k})`);
})
);
If you noticed the getEvent() as a function, it was because importing event from d3-selection was not working in my case. So I had to do
const getEvent = () => require('d3-selection').event;