Reset D3 zoom translate - javascript

I'm trying to create a draggable element in D3 that doesn't drag beyond the page.
Like in this example:
http://jsfiddle.net/Lhwpff2r/1/
var margin = {top: 20, right: 10, bottom: 20, left: 10};
var width = 600;
var height = 600;
var zoom = d3.behavior.zoom()
.scaleExtent([0.5, 10])
.on("zoom", zoomed);
var svg = d3.select('body').append('svg')
.attr('class', 'chart')
.attr('width', width)
.attr('height', height)
.append('g')
.attr("transform", "translate(" + margin.left + "," + margin.right + ")")
.call(zoom);
rect = svg.append("rect")
.attr("width", width)
.attr("height", height)
.style("fill", "none")
.style("pointer-events", "all");
svg.append("g")
.append("rect")
.attr("class", "rectangle")
.attr('width', width)
.attr('height', height);
function zoomed() {
console.log('1: ', zoom.translate());
if (zoom.translate()[0] > 0) {
//zoom.translate([0, zoom.translate()[1]]);
//console.log('2: ', zoom.translate());
}
svg.attr("transform", "translate(" + zoom.translate() + ")scale(" + zoom.scale() + ")");
}
In other words, I don't want to see the background behind the red rectangle.
For this I'm checking whether the zoom.translate()[0] is bigger than 0 and manually setting it to 0.
This works fine, but the problem is that when I drag to the left again the zoom.translate() x value is still the original one, not the one that I manually set (unless I release the mouse click before dragging to the left).
Does anyone know how I make the zoom.translate value persist?
Thanks.

Related

d3.event.pageX & d3.mouse(this)[0]

I tried to figure out the difference between 'd3.event.pageX' & 'd3.mouse(this)[0]'.
I guessed both are same but,
when I console.log both,
the value was different by '8' in my code.
var height=600;
var width=600;
var graphgap=60;
d3.csv('./details.csv').then(function(data){
var svg =d3.select('section').append('svg')
.attr('width',600).attr('height',600)
.on('mousemove',mousemove)
drawrect(data);
})
function drawrect(data){
let bars=d3.select('svg').selectAll('rect').data(data);
bars.enter().append('rect').classed('bargraph',true)
.attr('x',function(d,i){return (i+1)*graphgap})
.attr('y',function(d){return height-(d.Age)*5})
.attr('width',55)
.attr('height',function(d){return (d.Age)*(5)})
}
function mousemove(){
let mouselocation =[];
d3.select('svg').append('text')
.text(d3.event.pageX)
.attr('x',d3.event.pageX)
.attr('y',d3.event.pageY)
console.log(d3.event.pageX)
console.log(d3.mouse(this)[0])
}
So, I think these two are two different things.
Can anyone let me know why it makes a difference?
The reason why I tried to figure this out is because I was re-writing the code below.
<script>
// set the dimensions and margins of the graph
var margin = {top: 10, right: 30, bottom: 30, left: 60},
width = 460 - margin.left - margin.right,
height = 400 - margin.top - margin.bottom;
// append the svg object to the body of the page
var svg = d3.select("#my_dataviz")
.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 + ")");
//Read the data
d3.csv("https://raw.githubusercontent.com/holtzy/D3-graph-gallery/master/DATA/data_IC.csv",function(data) {
// Add X axis --> it is a date format
var x = d3.scaleLinear()
.domain([1,100])
.range([ 0, width ]);
svg.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
// Add Y axis
var y = d3.scaleLinear()
.domain([0, 13])
.range([ height, 0 ]);
svg.append("g")
.call(d3.axisLeft(y));
// This allows to find the closest X index of the mouse:
var bisect = d3.bisector(function(d) { return d.x; }).left;
// Create the circle that travels along the curve of chart
var focus = svg
.append('g')
.append('circle')
.style("fill", "none")
.attr("stroke", "black")
.attr('r', 8.5)
.style("opacity", 0)
// Create the text that travels along the curve of chart
var focusText = svg
.append('g')
.append('text')
.style("opacity", 0)
.attr("text-anchor", "left")
.attr("alignment-baseline", "middle")
// Create a rect on top of the svg area: this rectangle recovers mouse position
svg
.append('rect')
.style("fill", "none")
.style("pointer-events", "all")
.attr('width', width)
.attr('height', height)
.on('mouseover', mouseover)
.on('mousemove', mousemove)
.on('mouseout', mouseout);
// Add the line
svg
.append("path")
.datum(data)
.attr("fill", "none")
.attr("stroke", "steelblue")
.attr("stroke-width", 1.5)
.attr("d", d3.line()
.x(function(d) { return x(d.x) })
.y(function(d) { return y(d.y) })
)
// What happens when the mouse move -> show the annotations at the right positions.
function mouseover() {
focus.style("opacity", 1)
focusText.style("opacity",1)
}
function mousemove() {
// recover coordinate we need
var x0 = x.invert(d3.mouse(this)[0]);
var i = bisect(data, x0, 1);
selectedData = data[i]
focus
.attr("cx", x(selectedData.x))
.attr("cy", y(selectedData.y))
focusText
.html("x:" + selectedData.x + " - " + "y:" + selectedData.y)
.attr("x", x(selectedData.x)+15)
.attr("y", y(selectedData.y))
}
function mouseout() {
focus.style("opacity", 0)
focusText.style("opacity", 0)
}
})
</script>
In documentation is written:
While you can use the native event.pageX and event.pageY, it is often
more convenient to transform the event position to the local
coordinate system of the container that received the event using
d3.mouse, d3.touch or d3.touches.
d3.event
d3.mouse - uses local coordinate (without margin (60px))
d3.event.pageX - uses global coordinate (with margin (60px))
But local cordinate start on 68px. I guess 8 pixels is used to describe the y-axis.

D3 Transform Rescale X jumps to right

I'm creating a timeline line chart showing 5 or 6 different lines and want to be able to zoom in and scroll (once zoomed). I used some examples that use area charts but for some reason my line chart jumps to the right when I zoom the first time and I lose some of the data off to the right (I can no longer scroll to see it or see it when zoomed fully out). Also the lines appear over the y axis when I zoom or scroll.
I've copied it to JSFiddle (see here) with a dataset from 1 of the lines in my chart. Why is the line jumping to the right as soon as you use the zoom function? How can I stop the line from appearing over the y-axis?
Here is the JS of my version if you'd prefer to read it here:
function drawTimeline() {
var margin = {top: 10, right: 0, bottom: 50, left: 60}
var width = d3.select('#timeline').node().getBoundingClientRect().width/3*2;
var height = 300;
var svg = d3.select("#timeline").append("svg")
.attr("width", width)
.attr("height", height+margin.top+margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
.attr("width", width - margin.left - margin.right)
.call(d3.zoom()
// .extent()
.scaleExtent([1, 10])
.translateExtent([[0, -Infinity], [width - margin.left - margin.right, Infinity]])
.on("zoom", zoom)
);
var view = svg.append("rect")
.attr("class", "view")
.attr("width", width - margin.left - margin.right)
.attr("height", height)
.attr("fill", "white");
var x = d3.scaleTime()
.domain([
d3.min(poll_data[0].avgpolls, function(p) { return p.date; }),
d3.max(poll_data[0].avgpolls, function(p) { return p.date; })
])
.range([0, width - margin.left - margin.right]);
var y = d3.scaleLinear()
.domain([50, 0])
.range([0, height]);
var xAxis = d3.axisBottom(x);
var yAxis = d3.axisLeft(y);
var gX = svg.append("g")
.attr("class", "axis xaxis")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
var gY = svg.append("g")
.attr("class", "axis yaxis")
.call(yAxis);
//All lines are drawn in the same way (x and y points)
var line = d3.line()
.x(function(d) { return x(d.date); })
.y(function(d) { return y(d.poll); });
//selectAll allows us to create and manipulate multiple groups at once
var party = svg.selectAll(".party")
.data(poll_data)
.enter()
.append("g")
.attr("class", "party");
//Add path to every country group at once
var pollPaths = party.append("path")
.attr("class", "line")
.attr("d", function(d) { return line(d.avgpolls); })
.attr("fill", "none")
.attr("stroke-width", 2)
.style("stroke", function(d) { return returnPartyColour(d.party); });
function zoom() {
console.log("zooming: " + d3.event.transform);
var new_x = d3.event.transform.rescaleX(x);
gX.call(d3.axisBottom(new_x));
//view.attr("transform", d3.event.transform);
//Redraw lines
//pollPaths.select(".line").attr("d", function(d) { return line(d.avgpolls); });
var newline = d3.line()
.x(function(d) { return new_x(d.date); })
.y(function(d) { return y(d.poll); });
pollPaths.attr("d", function(d) { return line(d.avgpolls); });
}
}
The data is formatted like this in my version but not the JSFiddle:
poll_data = [
{
'party' : 'Party 1',
'avgpolls' : [
{'date' : new Date(year, month, day), 'poll' : 0, },
],
]
Thanks
Two things required to fix these issues.
The reason the line was jumping on zoom was because the zoom extent was not set. This was set and the value of translateExtent updated:
d3.zoom()
.scaleExtent([1, 10])
.translateExtent([[0, 0], [width - margin.left - margin.right, Infinity]])
.extent([[0, 0], [width - margin.left - margin.right, height]])
.on("zoom", zoom)
To prevent the paths from overflowing a clipping path is required. After creating the svg, before other elements are added, I added a clip-path as follows:
svg.append("defs").append("clipPath")
.attr("id", "clip")
.append("rect")
//Same dimensions as the area for the lines to appear
.attr("width", width - margin.left - margin.right)
.attr("height", height);
Then this had to be added to each path.
var pollPaths = party.append("path")
...
.attr("clip-path", "url(#clip)");
The original JSFiddle has been updated to reflect these changes.

d3js v4: independent multiple axes scaling and panning

I'm trying to implement a chart framework which is able to create line and area charts with multiple axes for each orientation, i.e 2-y-axis left, 1-yaxis right and 1-x-axis bottom.
That I got to work. My next task would be to implement zoom behaviour to the chart. I indented to have a global zoom behaviour, which is trigged if the user uses his mouse within the plot area. The displayed series would get rescaled and it would be possible to pan the plot. This one I got to work too.
In addition I wanted an independent zoom/scaling for each axis. I got the scaling, but I still have problems with the global zooming and panning. If I scale one axis, the associated series in the plot area gets rescaled but the panning does not work. And after the independent scaling of an axis, if I use the global rescale, the scaling gets reset and then gets scaled by the global zoom behaviour.
On the d3.js page I found an simple example for independent and global scaling and panning, but written with d3v3 .
I changed the example in such a way, so that it displays my problem jsfiddle demo. Use you mouse on the axes and in the plot area.
<!DOCTYPE html>
<html>
<head>
<title>TODO supply a title</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Simple Independent Axis Zooms on x, y, or xy</title>
<script src="//d3js.org/d3.v4.min.js"></script>
<style>
.axis path, .axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
</style>
</head>
<body>
<div id="chart"></div>
<script>
var data = [];
for (var i = 0; i < 100; i++) {
data.push([Math.random(), Math.random()]);
}
var svg = d3.select('#chart')
.append("svg")
.attr("width", window.innerWidth).attr("height", window.innerHeight);
function example(svg, data) {
var svg;
var margin = {
top: 60,
bottom: 80,
left: 60,
right: 0
};
var width = 500;
var height = 400;
var xaxis = d3.axisBottom();
var yaxis = d3.axisLeft();
var xscale = d3.scaleLinear();
var yscale = d3.scaleLinear();
var xcopyScale, ycopyScale;
var xyzoom, xzoom, yzoom;
updateZooms();
function update() {
var gs = svg.select("g.scatter");
var circle = gs.selectAll("circle")
.data(data);
circle.enter().append("svg:circle")
.attr("class", "points")
.style("fill", "steelblue")
.attr("cx", function (d) {
return X(d);
})
.attr("cy", function (d) {
return Y(d);
})
.attr("r", 4);
circle.attr("cx", function (d) {
return X(d);
})
.attr("cy", function (d) {
return Y(d);
});
circle.exit().remove();
}
function updateZooms() {
xyzoom = d3.zoom()
.on("zoom", function () {
xaxis.scale(d3.event.transform.rescaleX(xscale));
yaxis.scale(d3.event.transform.rescaleY(yscale));
draw();
});
xzoom = d3.zoom()
.on("zoom", function () {
xaxis.scale(d3.event.transform.rescaleX(xscale));
draw();
});
yzoom = d3.zoom()
.on("zoom", function () {
yaxis.scale(d3.event.transform.rescaleY(yscale));
draw();
});
}
function draw() {
svg.select('g.x.axis').call(xaxis);
svg.select('g.y.axis').call(yaxis);
update();
// After every draw, we reinitialize zoom. After every zoom, we reexecute draw, which will reinitialize zoom.
// This is how we can apply multiple independent zoom behaviors to the scales.
// (Note that the zoom behaviors will always end up with zoom at around 1.0, and translate at around [0,0])
svg.select('rect.zoom.xy.box').call(xyzoom);
svg.select('rect.zoom.x.box').call(xzoom);
svg.select('rect.zoom.y.box').call(yzoom);
}
// X value to scale
function X(d) {
return xaxis.scale() !== undefined && xaxis.scale() !== null
? xaxis.scale()(d[0])
: xscale(d[0]);
}
// Y value to scale
function Y(d) {
return yaxis.scale() !== undefined && yaxis.scale() !== null
? yaxis.scale()(d[1])
: yscale(d[1]);
}
var g = svg.append('g')
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
g.append("defs").append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("width", width - margin.left - margin.right)
.attr("height", height - margin.top - margin.bottom);
g.append("svg:rect")
.attr("class", "border")
.attr("width", width - margin.left - margin.right)
.attr("height", height - margin.top - margin.bottom)
.style("stroke", "black")
.style("fill", "none");
g.append("g").attr("class", "x axis")
.attr("transform", "translate(" + 0 + "," + (height - margin.top - margin.bottom) + ")");
g.append("g").attr("class", "y axis");
g.append("g")
.attr("class", "scatter")
.attr("clip-path", "url(#clip)");
g
.append("svg:rect")
.attr("class", "zoom xy box")
.attr("width", width - margin.left - margin.right)
.attr("height", height - margin.top - margin.bottom)
.style("visibility", "hidden")
.attr("pointer-events", "all")
.call(xyzoom);
g
.append("svg:rect")
.attr("class", "zoom x box")
.attr("width", width - margin.left - margin.right)
.attr("height", height - margin.top - margin.bottom)
.attr("transform", "translate(" + 0 + "," + (height - margin.top - margin.bottom) + ")")
.style("visibility", "hidden")
.attr("pointer-events", "all")
.call(xzoom);
g
.append("svg:rect")
.attr("class", "zoom y box")
.attr("width", margin.left)
.attr("height", height - margin.top - margin.bottom)
.attr("transform", "translate(" + -margin.left + "," + 0 + ")")
.style("visibility", "hidden")
.attr("pointer-events", "all")
.call(yzoom);
// Update the x-axis
xscale.domain(d3.extent(data, function (d) {
return d[0];
})).range([0, width - margin.left - margin.right]);
xaxis.scale(xscale)
.tickPadding(10);
svg.select('g.x.axis').call(xaxis);
// Update the y-scale.
yscale.domain(d3.extent(data, function (d) {
return d[1];
})).range([height - margin.top - margin.bottom, 0]);
yaxis.scale(yscale)
.tickPadding(10);
svg.select('g.y.axis').call(yaxis);
draw();
}
var exampleChart = example(svg, data);
</script>
</body>
</html>
To put it briefly: How can I solve my problem by using d3v4 to create a chart with multiple axes that has global and independent scaling and panning behaviour ?
as of now the current release of d3v4 doesn't natively support multiple independent zoom behaviours.
A possible solution would be to reset the internal transform state stored inside the selection on which you called the appropriate zoom behaviour.
There is an already open issue on the argument and i encourage you to go and read it and offer your input as well.
Best of luck!
There seems to be no correct solution (see https://github.com/d3/d3-zoom/issues/48).
But if you clear the scale after each zoom it seems to work
(see https://jsfiddle.net/DaWa/dLmp8zk8/2/).

Get size of svg g of element

As you can see here element g http://imgur.com/SZImQNB with different browser zoom levels. I would like to get those values 402x398 and 1608x1582 or whatever the values would be while zooming.
var container - is the svg g that I am looking for dimensions of
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
svg.append('rect')
.classed('bg', true)
.attr('stroke', 'transparent')
.attr('fill', 'transparent')
.attr("x", 0)
.attr("y", 0)
.attr('width', w)
.attr('height', h)
.call(d3.behavior.zoom()
.on("zoom", function() {
container.attr("transform", "translate(" + d3.event.translate +
")scale(" + d3.event.scale + ")");
}));
var container = svg.append("g")
.classed("node-area", true)
.attr("id", "test");
I'm using container.node().getBBox().width to get those values and I always receive 70 px width 30 px height no matter what.
console.log("width: "+container.node().getBBox().width+" height: "+container.node().getBBox().height+ "x: "+container.node().getBBox().x+" y: "+container.node().getBBox().y);
I am following d3.js force layout auto zoom/scale after loading TWiStErRob answer
#RobertLongson suggestion does work. In the code, getBoundingClientRect().width has to be used after the transform:
function zoomed() {
container.attr("transform", "translate(" + d3.event.translate
+ ")scale(" + d3.event.scale + ")");
console.log("width: "+container.node().getBoundingClientRect().width);
};
Check the console in this fiddle, while you apply zoom: http://jsfiddle.net/gerardofurtado/tr4kx4af/

D3.js transition starts at height 0, needs to start at previous value

I have a bar chart displaying data on which you can filter through different years with the press of a button. I want the chart to transition from the current value to the new value, but now it starts at the bottom each time you press a button. How can I fix this?
Thanks!
var margin = {top: 20, right: 20, bottom: 30, left: 20},
height = 500 - margin.top - margin.bottom,
width = 600 - margin.left - margin.right
function bars(data) {
max = d3.max(data)
var yScale = d3.scale.linear()
.domain([0, d3.max(data)])
.range([0, height])
var xScale = d3.scale.ordinal()
.domain(d3.range(0, data.length))
.rangeBands([0, width], .2)
var myChart = d3.select("#chart")
var bars = myChart.selectAll("rect.bar")
.data(data)
//enter
bars.enter()
.append("svg:rect")
.attr("class", "bar")
.attr("fill", "#800")
//apply to everything (enter and update)
bars.style('fill', '#C64567')
.attr('width', xScale.rangeBand())
.attr('x', function(d,i){
return xScale(i);
})
.attr('height', 0)
.attr('y', height)
bars.transition()
.attr('height', function(d){
return yScale(d);
})
.attr('y', function(d){
return height - yScale(d);
})
.duration(1000)
.ease('elastic')
}
function init() {
//setup the svg
var svg = d3.select("#svg")
.style('background', '#000')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.append("svg:g")
.attr("id", "chart")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
//UI
d3.select("#button1")
.on("click", function (d, i) {
bars(j1996);
})
d3.select("#button2")
.on("click", function (d, i) {
bars(j1997);
})
d3.select("#button3")
.on("click", function (d, i) {
bars(j1998);
})
//draw the bars
bars(j1996);
}
A fresh new look at it and I managed to find the solution. I removed the tag bars in this line:
//apply to everything (enter and update)
bars.style('fill', '#C64567')
So all the attributes are now set on the bar.enter() command, this way d3.js makes the transitions automatically from the last value

Categories