OK, então já tive outra chance - como mencionado na minha resposta anterior , o maior problema que você precisa superar é que o zoom d3 permite apenas dimensionamento simétrico. Isso é algo amplamente discutido , e acredito que Mike Bostock abordará isso no próximo lançamento.
Portanto, para superar o problema, você precisa usar o comportamento de zoom múltiplo. Criei um gráfico com três, um para cada eixo e outro para a área de plotagem. Os comportamentos de zoom X e Y são usados para dimensionar os eixos. Sempre que um evento de zoom é gerado pelos comportamentos de zoom X e Y, seus valores de conversão são copiados para a área de plotagem. Da mesma forma, quando ocorre uma translação na área de plotagem, os componentes x e y são copiados para os respectivos comportamentos do eixo.
A escala na área de plotagem é um pouco mais complicada, pois precisamos manter a proporção. Para conseguir isso, armazeno a transformação de zoom anterior e uso o delta da escala para elaborar uma escala adequada para aplicar aos comportamentos de zoom X e Y.
Por conveniência, agrupei tudo isso em um componente do gráfico:
const interactiveChart = (xScale, yScale) => {
const zoom = d3.zoom();
const xZoom = d3.zoom();
const yZoom = d3.zoom();
const chart = fc.chartCartesian(xScale, yScale).decorate(sel => {
const plotAreaNode = sel.select(".plot-area").node();
const xAxisNode = sel.select(".x-axis").node();
const yAxisNode = sel.select(".y-axis").node();
const applyTransform = () => {
// apply the zoom transform from the x-scale
xScale.domain(
d3
.zoomTransform(xAxisNode)
.rescaleX(xScaleOriginal)
.domain()
);
// apply the zoom transform from the y-scale
yScale.domain(
d3
.zoomTransform(yAxisNode)
.rescaleY(yScaleOriginal)
.domain()
);
sel.node().requestRedraw();
};
zoom.on("zoom", () => {
// compute how much the user has zoomed since the last event
const factor = (plotAreaNode.__zoom.k - plotAreaNode.__zoomOld.k) / plotAreaNode.__zoomOld.k;
plotAreaNode.__zoomOld = plotAreaNode.__zoom;
// apply scale to the x & y axis, maintaining their aspect ratio
xAxisNode.__zoom.k = xAxisNode.__zoom.k * (1 + factor);
yAxisNode.__zoom.k = yAxisNode.__zoom.k * (1 + factor);
// apply transform
xAxisNode.__zoom.x = d3.zoomTransform(plotAreaNode).x;
yAxisNode.__zoom.y = d3.zoomTransform(plotAreaNode).y;
applyTransform();
});
xZoom.on("zoom", () => {
plotAreaNode.__zoom.x = d3.zoomTransform(xAxisNode).x;
applyTransform();
});
yZoom.on("zoom", () => {
plotAreaNode.__zoom.y = d3.zoomTransform(yAxisNode).y;
applyTransform();
});
sel
.enter()
.select(".plot-area")
.on("measure.range", () => {
xScaleOriginal.range([0, d3.event.detail.width]);
yScaleOriginal.range([d3.event.detail.height, 0]);
})
.call(zoom);
plotAreaNode.__zoomOld = plotAreaNode.__zoom;
// cannot use enter selection as this pulls data through
sel.selectAll(".y-axis").call(yZoom);
sel.selectAll(".x-axis").call(xZoom);
decorate(sel);
});
let xScaleOriginal = xScale.copy(),
yScaleOriginal = yScale.copy();
let decorate = () => {};
const instance = selection => chart(selection);
// property setters not show
return instance;
};
Aqui está uma caneta com o exemplo de trabalho:
https://codepen.io/colineberhardt-the-bashful/pen/qBOEEGJ
Existem alguns problemas no seu código, um que é fácil de resolver e outro que não é ...
Em primeiro lugar, o d3-zoom funciona armazenando uma transformação nos elementos DOM selecionados - você pode ver isso através da
__zoom
propriedade Quando o usuário interage com o elemento DOM, essa transformação é atualizada e os eventos são emitidos. Portanto, se você tiver comportamentos diferentes de zoom, ambos controlando a panorâmica / zoom de um único elemento, precisará manter essas transformações sincronizadas.Você pode copiar a transformação da seguinte maneira:
No entanto, isso também fará com que os eventos de zoom sejam disparados a partir do comportamento de destino.
Uma alternativa é copiar diretamente para a propriedade de transformação 'stashed':
No entanto, há um problema maior com o que você está tentando alcançar. A transformação d3-zoom é armazenada como 3 componentes de uma matriz de transformação:
https://github.com/d3/d3-zoom#zoomTransform
Como resultado, o zoom pode representar apenas uma escala simétrica junto com uma tradução. Seu zoom assimétrico aplicado ao eixo x não pode ser fielmente representado por essa transformação e reaplicado à área da plotagem.
fonte
Este é um próximo recurso , como já observado pelo @ColinE. O código original está sempre fazendo um "zoom temporal" que não é sincronizado a partir da matriz de transformação.
A melhor solução é ajustar o
xExtent
intervalo para que o gráfico acredite que há velas adicionais nas laterais. Isso pode ser conseguido adicionando almofadas nas laterais. Oaccessors
, em vez de ser,torna-se,
Nota: Note que existe uma
pad
função que deve fazer isso, mas, por algum motivo, ela funciona apenas uma vez e nunca é atualizada novamente, por isso é adicionada como umaaccessors
.Nota 2: Função
addDays
adicionada como um protótipo (não é a melhor coisa a fazer) apenas por simplicidade.Agora, o modifica evento zoom nosso fator de zoom X,
xZoom
,É importante ler o diferencial diretamente de
wheelDelta
. É aqui que está o recurso não suportado: Não podemos ler,t.x
pois ele mudará mesmo se você arrastar o eixo Y.Por fim, recalcule
chart.xDomain(xExtent(data.series));
para que a nova extensão esteja disponível.Veja a demonstração de trabalho sem pular aqui: https://codepen.io/adelriosantiago/pen/QWjwRXa?editors=0011
Corrigido: Reversão de zoom, comportamento aprimorado no trackpad.
Tecnicamente, você também pode ajustar
yExtent
adicionando extrasd.high
ed.low
's. Ou até os doisxExtent
eyExtent
para evitar o uso da matriz de transformação.fonte