Skip to content

[BUG]: Image layer on a map trace renders a little north of where coordinates says it should #8043

Description

@aldichollow

Hi, thanks for maintaining this library. I ran into something while building a small feature that draws an interpolated raster on top of a map (MapLibre-based) figure as an image layer, and I wanted to check whether this is expected or might be a bug somewhere in how the image source's coordinates get turned into screen positions. Found this while using plotly.py, but hope this is the right place.

Description

When I add an image via layout.map.layers with sourcetype: "image", the image renders slightly north of the position its coordinates describe. It's small, but it's consistent and I can reproduce it reliably. A static export of the exact same figure through kaleido does not show this offset - only the live, interactive rendering does. That's actually how I first noticed it: I was checking my own coordinate math against a kaleido render, everything lined up, and then it looked off again as soon as I opened the figure in a browser.

Versions

  • plotly (Python): 6.9.0
  • I tried both include_plotlyjs="cdn" and include_plotlyjs=True (the bundle that ships with 6.9.0) and got the same result either way, so it doesn't seem to be tied to a particular plotly.js build.
  • Browser: Chromium
  • OS: macOS

Steps to reproduce (Minimal example)

Needs plotly, numpy, and pillow.

import base64
import io

import numpy as np
import plotly.graph_objects as go
import plotly.io as pio
from PIL import Image

# A small raster: mostly transparent white, with a 2-row-thick red band
# painted across the exact middle row. Row 0 is the top of the image (north),
# matching the coordinate order below.
ny, nx = 100, 100
rgba = np.full((ny, nx, 4), 255, dtype=np.uint8)
rgba[..., 3] = 160
mid = ny // 2
rgba[mid - 1 : mid + 1, :] = [255, 0, 0, 255]

buffer = io.BytesIO()
Image.fromarray(rgba).save(buffer, format="PNG")
data_uri = "data:image/png;base64," + base64.b64encode(buffer.getvalue()).decode()

WEST, SOUTH, EAST, NORTH = 134.5, 32.0, 142.5, 40.5
# Where the red band actually sits, by construction.
true_lat = NORTH - (mid + 0.5) / ny * (NORTH - SOUTH)

figure = go.Figure()
figure.update_layout(
    map={
        "style": "carto-positron",
        "center": {"lat": (SOUTH + NORTH) / 2, "lon": (WEST + EAST) / 2},
        "zoom": 5.2,
        "layers": [
            {
                "sourcetype": "image",
                "source": data_uri,
                "coordinates": [[WEST, NORTH], [EAST, NORTH], [EAST, SOUTH], [WEST, SOUTH]],
                "opacity": 1.0,
                "below": "traces",
            }
        ],
    },
    width=1200,
    height=1000,
    title="Red band should sit on the green line, not above it",
)
# A second, independent way of marking the same latitude, so there's
# something to compare the image against that isn't derived from the image
# layer itself.
figure.add_trace(
    go.Scattermap(
        lat=[true_lat, true_lat],
        lon=[WEST, EAST],
        mode="lines",
        line={"color": "lime", "width": 1},
        hoverinfo="skip",
        showlegend=False,
    )
)

pio.write_html(figure, "repro.html", include_plotlyjs=True)

Opening repro.html, I'd expect the red band to sit right on top of the green line, since both mark the same latitude computed two different ways. Instead the red band shows up a bit north of it - see the attached screenshot (plotly_issue_repro_minimal.png).

If it helps, pio.write_image(figure, "repro_kaleido.png") (needs kaleido) renders this correctly, no offset - which is what made me think this is specific to the live MapLibre GL rendering path rather than something wrong with the coordinates I'm passing in.

Trying to narrow it down a little further

I wasn't sure if this was a fixed number of pixels (which would point at something like a CSS/layout offset) or a fixed number of degrees (which would point at the coordinate conversion itself), so I measured it two ways:

  • Read map.project([lon, lat]) from the live MapLibre instance for the reference latitude, as a pixel ground truth.
  • Located the actual rendered red band in a screenshot of the page.

At zoom: 5.2 the offset came out to about 13 device pixels. At zoom: 7.5, same figure otherwise, it was about 62.5 device pixels. That ratio (about 4.8x) is close to the ratio of pixels-per-degree between those two zoom levels (about 4.9x), which suggests the offset is roughly constant in latitude (somewhere around 0.1° at this location) rather than constant in screen pixels. I haven't checked whether it varies with latitude or with the image's aspect ratio - happy to dig further if that would be useful.

Why I noticed / why it might matter for others

I'm drawing an interpolated field (station observations, not tiles) under some markers, and the markers and the raster underneath them are now visibly not quite aligned in the interactive view, even though they line up fine in a static export of the same figure. I've attached a screenshot from that real case too (plotly_issue_real_data_example.png) with the true bounds drawn in black for reference, in case it's useful to see this on something other than the synthetic example above.

Let me know if there's anything else I can provide, or if I've missed something obvious here - happy to help track it down further if useful.

Screenshots

Image Image

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions