Shapes that reference multiple axes simultaneously
The x or y coordinates in shapes currently reference exactly one axis each, but if we could allow multiple axis references within one shape, you could link up features in different subplots in a way that reacts as each subplot is independently zoomed or panned.
This was mentioned a long time ago on the forum: https://community.plotly.com/t/draw-a-line-between-data-points-of-different-subplots/29115 (with a partial workaround suggested by @empet)
and various times on SO, eg https://stackoverflow.com/questions/44670708/draw-line-on-top-of-subplot-to-render-a-zoom-effect
and https://stackoverflow.com/questions/17543359/drawing-lines-between-two-plots-in-matplotlib
My interest in this stems from striplogs, a common tool in mining and oil & gas for analyzing drilling data, where you want to correlate features you see in multiple drillholes, for example https://help.rockware.com/rockworks17/WebHelp/striplog_h2h.htm where you can see lines or filled regions that are paper-referenced in their x coordinates, but their y coordinates reference a different axis for each drill hole:
and it could be particularly powerful to pan each drillhole axis separately to align one of these features and see how the other features do or don't line up.
As to API: for simple shapes (lines are the most important one here) this could be accomplished by extending the idea of xref to add x0ref and x1ref (similarly for y). For path shapes (important for the filled areas in the striplog use case) this would need to be handled within the path string, perhaps something like :x3 after a coordinate to describe its reference, so the paths would look like: "M0:paper 470:y H0.2:paper L0.3:paper 392:y2 H0.5:paper L0.7:paper 524:y3 H0.9:paper". Then we could either ignore xref/yref, require you to set something like xref='variable' to opt in to this behavior, or allow an xref and use that for any coordinate for which you omit the reference.
Then when drawing, the pixel coordinates would need to be re-evaluated every time any of the referenced axes was zoomed or panned - which I think we may do today anyway, unlike graph data that just has a svg transform applied to it during a zoom or pan and is only re-evaluated when the interaction is done.
I'm hopeful that this can happen as well. We have users who like to have drawn lines between two different Y axes on a single X.
Our workaround is to scale Y-axis 2 to match Y-axis 1 and plot both traces on that same axis and just draw the lines between them that way. Not the greatest solution, but it has worked so far to some degree.
Here's an ongoing internal request for discussion (RFD) that we have for this feature:
Summary
As part of an ongoing Plotly Professional Services contract, Kobold Metals has requested multiple feature requests/bug fixes for Plotly.js. The first deliverable extends the shapes feature to support per-coordinate axis reference, so that each defining coordinate can refer to a different axis (e.g. xref: ['x', 'x2'] for a rectangle's x0 and x1) rather than applying a single xref/yref to all coordinates.
This feature enhancement would be used for Kobold Metals' geological visualization use cases, where a shape span across multiple graph subplots of drill hole data and each vertex of the shape must be anchored to a different subplot axis. This would allow users to zoom or pan individual subplots, with the corresponding vertex of the shape moving with that subplot's scale.
Problem Statement
Shapes currently support only a single xref and yref that applies to all vertices. We need per-vertex axis references so shapes can span multiple subplots (e.g., striplogs where lines connect drill holes with different depth scales).
Key Decision: What API design should we use to specify per-vertex axis references?
Options Considered
Option 1: Parallel Arrays
API:
shapes: [{
type: 'rect',
x0: 0, y0: 0, x1: 1, y1: 1,
xref: ['x', 'x2'],
yref: ['y', 'y2']
}]
When xref/yref are provided as arrays, each element corresponds to a vertex of the shape. For rectangles, this means one reference axis for x0 and one for x1 (similarly for y0 and y1). When provided as strings (current behavior), the single value applies to all coordinates. This maintains backward compatibility while extending functionality.
If the array length doesn't match the number of defining vertices, we can fall back to using the single xref/yref value (if provided as a string) for all vertices, or use the first element of the list. Empty lists can be treated as an invalid value, so we fall back to the default xref/yref value, paper.
Pros: Matches existing xref/yref pattern and array index = vertex index. Backward compatible in that passing string values will continue to work. No new attributes needed.
Cons: As @emilykl has addressed a comment, the major drawback for this option is with path shapes:
It could become unwieldy for the user to mentally match up path vertices with xref/yref list items. So if we agree on that approach internally it’s probably worth validating with Kobold whether they are happy with that API
Option 2: Separate Attributes per Vertex (for Rectangles)
Idea from Alex Johnson, drafted in this issue
API:
shapes: [{
type: 'rect',
x0: 0, y0: 0, x1: 1, y1: 1,
x0ref: 'paper', y0ref: 'y',
x1ref: 'x2', y1ref: 'y2' }]
Pros: Simple for rectangle shapes, clear separation of corner points Cons: Only works for simple shapes like rect or line but doesn't scale as well for complex path shapes with many vertices
Option 3: Coordinate-Pair Objects
API:
shapes: [{
type: 'rect',
x0: 0, y0: 0, x1: 1, y1: 1,
vertexRefs: [
{x: 'x', y: 'y'}, // (x0, y0)
{x: 'x2', y: 'y2'} // (x1, y1)
]
}]
Pros: X/Y references stay together Cons: Still need vertex mapping like in option 1. Slightly more verbose and differs a bit from the existing API
Option 4: Client Proposal - Inline Path Syntax
API:
shapes: [{
type: 'path',
path: 'M0:paper 0:y L1:x2 0:y L1:x2 1:y2 L0:paper 1:y2 Z'
}]
Another idea by Alex Johnson from the same issue. Here, we specify axis references inline after each coordinate.
Pros: References are embedded in the path definition without a need for separate arrays Cons: Requires modifying the existing SVG parser to read this non-standard SVG syntax. Breaks SVG compatibility and probably would be harder to programmatically modify
Recommendations
My recommended option is option 1, since it matches existing Plotly.js patterns and would work for all shape types (line, rect, path). Any SVG paths would remain valid/compatible. It would be a lot easier to programmatically generate/modify as well, compared to option 4.
Implementation Plan
Major Steps (with rough timeline)
- Add attributes and validation (Week 1)
- Update
xrefandyrefinshapes/attributes.jsto accept both string and array types. Add validation for array length matching the number of defining coordinates, and handle fallback behavior when arrays don't match coordinates. Ensure backward compatibility with existing string values.
- Update
- Update coordinate conversion (Week 2-3)
- There are a couple of functions (
getPathStringandconvertPathinshapes/helpers.js) that need to be refactored to take in arrays for thexandyreferences and convert them to usable coordinates
- There are a couple of functions (
- Update shape rendering (Week 4-5)
- Modify shape rendering functions in
shapes/draw.jsto allow for array values ofxrefandyref. The main areas that need updating: - Layer selection logic: Currently checks
options.xref === 'paper' || options.yref === 'paper'to figure out which layer to draw on. With arrays, we'll need to check if any element in the arrays is paper setClipPathfunction: ConcatenatesshapeOptions.xref + shapeOptions.yrefto determine clip axes. With arrays, we'll probably need to handle this differently - maybe combine all unique axis references, or determine clipping based on the bounding box of all vertices?setupDragElementfunction: For dragging/resizing, we might need to map the appropriate axis reference for each coordinate being dragged. This could get tricky for shapes spanning multiple subplots but need to think through this more thoroughly.
- Modify shape rendering functions in
- Testing and documentation (Week 6)
- Add test cases covering multi-axis shapes, using array values for
xref/yrefand spanning multiple subplots. Including edge cases like mixing paper and axis references, draggable shapes, etc. - Update documentation in Plotly.js codebase to reflect new multi-axis shapes features
- Add test cases covering multi-axis shapes, using array values for
Conclusion
This RFD is to determine the API design options and subsequently, the implementation of enabling per-vertex axis references in Plotly.js shapes. This will allow shapes to span multiple subplots with independent coordinate systems.
My recommendation is Option 1 (extending existing xref/yref to support arrays). The implementation aligns with existing patterns in Plotly.js and should work with all shape types.