All work
Open SourceMar 2020 – Apr 2020

de-transform — CSS transform coordinate correction

Canvas and charting libraries report wrong mouse event coordinates when CSS transforms are on parent elements. Built a JavaScript module that mathematically inverts the full transform stack to give correct coordinates.

github.com/siddharthapaul001/de-transform · Solves IE9+ cross-browser


The problem

JavaScript canvas and SVG charting libraries — including FusionCharts — rely on mouse event coordinates to detect which element a user clicked or hovered over. The browser provides these as clientX/clientY relative to the viewport.

When a CSS transform (rotate, scale, perspective, matrix3d) is applied to the chart container or any of its parent elements, the coordinates the library receives are in the transformed space, not the element's own space. The result: click detection is wrong. Hovering over a data point selects a different one. Drag operations move in the wrong direction. The further the transform, the more broken the interaction.

This is a fundamental issue with how the browser's coordinate system interacts with CSS transforms — no charting library could handle it without external help.

What I built

Built a standalone JavaScript module — de-transform — with three APIs:

getOriCoordinate(element, event, useOffset) — the core function. Takes an HTML element and a MouseEvent and returns the correct coordinates relative to that element, after mathematically cancelling all CSS transforms on it and its parents. Two approaches depending on browser support (see architecture).

applyTransformAtPoint(point, matrix, origin) — the raw math primitive. Applies a CSS matrix() or matrix3d() transform to any {x, y} or {x, y, z} point at a given transform-origin. Handles both 2D (6-value matrix) and 3D (16-value matrix3d). Used internally by getOriCoordinate.

getOriDragDistance(element, event, state, useOffset) — wraps getOriCoordinate across a drag sequence (start → move → end) to return correct dx/dy deltas even when the element is inside a rotated or scaled container.

Approaches considered

element.getBoundingClientRect() alone — returns the bounding box in screen space but doesn't give per-pixel coordinate correction for arbitrary nested 3D transforms
MouseEvent.offsetX / offsetY (useOffset=true) — simpler and faster, but these are experimental properties; Firefox returns incorrect values when a 3D transform is involved
Ask the charting library to handle transforms — not feasible: would require each library to implement its own matrix math; a standalone module is reusable across any library
DOM traversal + matrix inverse (useOffset=false) — chosen: traverses from the target element to BODY, collects every CSS transform matrix and its origin, then applies the inverse chain bottom-up. Works IE9+, handles nested 2D transforms on multiple parents, and a single 3D transform.

Architecture

getOriCoordinate (matrix-inverse path): traverse DOM from element → BODY → collect elements with non-identity CSS transforms → read their computed matrix() / matrix3d() and transform-origin → build inverse transform stack → apply inverse chain to clientX/clientY top-down → return coordinate in element's own space.

applyTransformAtPoint: parse CSS matrix string → translate point by subtracting origin → apply 2×2 (2D) or 4×4 (3D) matrix multiplication → translate back by adding origin → return transformed point.

getOriDragDistance: on 'start', store getOriCoordinate result; on 'move', subtract stored start from current getOriCoordinate result; on 'end', clear state.

Result

Published as an open source library at github.com/siddharthapaul001/de-transform. Built specifically to solve a real production problem encountered at FusionCharts when charts are embedded inside transformed containers. Supports IE9+. Handles nested 2D transforms on any number of parent elements, and single CSS 3D transforms (perspective + rotateX/Y/Z combinations).

JavaScriptCSS TransformsMatrix MathDOMCanvas
Back to all work