OK, więc postanowiłem to jeszcze raz - jak wspomniano w mojej poprzedniej odpowiedzi , największym problemem, który musisz rozwiązać, jest to, że zoom d3 pozwala tylko na skalowanie symetryczne. Jest to szeroko dyskutowane i uważam, że Mike Bostock zajmuje się tym w następnym wydaniu.
Tak więc, aby rozwiązać ten problem, musisz użyć funkcji wielokrotnego powiększania. Utworzyłem wykres, który ma trzy, po jednym dla każdej osi i jeden dla obszaru wykresu. Zachowania zoomu X i Y są używane do skalowania osi. Za każdym razem, gdy zachowanie zoomu wywołuje zdarzenie zoomu, ich wartości translacji są kopiowane do obszaru wydruku. Podobnie, gdy występuje translacja w obszarze wydruku, komponenty x i y są kopiowane do odpowiednich zachowań osi.
Skalowanie w obszarze wykresu jest nieco bardziej skomplikowane, ponieważ musimy zachować proporcje. Aby to osiągnąć, przechowuję poprzednią transformację zoomu i używam delty skali, aby opracować odpowiednią skalę do zastosowania dla zachowań zoomu X i Y.
Dla wygody zawarłem to wszystko w składnik wykresu:
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;
};
Oto długopis z działającym przykładem:
https://codepen.io/colineberhardt-the-bashful/pen/qBOEEGJ
Kod ma kilka problemów, z których jeden jest łatwy do rozwiązania, a drugi nie ...
Po pierwsze, zoom d3 działa poprzez zapisanie transformacji na wybranych elementach DOM - możesz to zobaczyć poprzez
__zoom
właściwość. Gdy użytkownik wchodzi w interakcję z elementem DOM, ta transformacja jest aktualizowana i emitowane są zdarzenia. Dlatego, jeśli musisz zmieniać zachowanie zoomu, z których oba kontrolują przesuwanie / powiększanie pojedynczego elementu, musisz synchronizować te transformacje.Możesz skopiować transformację w następujący sposób:
Spowoduje to jednak również, że zdarzenia zoom będą uruchamiane również z zachowania docelowego.
Alternatywą jest skopiowanie bezpośrednio do właściwości transformacji „ukrytych”:
Istnieje jednak większy problem z tym, co próbujesz osiągnąć. Transformacja d3-zoom jest przechowywana jako 3 elementy macierzy transformacji:
https://github.com/d3/d3-zoom#zoomTransform
W rezultacie powiększenie może reprezentować jedynie symetryczne skalowanie wraz z tłumaczeniem. Twój asymetryczny zoom zastosowany do osi X nie może być wiernie reprezentowany przez tę transformację i ponownie zastosowany do obszaru kreślenia.
źródło
To nadchodząca funkcja , jak już zauważyła @ColinE. Oryginalny kod zawsze wykonuje „zbliżenie czasowe”, które nie jest synchronizowane z macierzą transformacji.
Najlepszym obejściem jest ulepszenie
xExtent
zakresu, aby wykres uznał, że po bokach znajdują się dodatkowe świece. Można to osiągnąć, dodając podkładki po bokach.accessors
, Zamiast,staje się,
Sidenote: Zauważ, że jest
pad
funkcja, która powinna to zrobić, ale z jakiegoś powodu działa tylko raz i nigdy nie aktualizuje się ponownie, dlatego została dodana jakoaccessors
.Sidenote 2: Funkcja
addDays
dodana jako prototyp (nie najlepsza rzecz do zrobienia) tylko dla uproszczenia.Teraz zdarzenie powiększenia modyfikuje nasz współczynnik powiększenia X
xZoom
,Ważne jest, aby odczytać różnicę bezpośrednio z
wheelDelta
. Oto, gdzie znajduje się nieobsługiwana funkcja: Nie możemy odczytać,t.x
ponieważ zmieni się, nawet jeśli przeciągniesz oś Y.Na koniec ponownie oblicz,
chart.xDomain(xExtent(data.series));
aby nowy zasięg był dostępny.Zobacz działające demo bez skoku tutaj: https://codepen.io/adelriosantiago/pen/QWjwRXa?editors=0011
Naprawiono: Cofanie powiększenia, ulepszone zachowanie na gładziku.
Technicznie możesz również ulepszyć
yExtent
, dodając dodatkowed.high
id.low
. Lub nawet jednoxExtent
i drugie,yExtent
aby w ogóle nie używać macierzy transformacji.źródło