Reset the zoom state dynamically?

So, I have various groups of SVG elements, wrapped in s and want the user to be able to drag around and resize these objects when they mouse over them.

I kind of got it going but when switching between gourps, by onMouseOut of one and onMouseOver of another there is weird stuff going on. The zoom state of the last group is carried over to the new target group and weirdness happens. The docs state:

The zoom behavior stores the zoom state on the element to which the zoom behavior was applied, not on the zoom behavior itself. This allows the zoom behavior to be applied to many elements simultaneously with independent zooming. The zoom state can change either on user interaction or programmatically via zoom.transform.

I figure what is going on is that the state is stored in the parent i.e. svg element usually, and the transform is applied to a sub element, using the parent relatively that the transform is calculated against.

some code

const svg = d3.select(svgRef.current);  // frame of reference
const g = svg.select("g");// should be the main group
function defaultZoomed({ transform }) {
    g.attr("transform", transform);
}
const zoomedFunc = zoomed ? zoomed : defaultZoomed;

// Reset to kill any previoud state
svg.on(".zoom", null);
// Create a new zoom behavior
svg.call(d3.zoom()
    .extent([[0, 0], [dimensions.width, dimensions.height]])
    .scaleExtent([0.01, 10])
    .on("zoom", zoomedFunc));

Here zoomed is set onMouseOver and onMouseOut of the various zoomable things (I’m doing all this in React to keep it interesting).

What I really need to do is be able to reset the zoom state which I assume is kept on the parent svg element, but how?

This is tricky to answer without seeing more code. First, I’d challenge your assumption that the zoom state is stored in the parent node, I don’t think that’s the case. You also seem to be setting values by hand, rather than using transitions. That might explain how the state seems to propagate from one group to another. If you set this up using a join and transitions, plus a key function to create a binding between the data and SVG elements, it’s likely to work better.

1 Like

Hi @eagereyes thanks for your response,

I guess what I’m trying to achieve is very similar to this video. Why i think the state is on the parent is if you look at the object getting zoomed you’ll see that the only change on the object is the transform attribute nothing changes on the SVG parent. If the state were to be kept on the object getting zoomed then why would there be this jerkiness when the target is switched. It’s implied by the docs

The zoom behavior stores the zoom state on the element to which the zoom behavior was applied, not on the zoom behavior itself.

You can see in the code the zoom object is applied to the parent SVG object

svg.call(d3.zoom().on("zoom", zoomedFunc));

.call takes the pasted function, here d3.zoom, and passes it’s parent into it, here svg. The transition is calculated relative to svg and then handed to the “zoom” event function (which I’m switching dynamically based on mouse events).

I don’t understand what you mean about “setting values by hand”, what values? Perhaps you could give an example or point me in the direction of one when you say use join and transitions?

Here is the full code:

function Space() {
    const defaultZoomed = () => ({ transform }) => d3.select("g#MAIN").attr("transform", { transform });
    const [activeZoomed, setActiveZoomed] = useState(defaultZoomed);

    return <Zoomable setZoomed={setActiveZoomed} key={e.contentUrl}>
        <Something />
    </Zoomable>;
}

const Zoomable = ({ svgRef, setZoomed, children }) => {
    const ref = useRef(null);
    // const [hovered, eventHandlers] = useHover(ref);

    const zoomed = () => ({ transform }) => {
        const g = d3.select(ref.current)
        g.attr("transform", transform);
    }

    const onMouseOver = () => {
        setZoomed(zoomed);
    }

    const onMouseOut = () => {
        setZoomed(null);
    }

    useEffect(() => {
        setZoomed(zoomed);
    }, [hovered])

    return (
        <g
            className="draggable"
            ref={ref}
            onMouseOver={onMouseOver}
            onMouseOut={onMouseOut}
        >
            {children}
        </g >
    );
};


function useZoomableSVG(svgRef, zoomed) {

    useEffect(() => {
        const svg = d3.select(svgRef.current);  // frame of reference
        const g = svg.select("g");// should be the main group
        function defaultZoomed({ transform }) {
            g.attr("transform", transform);
        }
        const zoomedFunc = zoomed ? zoomed : defaultZoomed;

        // Reset to kill any previoud state
        svg.on(".zoom", null);
        // Create a new zoom behavior
        svg.call(d3.zoom()
            .extent([[0, 0], [dimensions.width, dimensions.height]])
            .scaleExtent([0.01, 10])
            .on("zoom", zoomedFunc));

    }, [ zoomed])
};

The use useZoomableSVG is a simple variant on this example

So looking at the code it seems to keep the state on a .__zoom property of the selection. I’ll try killing that when switching the zoomed function.

ok, so somebody has been doing category theory and so I need to set the .__zoom property to the Transform Identity object which I have no idea how to get at…? Transform is fairly small so I guess I’ll just copy the whole class file and keep my finger crossed that it doesn’t change…

This might help (ignore the part about removing the handlers):

1 Like