d3 zoom behavior when window is resized - javascript

I am using D3 to set up my chart area that makes use of the zoomable behavior. The chart includes both x and y axis.
The chart area should be responsive when window is resized. In this case, I need to reset the x and y axis domain and range on window resizing.
The issue happened with window resizing. I've noticed the zoom focus isn't lined up with the mouse anymore after the following steps:
First pan and zoom in the chart area
Then resize the window
Then pan and zoom the chart area again
After that, the above problem happened. See the following jsfiddle which has this issue.
So what's the right way of handing this? I've noticed a couple discussions about this issue such as:
d3 Preserve scale/translate after resetting range
I tried this approach but I couldn't make it working using D3 V4 API.

The main idea is the use the window.addEventListener to update the plot:
window.addEventListener('resize', () => {
// obtain the current transformation by calling the `d3.zoomTransform` function
const t0 = d3.zoomTransform(
chart.node());
// obtain the client width by evaluating the `div` (class: `zoom-chart`)
width = document.getElementById('zoom-chart').clientWidth;
height = 480;
// update the base scale
baseX.range([0, width])
.interpolate(d3.interpolateNumber);
baseY.range([height, 0])
.interpolate(d3.interpolateNumber);
// update the view port
vb = [0,0, width,height];
chart
.attr('viewBox', vb);
// replot with the original transform
chart.call(zoom.transform, t0);
});
I also added the whole solution here:
const data = [
{x:1 , y:1},
{x:9 , y:1},
{x:9 , y:9},
{x:1 , y:9},
];
var width = document.getElementById('zoom-chart').clientWidth;
var height = 480;
var baseX = d3.scaleLinear().domain([0,10])
.range([0, width])
.interpolate(d3.interpolateNumber);
var baseY = d3.scaleLinear().domain([0,10])
.range([height, 0])
.interpolate(d3.interpolateNumber);
var scaleX = baseX;
var scaleY = baseY;
const zoom = d3.zoom()
.on('zoom', () => {
const transform = d3.event.transform;
scaleX = transform.rescaleX(baseX)
.interpolate(d3.interpolateNumber)
scaleY = transform.rescaleY(baseY)
.interpolate(d3.interpolateNumber)
render();
});
const chart = d3.select('#zoom-chart')
.append('svg')
.attr('class', 'chart')
.call(zoom);
const rect = chart
.append('rect')
.attr('rx', 10).attr('ry', 10)
.attr('x', 10).attr('y', 10)
.attr('fill', '#00222b')
.attr('width', width-20)
.attr('height', height-20);
const plot = chart
.append('g')
.attr('class', 'plot-area');
function render() {
plot.selectAll('.data')
.data(data)
.join(
enter => enter.append('circle')
.attr('class', 'data')
.attr('r', 10)
.attr('stroke', '#004455')
.attr('stroke-width', 5),
update => update,
exit => exit.remove()
)
.attr('cx', d => scaleX(d.x))
.attr('cy', d => scaleY(d.y));
}
window.addEventListener('resize', () => {
const t0 = d3.zoomTransform(
chart.node());
width = document.getElementById('zoom-chart').clientWidth;
height = 480;
baseX.range([0, width])
.interpolate(d3.interpolateNumber);
baseY.range([height, 0])
.interpolate(d3.interpolateNumber);
vb = [0,0, width,height];
chart
.attr('viewBox', vb);
rect
.attr('width', width-20)
.attr('height', height-20);
chart.call(zoom.transform, t0);
});
let vb = [0,0, width,height];
chart
.attr('viewBox', vb);
chart.call(zoom.transform, d3.zoomIdentity);
<script src="https://unpkg.com/d3#5.15.0/dist/d3.min.js"></script>
<div id="zoom-chart" class="chart large">
</div>

Related

How to create Stacked Line Chart in d3js Multiple Y Axis and common X Axis

Example Linechart
How can i draw a linechart like this in d3js?
You can draw individual line charts which are translated vertically. Translating them vertically can be achieved by placing each chart into a SVG group (g) and translating the group by setting the transform attribute.
They all share the same x axis with the same domain and range.
const x = d3.scaleLinear()
.domain([d3.min(t), d3.max(t)])
.range([0, width]);
Here is the entire code:
// dummy data with common domain for all charts
const t = d3.range(200).map(o => o * 0.1 + 1);
// three series of data => three charts
const data = [
t.map(t => (1 + Math.cos(t))/2),
t.map(t => 1 - 1/Math.pow(0.2*t, 2)),
t.map(t => (1 + Math.sin(t))/2),
];
// three different colors for three graphs
const graphColor = ['#F2CD5C', '#a71df2', '#A61f69'];
// we need to add some margin to see the full axis
const margin = { top: 10, bottom: 75, left: 30, right: 10 };
// static width and height - change this to whatever suits you
const width = 300;
const height = 400;
// the height of each stacked
const singleChartHeight = height / data.length;
const svg = d3.select('#stacked-charts')
.append('svg')
// set the size of the chart
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.attr("viewBox", [0, 0, width, height]);
// create a common x-axis for all charts
const x = d3.scaleLinear()
.domain([d3.min(t), d3.max(t)])
.range([0, width]);
// a global group with padding
const coreGroup = svg.append('g')
.attr('class', 'stacked-charts')
// translate the padding
.attr('transform', 'translate(' + margin.left + ' 0)');
// create three different y-axes - one for each series
const y = data.map(dataSeries => {
return d3.scaleLinear()
.domain([d3.min(dataSeries), d3.max(dataSeries)])
.range([singleChartHeight, 0]);
});
// the line generator
const lineGenerator = d3.line();
// create a chart for each series of data
data.forEach((dataSeries, chartIndex) => {
// create an individual group
const chartGroup = coreGroup.append('g')
.attr('class', 'chart')
// stack by translating vertically
.attr('transform', 'translate(0 ' + (chartIndex * singleChartHeight) + ')');
// use x/y axes to create the points
const points = t.map((timestamp, timeStampIndex) => {
return [
// the x value
x(timestamp),
// the y value from one of the three axes
y[chartIndex](dataSeries[timeStampIndex])
]
});
// create the SVG path for the line
const path = lineGenerator(points);
// draw the graph
chartGroup.append('path')
.attr('class', 'graph')
.attr('d', path)
.attr('fill', 'none')
.attr('stroke', graphColor[chartIndex]);
// add x axis
chartGroup.append('g')
.attr('class', 'x-axis')
.call(d3.axisBottom(x))
.attr('transform', 'translate(0 '+ singleChartHeight + ')');
// add y axis
chartGroup.append('g')
.attr('class', 'y-axis')
.call(d3.axisLeft(y[chartIndex]));
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<div style="text-align: center;">
<h1>Stacked Linear Charts</h1>
<div id="stacked-charts"></div>
</div>
If you want all charts to have the same y-scale, they could also share the same y-axis.

How can I remove non-integer ticks while zooming in with D3.js?

Problem: I am trying to create a pannable/zoomable Linear X axis with ticks on each integer from 0 to a predefined large number (let's say 1000 but could be much higher). Obviously this will not all fit on the screen and be readable, so I need it to go offscreen and be pannable. Starting with 0 through 10 is acceptable. There are a number of problems with my solution so far, but the first being that if I zoom in, D3 automatically adds ticks in between the integers at decimal places to continue to have 10 ticks on the screen. The second being when I zoom out, the range will display beyond 1000.
A quick note: The data that will be displayed is irrelevant. The axis has to go from 0 to 1000 regardless if there is a datapoint on 1000 or not so I cannot determine the domain based on my dataset.
Solutions I've tried:
.ticks(). This lets me specify the number of ticks I want. However this appears to put all the ticks into the initial range specified. If I increase the width of the range past the svg size, it spreads out the ticks, but not in any controllable way (to my knowledge). Additionally I run into performance issues where the panning and zooming. I find this behavior strange since if I don't specify ticks, it makes an infinite number I can pan through without any lag. I assume the lag occurs because it's trying to render all 1000 ticks immediately and the default d3.js functionality renders them dynamically as you scroll. This seems like a potential solution, but I'm not sure how to execute it.
.tickValues(). I can provide an array of 0 through 1000 and this works but exhibits the exact same lag behavior as .ticks(). This also doesn't dynamically combine ticks into 10s or 100s as I zoom out.
.tickFormat(). I can run a function through so that any non-integer number is converted to an empty string. However, this still leaves the tick line.
Here are the relevant parts of my code using the latest version of D3.js(7.3):
const height = 500
const width = 1200
const margin = {
top: 20,
right: 20,
bottom: 20,
left: 35
}
const innerWidth = width - margin.left - margin.right
const innerHeight = height - margin.top - margin.bottom
const xScale = d3.scaleLinear()
.domain([0, 10])
.range([0, innerWidth])
const svg = d3.select('svg')
.attr("width", width)
.attr("height", height)
svg.append('defs').append('clipPath')
.attr('id', 'clip')
.append('rect')
.attr('width', innerWidth)
.attr('height', innerHeight)
const zoom = d3.zoom()
.scaleExtent([1, 8])
.translateExtent([
[0, 0],
[xScale(upperBound), 0]
])
.on('zoom', zoomed)
const g = svg.append('g')
.attr('transform', `translate(${margin.left}, ${margin.top})`)
const xAxis = d3.axisBottom()
.scale(xScale)
const xAxisG = g.append('g')
.style('clip-path', 'url(#clip)')
.attr("class", "x-axis")
.call(xAxis)
.attr('transform', `translate(0, ${innerHeight})`)
svg.call(zoom)
function zoomed(event) {
const updateX = event.transform.rescaleX(xScale)
const zx = xAxis.scale(updateX)
xAxisG.call(zx)
}
Instead of dealing with the axis' methods, you can simply select the ticks in the container group itself, removing the non-integers:
xAxisG.call(zx)
.selectAll(".tick")
.filter(e => e % 1)
.remove();
Here is your code with that change:
const upperBound = 1000
const height = 100
const width = 600
const margin = {
top: 20,
right: 20,
bottom: 20,
left: 35
}
const innerWidth = width - margin.left - margin.right
const innerHeight = height - margin.top - margin.bottom
const xScale = d3.scaleLinear()
.domain([0, 10])
.range([0, innerWidth])
const svg = d3.select('svg')
.attr("width", width)
.attr("height", height)
svg.append('defs').append('clipPath')
.attr('id', 'clip')
.append('rect')
.attr('width', innerWidth)
.attr('height', innerHeight)
const zoom = d3.zoom()
.scaleExtent([1, 8])
.translateExtent([
[0, 0],
[xScale(upperBound), 0]
])
.on('zoom', zoomed)
const g = svg.append('g')
.attr('transform', `translate(${margin.left}, ${margin.top})`)
const xAxis = d3.axisBottom()
.scale(xScale)
.tickFormat(d => ~~d)
const xAxisG = g.append('g')
.style('clip-path', 'url(#clip)')
.attr("class", "x-axis")
.call(xAxis)
.attr('transform', `translate(0, ${innerHeight})`)
svg.call(zoom)
function zoomed(event) {
const updateX = event.transform.rescaleX(xScale)
const zx = xAxis.scale(updateX)
xAxisG.call(zx)
.selectAll(".tick")
.filter(e => e % 1)
.remove();
}
<script src="https://d3js.org/d3.v7.min.js"></script>
<svg></svg>

d3 zoom consumes event and prevents mouseover

I'm attempting to make a stream graph in d3 that is both zoomable and the paths are hoverable.
From reading around I see that as of v4 d3 zoom consumes the event and therefore may be why my mouseover event no longer fires, however no amount of reordering or pointer-events: all I set seems to have an effect.
Is anybody able to help me understand what I have to do to get both my zoom and hover working in the following example? (There's also a codesandbox with the example)
const width = 500;
const height = 500;
let numberOfDataPoints = 5;
const numberOfLayers = 3;
let data = [];
for (let i = 0; i < numberOfDataPoints; i++) {
let point = [];
for (let j = 0; j < numberOfLayers; j++) {
point.push(Math.floor(Math.random() * Math.floor(120)));
}
data.push(point);
}
const x = d3
.scaleLinear()
.domain([0, numberOfDataPoints - 1])
.range([0, width]);
const y = d3.scaleLinear().range([height, 0]);
const area = d3
.area()
.x((d, i) => x(i))
.y0(d => y(d[0]))
.y1(d => y(d[1]));
const stack = d3
.stack()
.keys(d3.range(numberOfLayers))
.offset(d3.stackOffsetWiggle)
.order(d3.stackOrderNone);
let layers = stack(data);
y.domain([
d3.min(layers, l => d3.min(l, d => d[0])),
d3.max(layers, l => d3.max(l, d => d[1]))
]);
update();
const zoomContainer = d3
.select('svg')
.append('rect')
.attr('width', width)
.attr('height', height)
.attr('fill', 'none')
.style('pointer-events', 'all');
const zoom = d3
.zoom()
.scaleExtent([1, 8])
.on('zoom', zoomed);
zoomContainer.call(zoom);
function update() {
let path = d3
.select('svg')
.selectAll('path')
.data(layers);
path
.enter()
.append('path')
.attr('fill', 'red')
.merge(path)
.attr('d', d => area(d))
.on('mouseover', () => {
d3.select('svg')
.selectAll('path')
.attr('fill', 'red');
d3.select(d3.event.target).attr('fill', 'green');
});
}
function zoomed() {
let transform = d3.event.transform;
transform.y = 0;
let updatedScale = transform.rescaleX(x);
d3.select('svg')
.selectAll('path')
.attr('d', area.x((d, i) => updatedScale(i)));
}
The hypothesis for why this didn't work was correct (the zoom container consumes the event), and the fix is to just remove the zoomContainer and apply the zoom to the svg instead i.e.
const zoom = d3
.zoom()
.on('zoom', zoomed);
d3.select('svg').call(zoom);

D3 bar graph does not re-size correctly

I need some help with scaling with D3.In my codepen I am attempting to create a graph with some retrieve GDP data.
The data is retrieved and displayed correctly, but when I attempt to scale the graph only one vertical bar is displayed.
Can someone point me in the right direction?
Here is a link to project codepen:
https://codepen.io/henrycuffy/pen/gKVdgv
The main.js:
$(document).ready(function() {
$.getJSON(
'https://raw.githubusercontent.com/freeCodeCamp/ProjectReferenceData/master/GDP-data.json',
function(data) {
const dataset = data.data;
const w = 1000;
const h = 500;
var maxX = d3.max(dataset, d => d[1]);
var minDate = new Date(dataset[0][0]);
var maxDate = new Date(dataset[dataset.length - 1][0]);
var xScale = d3
.scaleTime()
.domain([minDate, maxDate])
.range([0, w]);
var yScale = d3
.scaleLinear()
.domain([0, maxX])
.range([h, 0]);
var xAxis = d3.axisBottom().scale(xScale);
var yAxis = d3.axisLeft().scale(yScale);
const svg = d3
.select('body')
.append('svg')
.attr('width', w)
.attr('height', h);
svg
.selectAll('rect')
.data(dataset)
.enter()
.append('rect')
.attr('x', (d, i) => xScale(i * 10))
.attr('y', d => h - yScale(d[1]))
.attr('width', 2)
.attr('height', d => yScale(d[1]))
.attr('fill', 'navy');
}
);
});
You are using a time scale for the x axis. But you aren't positioning the bars based on a time:
.attr('x', (d, i) => xScale(i * 10))
You are positioning each bar based on its index. The scale expects you to feed a date to it (it is taking the provided number and treating it as a date, which is pretty near the beginning of the epoch (Jan 1, 1970), which explains the positioning. The bars appear as one because each one is placed 10 milliseconds out from the previous one on a scale that covers decades, an imperceptible difference).
Instead let's feed the x scale the date in the data:
.attr('x', d => xScale(new Date(d[0]) )
Since the datum contains a string representation of the date, I'm converting to a date object here. You could do this to the data once it is loaded, but to minimize changes I'm just doing it when assigning the x attribute.
Here's an updated plunkr.

D3.js zoomable axis- how to zoom in only one direction?

I am using D3.js v4.
I have a minimum example working with zooming in and out on a single axis, with the following code:
// Create dummy data
var data = [];
for (var i = 0; i < 100; i++) {
data.push([Math.random(), Math.random()]);
}
// Set window parameters
var width = 330
var height = 200
// Append div, svg
d3.select('body').append('div')
.attr('id', 'div1')
d3.select('#div1')
.append("svg").attr("width", width).attr("height",height)
.attr('id','chart')
// Create scaling factors
var x = d3.scaleLinear()
.domain([0,1])
.range([0, (width - 30)])
var y = d3.scaleLinear()
.domain([0,1])
.range([0,height])
// Create group, then append circles
d3.select('#chart').append('g')
.attr('id','circlesplot')
d3.select('#circlesplot')
.selectAll('circles')
.data(data)
.enter().append('circle')
.attr('cx', function(d,i){ return x(d[0]); })
.attr('cy', function(d,i){ return y(d[1]); })
.attr('r', 4)
// Create y axis, append to chart
var yaxis = d3.axisRight(y)
.ticks(10)
var yaxis_g = d3.select('#chart').append('g')
.attr('id', 'yaxis_g')
.attr('transform','translate(' + (width - 30) +',0)')
.call(yaxis)
// Create zoom svg to the right
var svg = d3.select('#div1')
.append('svg')
.attr('width', 30)
.attr('height', height)
.attr('transform', 'translate('+ width + ',0)')
.call(d3.zoom()
.on('zoom', zoom))
function zoom() {
// Rescale axis during zoom
yaxis_g.transition()
.duration(50)
.call(yaxis.scale(d3.event.transform.rescaleY(y)))
// re-draw circles using new y-axis scale
var new_y = d3.event.transform.rescaleY(y);
d3.selectAll('circle').attr('cy', function(d) { return new_y(d[1])})
}
fiddle here: https://jsfiddle.net/v0aw9Ler/#&togetherjs=2wg7s8xfhC
Putting the mouse just to the right of the yaxis and scrolling gives the zooming function on the y axis.
What I'd like to happen is for the y axis maximum (in this case 1.0) to stay fixed, while zooming only in the other direction. You can kind of see what I mean by placing the mouse at the very bottom and just to the right of the y axis, and see the points cluster at the bottom of the graph.
I think it has to do with using zoom.extent(), but I'm just really not sure where to go from here; advice is greatly appreciated.
Source for this min working example:
http://bl.ocks.org/feyderm/03602b83146d69b1b6993e5f98123175

Categories

Resources