The following code is an example of a chart that combines manual zooming triggered by drag/wheel/click and automatic zooming executed by the function “zoomToArea”. The current behavior is quite satisfactory, but the zoom state update remains independent for each zoom source. I’m trying to store the zoom.transform state in a “zoomTransform” variable, but I haven’t managed to override the state to avoid resetting the zoom when resuming the wheel interaction.
My question: How can I ensure that the current zoom position is uniquely stored for both triggers?
If you have any suggestions to fix it, I’d love to hear them!
https://jsfiddle.net/MG31470/Lcxyzaek/
|
https://codepen.io/MG31/pen/pvzRwQq
<div>
<button id="zoom_to_home">Reset Zoom</button>
<button id="zoom1">Zoom1</button>
<button id="zoom2">Zoom2</button>
<button id="zoom3">Zoom3</button>
<button id="zoom4">Zoom4</button>
</div>
<div>
<button id="small_data_btn">Load small data</button>
<button id="medium_data_btn">Load medium data</button>
<button id="large_data_btn">Load large data</button>
</div>
<div id="chart_canvas_ctn"></div>
// DIMENSIONS
const outerWidth = 500
const outerHeight = 500
const margin = { top: 50, right: 50, bottom: 50, left: 50 }
const width = outerWidth - margin.left - margin.right
const height = outerHeight - margin.top - margin.bottom
// CREATE SVG CTN
var SVG = d3
.select("#chart_canvas_ctn")
.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 + ")")
// CLIP PATH
var clipPath = SVG.append("defs")
.append("SVG:clipPath")
.attr("id", "clip")
.append("SVG:rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", width)
.attr("height", height)
// SCALES
var x = d3.scaleLinear().domain([0, 400]).range([0, width])
var y = d3.scaleLinear().domain([0, 400]).range([height, 0])
// GRIDS
var xGrid = SVG.append("g")
xGrid
.selectAll(".x-line")
.attr("clip-path", "url(#clip)")
.data(d3.range(0, 400, 100))
.enter()
.append("line")
.attr("class", "grid")
.attr("x1", (d) => x(d))
.attr("x2", (d) => x(d))
.attr("y1", 0)
.attr("y2", height)
.attr("stroke", "#ccc")
.attr("stroke-width", 1)
var yGrid = SVG.append("g")
yGrid
.selectAll(".y-line")
.attr("clip-path", "url(#clip)")
.data(d3.range(0, 400, 100))
.enter()
.append("line")
.attr("class", "grid")
.attr("x1", 0)
.attr("x2", width)
.attr("y1", (d) => y(d))
.attr("y2", (d) => y(d))
.attr("stroke", "#ccc")
.attr("stroke-width", 1)
// AXIS
var xAxisBot = SVG.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x))
var yAxisLeft = SVG.append("g")
.attr("transform", "translate(0,0)")
.call(d3.axisLeft(y))
// ZOOM
// to store zoom last state before change
let zoomTransform = d3.zoomIdentity
var zoom = d3
.zoom()
.scaleExtent([1, 100])
.translateExtent([
[0, 0],
[width, height],
])
.extent([
[0, 0],
[width, height],
])
.interpolate(d3.interpolate)
.on("start", function (event) {})
.on("end", function (event) {})
.on("zoom", function (event) {
renderChart(event.transform)
})
SVG.append("rect")
.attr("width", width)
.attr("height", height)
.style("fill", "none")
.style("pointer-events", "all")
.call(zoom)
// INIT RECTANGLES
const size_rect = 1.26
var squares = SVG.append("g")
// RESIZE
function renderChart(transform) {
var prevTransform = zoomTransform
var zoomFactor = transform.k
// NEW SCALE
var newX = transform.rescaleX(x)
var newY = transform.rescaleY(y)
// AXIS
xAxisBot.call(d3.axisBottom(newX))
yAxisLeft.call(d3.axisLeft(newY))
// GRID
xGrid
.selectAll("line")
.attr("clip-path", "url(#clip)")
.attr("x1", (d) => transform.applyX(x(d)))
.attr("x2", (d) => transform.applyX(x(d)))
yGrid
.selectAll("line")
.attr("clip-path", "url(#clip)")
.attr("y1", (d) => transform.applyY(y(d)))
.attr("y2", (d) => transform.applyY(y(d)))
// ELEMENTS
squares
.selectAll("rect")
.attr("clip-path", "url(#clip)")
.attr("x", function (d) {
return newX(d.x) - (size_rect / 2) * zoomFactor
})
.attr("y", function (d) {
return newY(d.y) - (size_rect / 2) * zoomFactor
})
.attr("width", size_rect * zoomFactor)
.attr("height", size_rect * zoomFactor)
// -
zoomTransform = transform
}
// AUTO ZOOM
document.getElementById("zoom_to_home").addEventListener("click", function (e) {
SVG.transition()
.duration(2000)
.call(zoom.transform, d3.zoomIdentity)
.on("end", function () {
console.log("Zoom réinitialisé")
})
})
function zoomHomeInstant() {
SVG.call(zoom.transform, d3.zoomIdentity).on("end", function () {
console.log("Zoom réinitialisé")
})
}
document.getElementById("zoom1").addEventListener("click", function (e) {
zoomToArea(0, 0, 300, 300)
})
document.getElementById("zoom2").addEventListener("click", function (e) {
zoomToArea(75, 75, 125, 125)
})
document.getElementById("zoom3").addEventListener("click", function (e) {
zoomToArea(90, 190, 210, 310)
})
document.getElementById("zoom4").addEventListener("click", function (e) {
SVG.transition()
.duration(1000)
.call(zoom.scaleTo, 4)
.on("end", function () {
console.log("Zoom 4 OK")
})
})
function zoomToArea(x1, y1, x2, y2) {
console.log("=============")
zoomHomeInstant()
// Récupérer la transformation actuelle du zoom
var currTransform = d3.zoomTransform(SVG.node())
console.log("currTransform", currTransform)
var storedTransform = zoomTransform
console.log("storedTransform", storedTransform)
// Récupérer les dimensions visibles du SVG
var rectBBox = SVG.select("rect").node().getBBox()
var rectWidth = rectBBox.width
var rectHeight = rectBBox.height
console.log("rect dimensions:", { rectWidth, rectHeight })
var scaledX1 = x(x1)
var scaledY1 = y(y1)
var scaledX2 = x(x2)
var scaledY2 = y(y2)
var appliedX1 = currTransform.applyX(x(x1))
var appliedY1 = currTransform.applyY(y(y1))
var appliedX2 = currTransform.applyX(x(x2))
var appliedY2 = currTransform.applyY(y(y2))
console.log("raw", { x1: x1, y1: y1, x2: x2, y2: y2 })
console.log("scaled", {
x1: scaledX1,
y1: scaledY1,
x2: scaledX2,
y2: scaledY2,
})
console.log("applied", {
x1: appliedX1,
y1: appliedY1,
x2: appliedX2,
y2: appliedY2,
})
// Calculer les dimensions de la zone cible
var targetWidth = Math.abs(appliedX2 - appliedX1)
var targetHeight = Math.abs(appliedY2 - appliedY1)
console.log("Target area dimensions:", { targetWidth, targetHeight })
// Calculer le scale automatiquement (min pour conserver les proportions)
var scaleX = rectWidth / targetWidth
var scaleY = rectHeight / targetHeight
var targetScale = Math.min(scaleX, scaleY)
console.log("Calculated scale:", { scaleX, scaleY, targetScale })
// Calculer les coordonnées du centre de la zone cible
var centerX = (appliedX1 + appliedX2) / 2 - currTransform.x
var centerY = (appliedY1 + appliedY2) / 2 - currTransform.y
console.log("Center of target area:", { centerX, centerY })
// Appliquer la transformation
SVG.transition()
.duration(1000)
.ease(d3.easeQuad) // d3.easeCubicInOut ou d3.easeQuad, d3.easeCircle
.call(
zoom.transform,
currTransform
.translate(rectWidth / 2, rectHeight / 2) // Translation pour centrer
.scale(targetScale / currTransform.k) // Zoom calculé
.translate(-centerX, -centerY), // Recentrer sur la zone cible
)
}
// LOAD DATA
var data = []
document
.getElementById("small_data_btn")
.addEventListener("click", function (e) {
generateRandomData(500)
})
document
.getElementById("medium_data_btn")
.addEventListener("click", function (e) {
generateRandomData(5000)
})
document
.getElementById("large_data_btn")
.addEventListener("click", function (e) {
generateRandomData(25000)
})
function generateRandomData(count) {
data = []
for ($i = 0; $i < count; $i++) {
data.push({
id: generateRandomIntBetween(1000, 9999),
x: generateRandomIntBetween(x.domain()[0], x.domain()[1]),
y: generateRandomIntBetween(y.domain()[0], y.domain()[1]),
color: generateRandomColorHex(),
})
}
refreshData()
}
function refreshData() {
// console.log('data',data)
squares.selectAll("rect").remove()
squares
.selectAll("rect")
.data(data)
.enter()
.append("rect")
// .attr("x", function (d) { return x(d.x)-size_rect/2;})
// .attr("y", function (d) { return y(d.y)-size_rect/2;})
// .attr("width", size_rect)
// .attr("height", size_rect)
.attr("fill", function (d) {
return d.color
})
renderChart(zoomTransform)
}
function generateRandomIntBetween(a, b) {
if (a > b) [a, b] = [b, a]
a = Math.ceil(a)
b = Math.floor(b)
return Math.floor(Math.random() * (b - a + 1)) + a
}
function generateRandomColorHex() {
return `#${Math.floor(Math.random() * 16777215).toString(16)}`
}
Martin Gagne is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.
1