Reputation: 43
I`m developing visualization with d3 v6 that's basically use a set of entries to show horizontal layers.
The visualization looks like this:
The caveat is that as number of layers grows some layers become impossible to see due scale decreasing. So I want to make a zoom (and pan) feature, but it has to work only on the Y axis. I've search a lot and found this tutorial (https://observablehq.com/@d3/x-y-zoom) from where I've made some changes to adapt to my visualization:
const svgHeight: any = d3.select(svgContainer.current).attr('height');
const yScaleGlobal = d3
.scaleLinear()
.domain([0, maxYValues])
.range([0, svgHeight - MARGINS.TOP - MARGINS.BOTTOM]);
const yAxis = d3.axisLeft(yScaleGlobal).tickFormat((d: any) => `${d} m`);
const gY = layerGroup.append('g').attr('class', 'y-axis');
// z holds a copy of the previous transform, so we can track its changes
let z = d3.zoomIdentity;
// set up the ancillary zooms and an accessor for their transforms
const zoomY = d3.zoom().scaleExtent([0.2, 5]);
const ty = () => d3.zoomTransform(gY.node());
gY.call(zoomY).attr('pointer-events', 'none');
drawViz();
const zoom = d3.zoom().on('zoom', function (e) {
const t = e.transform;
const k = t.k / z.k;
const point = center(e, this);
if (k === 1) {
// pure translation?
gY.call(zoomY.translateBy, 0, (t.y - z.y) / ty().k);
} else {
// if not, we're zooming on a fixed point
gY.call(zoomY.scaleBy, k, point);
}
z = t;
drawViz();
});
svg.call(zoom).call(zoom.transform, d3.zoomIdentity.scale(0.8)).node();
function drawViz() {
const yr = ty().rescaleY(yScaleGlobal);
yAxis.scale(yr);
gY.call(yAxis, yr);
if (data) updateLayers(data, yr);
}
function center(event, target) {
if (event.sourceEvent) {
const p = d3.pointers(event, target);
return [d3.mean(p, (d) => d[0]), d3.mean(p, (d) => d[1])];
}
return [svgWidth / 2, svgHeight / 2];
}
The updateLayers
function is something like this:
const updateLayers = (data: LAYER_COMPONENT_TYPE[], yScale) => {
const layerFill = getLayerFill(data);
const rects = layerGroup.selectAll('rect').data(data);
rects.exit().remove();
const newLayers = rects
.enter()
.append('rect')
.attr('x', 10)
.style('opacity', 0.5)
.attr('width', 300)
.style('stroke', '#101010')
.style('stroke-width', '1px');
newLayers
.merge(rects)
.style('fill', (d: LAYER_COMPONENT_TYPE) => {
if (!layerFill[`${d.fgdc_texture}.${d.from}`].url) {
return layerFill[`${d.fgdc_texture}.${d.from}`];
}
svg.call(layerFill[`${d.fgdc_texture}.${d.from}`]);
return layerFill[`${d.fgdc_texture}.${d.from}`].url();
})
.attr('y', (d: LAYER_COMPONENT_TYPE, i) => {
if (i === 0) return yScale(d.from);
return yScale(data[i - 1].to);
})
.attr('height', (d: LAYER_COMPONENT_TYPE, i) => {
return yScale(d.to - d.from);
});
};
The zoom and pan works fine and the whole visualization too, but the yScale have a strange behavior. The height of the rectangles changes out of scale at zoom or pan (i've realized that the anomaly of the height is equal to the distance of "0 m" point at yScale) here you can view the problem
Anyways, I can figure out what's wrong with the code or what's causing the anomaly. So i want some help with this problem please
Edit 1: I'm using react btw
Upvotes: 1
Views: 564
Reputation: 43
I've already came out with a solution (with a better and cleaner approach). This solution is based the TBD's d3 timeline implementation. Using his solution I just adapted to the y axis and to my code
const maxYValues = d3.max(maxValues) || 0;
const yScaleGlobal = d3
.scaleLinear()
.domain([0, maxYValues])
.range([0, svgHeight - MARGINS.TOP - MARGINS.BOTTOM]);
const yAxis = d3.axisLeft(yScaleGlobal).tickFormat((d: any) => `${d} m`);
const gY = litoligicalGroup.select(`.${styles.yAxis}`).call(yAxis);
const spanY = (d) => {
if (d.thickness) return yScaleGlobal(0) - 10;
return yScaleGlobal(d.from);
};
const spanH = (d) => {
if (d.thickness) return 10;
return yScaleGlobal(d.to - d.from);
};
const zoom = d3
.zoom()
.scaleExtent([0.2, 5])
.on('zoom', (e) => {
const transform = e.transform;
gY.call(yAxis.scale(transform.rescaleY(yScaleGlobal)));
// the viz group is where all the layers are attached
vizGroup
.selectAll('rect')
.attr('y', (d) => {
if (!d) return null;
return transform.applyY(spanY(d));
})
.attr('height', (d) => {
if (!d) return null;
return transform.k * spanH(d);
});
});
drawProfile();
svg.call(zoom);
function drawProfile() {
if (geologicData) updateGeology(geologicData, yScaleGlobal);
if (constructionData) updatePoco(constructionData, yScaleGlobal);
}
Upvotes: 1