Reputation: 11
I am working with D3 v4 and JS. I have a scatter plot with a predefined set of data loaded along with axes with the ability to pan and zoom. I need to be able to then dynamically add points and eventually output them in data space not pixel space. I am using the "rescaleX" and "rescaleY" methods of the zoom object. They work fine for rescaling the axes but, when I try to add new points, the location of the plotted point does correspond to the mouse location. Here is a simplified version of the code:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v4.min.js"></script>
</head>
<body>
<script>
var data = [{x:17,y:3},
{x:20,y:16},
{x:2,y:13},
{x:19,y:10},
{x:13,y:15},
{x:2,y:2},
{x:5,y:8},
{x:11,y:19},
{x:20,y:12},
{x:10,y:20}];
var width = 600;
var height = 600;
var padding = 50;
var newXscale, newYscale;
var dataScale = d3.scaleLinear()
.domain([0,21])
.range([0, width]);
var svg = d3.select('body').append('svg')
.attr('width', width+2*padding)
.attr('height', height+2*padding)
.on('click', clicked);
var xAxis = d3.axisTop()
.scale(dataScale);
var gX = svg.append('g')
.attr('transform','translate(50,50)')
.call(xAxis);
var yAxis = d3.axisLeft()
.scale(dataScale);
var gY = svg.append('g')
.attr('transform','translate(50,50)')
.call(yAxis);
var canvas = svg.append('g')
var points = canvas.append('g');
points.selectAll('circle').data(data)
.enter().append('circle')
.attr('cx', function(d) {return dataScale(d.x)+padding})
.attr('cy', function(d) {return dataScale(d.y)+padding})
.attr('r', 5);
var zoom
var zoomOn = false;
window.addEventListener('keydown', function (event) {
if (event.key=='z') {
if (zoomOn) {
d3.select('#zoomBox').remove();
zoomOn = false;
} else {
zoom = d3.zoom()
.scaleExtent([0.1, 10])
.on('zoom', zoomed);
svg.append("rect")
.attr('cursor','move')
.attr("width", width+padding*2)
.attr("height", height+padding*2)
.attr('id','zoomBox')
.style("fill", "none")
.style("pointer-events", "all")
.call(zoom);
zoomOn = true;
}
}
});
function zoomed() {
canvas.attr("transform", d3.event.transform)
newXscale = d3.event.transform.rescaleX(dataScale);
newYscale = d3.event.transform.rescaleY(dataScale);
gX.call(xAxis.scale(newXscale));
gY.call(yAxis.scale(newYscale));
}
function clicked() {
var coords = d3.mouse(this);
points.append('circle')
.attr('cx',coords[0])
.attr('cy',coords[1])
.attr('r',5);
var x = newXscale.invert(coords[0]-padding);
var y = newYscale.invert(coords[1]-padding);
console.log(x+' '+y);
}
</script>
</body>
</html>
Upvotes: 1
Views: 892
Reputation: 11
I figured it out. The problem lied in the fact that I was removing the zoom box when toggling the zoom. I switched the event listener to just hide the box and unbind the pointer-events. Here is the final code:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v4.min.js"></script>
</head>
<body>
<script>
var data = [{x:17,y:3},
{x:20,y:16},
{x:2,y:13},
{x:19,y:10},
{x:13,y:15},
{x:2,y:2},
{x:5,y:8},
{x:11,y:19},
{x:20,y:12},
{x:10,y:20}];
var width = 600;
var height = 600;
var padding = 50;
var newXscale, newYscale;
var zoomOn = false;
var xScale = d3.scaleLinear()
.domain([0,21])
.range([0, width]);
var yScale = d3.scaleLinear()
.domain([0,21])
.range([0, width]);
var svg = d3.select('body').append('svg')
.attr('width', width+2*padding)
.attr('height', height+2*padding)
.on('click', clicked)
.attr('cursor','crosshair');
var xAxis = d3.axisTop()
.scale(xScale);
var gX = svg.append('g')
.attr('transform','translate(50,50)')
.call(xAxis);
var yAxis = d3.axisLeft()
.scale(yScale);
var gY = svg.append('g')
.attr('transform','translate(50,50)')
.call(yAxis);
var canvas = svg.append('g')
var points = canvas.append('g');
points.selectAll('circle').data(data)
.enter().append('circle')
.attr('cx', function(d) {return xScale(d.x)+padding})
.attr('cy', function(d) {return yScale(d.y)+padding})
.attr('r', 5);
var zoom = d3.zoom()
.scaleExtent([0.1, 10])
.on('zoom', zoomed);
var zoombox = svg.append("rect")
.attr("width", width+padding*2)
.attr("height", height+padding*2)
.attr('id','zoomBox')
.style("fill", "none")
.style("pointer-events", "none")
.style('visibility','off')
.call(zoom);
window.addEventListener('keydown', function (event) {
if (event.key=='z') {
if (zoomOn) {
d3.select('#zoomBox')
.attr('cursor','auto')
.style('pointer-events','none')
.style('visibility','off');
zoomOn = false;
} else {
d3.select('#zoomBox')
.attr('cursor','move')
.style('pointer-events','all')
.style('visibilty','on')
zoomOn = true;
}
}
});
function zoomed() {
canvas.attr("transform", d3.event.transform)
newXscale = d3.event.transform.rescaleX(xScale);
newYscale = d3.event.transform.rescaleY(yScale);
gX.call(xAxis.scale(newXscale));
gY.call(yAxis.scale(newYscale));
newZscale = d3.event.transform.k;
}
function clicked() {
var coords = d3.mouse(this);
if (newXscale && newYscale) {
var x = newXscale.invert(coords[0] - padding);
var y = newYscale.invert(coords[1] - padding);
};
console.log(newZscale);
points.append('circle')
.attr('cx', (!x) ? coords[0] : xScale(x) + (padding / newZscale))
.attr('cy', (!y) ? coords[1] : yScale(y) + (padding / newZscale))
.attr('r', 5);
console.log(x + ' ' + y);
}
</script>
</body>
</html>
Upvotes: 0
Reputation: 102174
Create a variable to store the zoom level:
newZscale = d3.event.transform.k;
And, in your clicked
function, use the dateScale
to plot the new circles, dividing the padding by the zoom level:
function clicked() {
var coords = d3.mouse(this);
if (newXscale && newYscale) {
var x = newXscale.invert(coords[0] - padding);
var y = newYscale.invert(coords[1] - padding);
};
console.log(newZscale);
points.append('circle')
.attr('cx', (!x) ? coords[0] : dataScale(x) + (padding / newZscale))
.attr('cy', (!y) ? coords[1] : dataScale(y) + (padding / newZscale))
.attr('r', 5);
console.log(x + ' ' + y);
}
Here is the demo:
var data = [{
x: 17,
y: 3
}, {
x: 20,
y: 16
}, {
x: 2,
y: 13
}, {
x: 19,
y: 10
}, {
x: 13,
y: 15
}, {
x: 2,
y: 2
}, {
x: 5,
y: 8
}, {
x: 11,
y: 19
}, {
x: 20,
y: 12
}, {
x: 10,
y: 20
}];
var width = 600;
var height = 600;
var padding = 50;
var newXscale, newYscale, newZscale;
var dataScale = d3.scaleLinear()
.domain([0, 21])
.range([0, width]);
var svg = d3.select('body').append('svg')
.attr('width', width + 2 * padding)
.attr('height', height + 2 * padding)
.on('click', clicked);
var xAxis = d3.axisTop()
.scale(dataScale);
var gX = svg.append('g')
.attr('transform', 'translate(50,50)')
.call(xAxis);
var yAxis = d3.axisLeft()
.scale(dataScale);
var gY = svg.append('g')
.attr('transform', 'translate(50,50)')
.call(yAxis);
var canvas = svg.append('g')
var points = canvas.append('g');
points.selectAll('circle').data(data)
.enter().append('circle')
.attr('cx', function(d) {
return dataScale(d.x) + padding
})
.attr('cy', function(d) {
return dataScale(d.y) + padding
})
.attr('r', 5);
var zoom
var zoomOn = false;
window.addEventListener('keydown', function(event) {
if (event.key == 'z') {
if (zoomOn) {
d3.select('#zoomBox').remove();
zoomOn = false;
} else {
zoom = d3.zoom()
.scaleExtent([0.1, 10])
.on('zoom', zoomed);
svg.append("rect")
.attr('cursor', 'move')
.attr("width", width + padding * 2)
.attr("height", height + padding * 2)
.attr('id', 'zoomBox')
.style("fill", "none")
.style("pointer-events", "all")
.call(zoom);
zoomOn = true;
}
}
});
function zoomed() {
canvas.attr("transform", d3.event.transform)
newXscale = d3.event.transform.rescaleX(dataScale);
newYscale = d3.event.transform.rescaleY(dataScale);
newZscale = d3.event.transform.k;
gX.call(xAxis.scale(newXscale));
gY.call(yAxis.scale(newYscale));
}
function clicked() {
var coords = d3.mouse(this);
if (newXscale && newYscale) {
var x = newXscale.invert(coords[0] - padding);
var y = newYscale.invert(coords[1] - padding);
};
points.append('circle')
.attr('cx', (!x) ? coords[0] : dataScale(x) + (padding / newZscale))
.attr('cy', (!y) ? coords[1] : dataScale(y) + (padding / newZscale))
.attr('r', 5);
}
<script src="https://d3js.org/d3.v4.min.js"></script>
Upvotes: 1