(To observe bug, make sure to open the block in its own window).
A pretty barebones, mostly unstyled block, used to demonstrate the zoom bug that occurs upon resize (as described here).
This chart is a timeline-style chart, with a time (continuous) scale for the x-axis and a band scale for the y-axis. The chart always fills the width of the window. The bug behaviour is described below:
We have a chart with a d3.scaleTime as the x-axis, and a d3.scaleBand as the y-axis. We only zoom and pan on the x-axis. When we zoom or pan the chart, there is a 1:1 movement with the mouse cursor. When the window is resized horizontally, the x-axis range is recalculated, so that the chart will shrink or expand to fill the space - however, this results in the zoom and panning having an offset (the mouse moves a longer or shorter distance than the chart, and the zoom will focus in on a point to the left or right of the mouse cursor).
The zoom should stay 1:1 with the mouse (move the same distance, zoom in on the mouse position) after window resize.
Built with blockbuilder.org
xxxxxxxxxx
<head>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v4.min.js"></script>
<style>
/* CSS to keep the chart filling the window */
#chart {
position: fixed;
left: 0px;
right: 0px;
top: 0px;
bottom: 0px;
}
</style>
</head>
<body>
<div id="chart"></div>
<script>
// The div which contains the chart, and which fits the full window
let chartDiv = document.getElementById("chart");
// The undefined variables below are set in calculateWidthsAndHeights()
// The width of the whole svg element created for the chart
let fullWidth = undefined;
// The height of the whole svg element created for the chart
let fullHeight = undefined;
// The height of only the section of the svg which contains the data
let dataHeight = undefined;
// The space in which to display x-axis labels
let xAxisLabelHeight = 25;
// The data to use for the chart
let chartLayers = [
{
identifier: 'layer1',
dates: [
'2010-02-01',
'2010-06-01'
]
},
{
identifier: 'layer2',
dates: [
'2010-07-01',
'2011-02-01'
]
}
];
// Set the undefined width and height variables
calculateWidthsAndHeights();
// --- D3 component definitions below ---
let xScale = d3.scaleTime()
.range([0, fullWidth])
.domain([
new Date('2010-01-01'),
new Date('2011-01-01'),
]);
let xScale2 = d3.scaleTime() // Used in the zoom() function
.range([0, fullWidth])
.domain([
new Date('2010-01-01'),
new Date('2011-01-01'),
]);
let xAxis = d3.axisBottom(xScale);
let yScale = d3.scaleBand()
.range([0, dataHeight])
.domain(chartLayers.map((layer) => {
return layer.identifier;
}));
let yAxis = d3.axisRight(yScale);
let zoomBehavior = d3.zoom()
.on('zoom', () => {
zoom();
});
let timeline = d3.select('#chart')
.append('svg')
.attr('width', fullWidth + 1)
.attr('height', fullHeight)
.call(zoomBehavior);
let timelineXAxisGroup = timeline.append('g')
.attr('transform', 'translate(0, ' + dataHeight +
')')
.call(xAxis);
let timelineYAxisGroup = timeline.append('g')
.call(yAxis);
let timelineData = timeline.append('g');
// --- D3 svg drawing below ---
timelineData.selectAll('.chart-layer')
.remove().exit()
.data(chartLayers)
.enter().append('g')
.attr('transform', (layer) => {
return 'translate(0, ' +
yScale(layer.identifier) +
')';
})
.append('rect')
.attr('class', 'chart-layer-bar')
.attr('x', (layer) => {
return xScale(new Date(layer.dates[0]).getTime());
})
.attr('y', 0)
.attr('height', 10)
.attr('width', (layer) => {
let startDateXPosition = xScale(new Date(layer.dates[0]).getTime());
let endDateXPosition = xScale(new Date(layer.dates[1]).getTime());
return endDateXPosition - startDateXPosition;
});
timelineYAxisGroup
.call(yAxis);
// --- Function definitions below ---
/**
* Called when the chart is zoomed.
*/
function zoom() {
xScale.domain(d3.event.transform.rescaleX(xScale2).domain());
timelineXAxisGroup.call(xAxis);
timelineData.selectAll('.chart-layer-bar')
.attr('x', (layer) => {
let startDate = layer.dates[0];
return xScale(new Date(startDate).getTime());
})
.attr('width', (layer) => {
let startDate = layer.dates[0];
let endDate = layer.dates[1];
return xScale(new Date(endDate).getTime()) - xScale(new Date(startDate).getTime());
});
}
/**
* Called when the chart div (window) is resized.
*/
function resize() {
calculateWidthsAndHeights();
timeline.attr('width', fullWidth + 1);
xScale.range([0, fullWidth]);
timelineXAxisGroup.call(xAxis);
// Adjust the positioning of the layer bars
timelineData.selectAll('.chart-layer-bar')
.attr('x', (layer) => {
let startDate = layer.dates[0];
return xScale(new Date(startDate).getTime());
})
.attr('width', (layer) => {
let startDate = layer.dates[0];
let endDate = layer.dates[1];
return xScale(new Date(endDate).getTime()) - xScale(new Date(startDate).getTime());
});
}
/**
* Recalculates the width and height variables.
*/
function calculateWidthsAndHeights(){
fullWidth = chartDiv.clientWidth;
dataHeight = 2 * 10; // 2 layers, 10 px high each
fullHeight = dataHeight + xAxisLabelHeight;
}
// Set the resize event listener
window.addEventListener('resize', () => {
resize();
});
</script>
</body>
https://d3js.org/d3.v4.min.js