Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Serialization Error When Visualizing PyDeck TextLayer #5346

Closed
diehl opened this issue Jul 28, 2023 · 18 comments · Fixed by #5427
Closed

Serialization Error When Visualizing PyDeck TextLayer #5346

diehl opened this issue Jul 28, 2023 · 18 comments · Fixed by #5427
Labels
has workaround type: bug Something isn't correct or isn't working
Milestone

Comments

@diehl
Copy link

diehl commented Jul 28, 2023

ALL software version info

PyDeck 0.8, Panel 1.2

Description of expected behavior and the observed behavior

I recently coded up a variation of the PyDeck TextLayer example from the PyDeck website and then attempted to integrate that into a Panel application. Panel is having a serialization issue with the PyDeck String type.

Complete, minimal, self-contained example code that reproduces the issue

This Google Colab notebook shows the visualization running without issue in PyDeck and then shows my attempt to render that same visualization via Panel.

@MarcSkovMadsen
Copy link
Collaborator

MarcSkovMadsen commented Jul 29, 2023

I've proposed a solution on Discourse. Would like to know if it solves the issue?

https://discourse.holoviz.org/t/pydeck-textlayer-in-panel/5790/2

@diehl
Copy link
Author

diehl commented Jul 29, 2023

@MarcSkovMadsen Not yet. I replied on Discourse and shared a notebook for you to try on your end.

@diehl
Copy link
Author

diehl commented Aug 10, 2023

@MarcSkovMadsen FYI I just tried realizing a TextLayer with a JSON spec fed directly to Panel and that seems to work. This provides an interim workaround.

@MarcSkovMadsen
Copy link
Collaborator

Could you share a json spec that works?

@diehl
Copy link
Author

diehl commented Aug 10, 2023

@MarcSkovMadsen Here's an example.

json_spec = {
    "initialViewState": {
        "bearing": 0,
        "latitude": 37.7749,
        "longitude": -122.4194,
        "pitch": 0,
        "zoom": 10
    },
    "layers": [{
        "@@type": "TextLayer",
        "data": "https://raw.githubusercontent.com/visgl/deck.gl-data/master/website/bart-stations.json",
        "getText": "@@=name",
        "getPosition": "@@=coordinates",
        "getSize": 160,
        "getColor": [0,0,0],
        "getAngle": 0,
        "sizeUnits": "meters",
        "id": "8a553b25-ef3a-489c-bbe2-e102d18a3211",
    }],
    "mapStyle": "mapbox://styles/mapbox/light-v9",
    "views": [
        {"@@type": "MapView", "controller": True}
    ]
}

import panel as pn
pn.extension('deckgl')
deck_gl = pn.pane.DeckGL(json_spec, mapbox_api_key=MAPBOX_KEY, sizing_mode='stretch_width', height=600)
deck_gl.show()

@diehl
Copy link
Author

diehl commented Aug 10, 2023

I'm realizing now that the JSON spec approach doesn't allow me to move forward with what I want to build. Not easily anyhow when I'm dynamically updating a stack of layers based on user interaction. If there's some possible pathway to get over this serialization hump @MarcSkovMadsen, I'm all ears.

@MarcSkovMadsen
Copy link
Collaborator

Hi @diehl

You also put in https://gist.github.com/diehl/4a3af23416c2f5e6e65b3ed6bf00833f. Thanks. But its the same json as above. Does the json solve your problem?

@diehl
Copy link
Author

diehl commented Aug 23, 2023

Hi @MarcSkovMadsen,

What I'm trying to avoid is having to actively manage / update a JSON specification as opposed to working with PyDeck layer objects as I update the DeckGL visualization based on user interaction. Right now, I'm blocked from doing that when a TextLayer is in the mix due to the serialization error that comes up when you pass the Deck object to pn.pane.DeckGL. I think I can get to where I need to go with a JSON spec if that's the only option, but that would be a shame since that will dramatically increase the amount of work I need to do to realize updates to the viz.

@MarcSkovMadsen
Copy link
Collaborator

MarcSkovMadsen commented Aug 23, 2023

Ok the serialization issue I proposed seems to fix one issue.

I've found another issue. Currently you have to set the mapbox_key on the PyDeck object because if not the code below will set the mapbox_api_key to "".

image

A third issue is that currently you will have to set the min_zoom and max_zoom in the ViewState otherwise Deckgl in Panels implementation will be totally zoomed out and not allow you to zoom.

view_state = pdk.ViewState(latitude=37.7749295, longitude=-122.4194155, zoom=10, bearing=0, pitch=0, min_zoom=1, max_zoom=16)

Still finding the last issues.

@diehl
Copy link
Author

diehl commented Aug 23, 2023

@MarcSkovMadsen Awesome! I think I encountered that zoom issue when I was getting a world map with nothing visible on it. I had no ability to zoom in.

@MarcSkovMadsen
Copy link
Collaborator

The last thing to figure out (I believe) is why inlining the data in the TextLayer does not work with Panel

import pandas as pd
import panel as pn
import pydeck as pdk
from bokeh.core.serialization import Serializer
from pydeck.types import String

MAPBOX_KEY = (
    "pk.eyJ1IjoicGFuZWxvcmciLCJhIjoiY2s1enA3ejhyMWhmZjNobjM1NXhtbWRrMyJ9.B_frQsAVepGIe-HiOJeqvQ"
)
data = {
    "initialViewState": {
        "bearing": 0,
        "longitude": -122.418466,
        "latitude": 37.752254,
        "pitch": 0,
        "zoom": 14
    },
    "layers": [
        {
            "@@type": "TextLayer",
            "data": [
                        {
                            "name": "Lafayette (LAFY)",
                            "code": "LF",
                            "address": "3601 Deer Hill Road, Lafayette CA 94549",
                            "entries": 3481,
                            "exits": 3616,
                            "coordinates": [-122.123801, 37.893394]
                        },
                        {
                            "name": "12th St. Oakland City Center (12TH)",
                            "code": "12",
                            "address": "1245 Broadway, Oakland CA 94612",
                            "entries": 13418,
                            "exits": 13547,
                            "coordinates": [-122.271604, 37.803664]
                        },
                        {
                            "name": "16th St. Mission (16TH)",
                            "code": "16",
                            "address": "2000 Mission Street, San Francisco CA 94110",
                            "entries": 12409,
                            "exits": 12351,
                            "coordinates": [-122.419694, 37.765062]
                        },
                        {
                            "name": "19th St. Oakland (19TH)",
                            "code": "19",
                            "address": "1900 Broadway, Oakland CA 94612",
                            "entries": 13108,
                            "exits": 13090,
                            "coordinates": [-122.269029, 37.80787]
                        },
                        {
                            "name": "24th St. Mission (24TH)",
                            "code": "24",
                            "address": "2800 Mission Street, San Francisco CA 94110",
                            "entries": 12817,
                            "exits": 12529,
                            "coordinates": [-122.418466, 37.752254]
                        }
            ],
            "getText": "@@=name",
            "getPosition": "@@=coordinates",
            "getSize": 160,
            "getColor": [0, 0, 0],
            "getAngle": 0,
            "sizeUnits": "meters",
            "id": "8a553b25-ef3a-489c-bbe2-e102d18a3211"
        }
    ],
    "mapProvider": "carto",
    "mapStyle": "https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json",
    "views": [{"@@type": "MapView", "controller": True}]
}

# data["layers"][0]["data"]="https://raw.githubusercontent.com/visgl/deck.gl-data/master/website/bart-stations.json"

pn.extension("deckgl")
pn.pane.DeckGL(data, mapbox_api_key=MAPBOX_KEY, sizing_mode="stretch_both").servable()

image

If you don't inline by uncommenting

data["layers"][0]["data"]="https://raw.githubusercontent.com/visgl/deck.gl-data/master/website/bart-stations.json"

image

@MarcSkovMadsen
Copy link
Collaborator

I've tried using the inlined json specification at. And it works. So I believe the issue is on the Panel side and not deckgl side.

image

{
    "initialViewState": {
        "bearing": 0,
        "longitude": -122.418466,
        "latitude": 37.752254,
        "pitch": 0,
        "zoom": 14
    },
    "layers": [
        {
            "@@type": "TextLayer",
            "data": [
                        {
                            "name": "Lafayette (LAFY)",
                            "code": "LF",
                            "address": "3601 Deer Hill Road, Lafayette CA 94549",
                            "entries": 3481,
                            "exits": 3616,
                            "coordinates": [-122.123801, 37.893394]
                        },
                        {
                            "name": "12th St. Oakland City Center (12TH)",
                            "code": "12",
                            "address": "1245 Broadway, Oakland CA 94612",
                            "entries": 13418,
                            "exits": 13547,
                            "coordinates": [-122.271604, 37.803664]
                        },
                        {
                            "name": "16th St. Mission (16TH)",
                            "code": "16",
                            "address": "2000 Mission Street, San Francisco CA 94110",
                            "entries": 12409,
                            "exits": 12351,
                            "coordinates": [-122.419694, 37.765062]
                        },
                        {
                            "name": "19th St. Oakland (19TH)",
                            "code": "19",
                            "address": "1900 Broadway, Oakland CA 94612",
                            "entries": 13108,
                            "exits": 13090,
                            "coordinates": [-122.269029, 37.80787]
                        },
                        {
                            "name": "24th St. Mission (24TH)",
                            "code": "24",
                            "address": "2800 Mission Street, San Francisco CA 94110",
                            "entries": 12817,
                            "exits": 12529,
                            "coordinates": [-122.418466, 37.752254]
                        }
            ],
            "getText": "@@=name",
            "getPosition": "@@=coordinates",
            "getSize": 160,
            "getColor": [0, 0, 0],
            "getAngle": 0,
            "sizeUnits": "meters",
            "id": "8a553b25-ef3a-489c-bbe2-e102d18a3211"
        }
    ],
    "mapProvider": "carto",
    "mapStyle": "https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json",
    "views": [{"@@type": "MapView", "controller": true}]
}

@MarcSkovMadsen
Copy link
Collaborator

MarcSkovMadsen commented Aug 23, 2023

Inspecting what Panel deckgl.ts does, I can see it throws away one of the coordinates when the data is inlined.

image

@MarcSkovMadsen
Copy link
Collaborator

MarcSkovMadsen commented Aug 23, 2023

This is where the issue is

image

image

The issue is that Panel does not expect/ support array values in the data in the layers

@MarcSkovMadsen
Copy link
Collaborator

MarcSkovMadsen commented Aug 23, 2023

You can workaround this issue by specifying lat and long seperately.

import pandas as pd
import panel as pn
import pydeck as pdk
from bokeh.core.serialization import Serializer
from pydeck.types import String

MAPBOX_KEY = (
    "pk.eyJ1IjoicGFuZWxvcmciLCJhIjoiY2s1enA3ejhyMWhmZjNobjM1NXhtbWRrMyJ9.B_frQsAVepGIe-HiOJeqvQ"
)
data = {
    "initialViewState": {
        "bearing": 0,
        "longitude": -122.418466,
        "latitude": 37.752254,
        "pitch": 0,
        "zoom": 14
    },
    "layers": [
        {
            "@@type": "TextLayer",
            "data": [
                        {
                            "name": "24th St. Mission (24TH)",
                            "code": "24",
                            "address": "2800 Mission Street, San Francisco CA 94110",
                            "entries": 12817,
                            "exits": 12529,
                            "lat": -122.418466,
                            "lon": 37.752254
                        }
            ],
            "getText": "@@=name",
            "getPosition": "@@=[lat,lon]",
            "getSize": 160,
            "getColor": [0, 0, 0],
            "getAngle": 0,
            "sizeUnits": "meters",
            "id": "8a553b25-ef3a-489c-bbe2-e102d18a3211"
        }
    ],
    "mapProvider": "carto",
    "mapStyle": "https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json",
    "views": [{"@@type": "MapView", "controller": True}]
}

# data["layers"][0]["data"]="https://raw.githubusercontent.com/visgl/deck.gl-data/master/website/bart-stations.json"

pn.extension("deckgl")
pn.pane.DeckGL(data, mapbox_api_key=MAPBOX_KEY, sizing_mode="stretch_both").servable()

image

@diehl
Copy link
Author

diehl commented Aug 23, 2023

@MarcSkovMadsen Awesome progress!

I've got another example I've been experimenting with on my end where the Panel viz fails to come up as expected when the PyDeck Deck object is passed to pn.pane.DeckGL but it almost works when I extract the JSON spec from the Deck object and pass that to pn.pane.DeckGL. What is missing is the Mapbox basemap which is probably explained by the issue you found above regarding the API key.

vtiles = pdk.Deck(
    initial_view_state=INITIAL_VIEW_STATE,
    views=[view],
    layers=[geojson_layer,marker_layer,text_layer],
    map_provider='mapbox',
    map_style='mapbox://styles/diehl/cliwf0hy500m401pw9qxbeac3',
)

import json
spec = json.loads(vtiles.to_json())
spec["mapProvider"] = 'mapbox'
spec["mapStyle"] = 'mapbox://styles/diehl/cliwf0hy500m401pw9qxbeac3'

map = pn.pane.DeckGL(spec,  mapbox_api_key=ACCESS_TOKEN, sizing_mode='stretch_both')
pn.Row(map.controls(),map).show()

And sure enough, your code above held the answer. By inserting the API key into the pn.pane.DeckGL call, the layer is now appearing! So we've got an interim workaround with this added step of dumping the JSON spec to pass to Panel.

@MarcSkovMadsen
Copy link
Collaborator

MarcSkovMadsen commented Aug 23, 2023

Workaround

This example will workaround the issues with 1) mapbox key 2) min/ max zoom and 3) no support for array columns.

image

I've marked the points with # Workaround in the code below.

import pandas as pd
import pydeck as pdk

from bokeh.core.serialization import Serializer
from pydeck.types import String


def pydeck_string_encoder(obj, serializer):
    return obj.value


Serializer._encoders[String] = pydeck_string_encoder

TEXT_LAYER_DATA = "https://raw.githubusercontent.com/visgl/deck.gl-data/master/website/bart-stations.json"  # noqa
MAPBOX_KEY = "pk.eyJ1IjoicGFuZWxvcmciLCJhIjoiY2s1enA3ejhyMWhmZjNobjM1NXhtbWRrMyJ9.B_frQsAVepGIe-HiOJeqvQ"

df = pd.read_json(TEXT_LAYER_DATA)

# Workaround
lat_lon =  pd.DataFrame(df["coordinates"].to_list(), columns=["lat", "lon"])
del df["coordinates"]
df = pd.concat([df, lat_lon], axis=1)

# Define a layer to display on a map
layer = pdk.Layer(
    "TextLayer",
    df,
    pickable=True,
    get_position="[lat,lon]", # Workaround
    get_text="name",
    get_size=160,
    get_color=[0, 0, 0],
    get_angle=0,
    get_text_anchor=String("middle"),
    get_alignment_baseline=String("center"),
    size_units=String("meters"),
    api_keys={"mapbox": MAPBOX_KEY},  # Workaround
)

# Set the viewport location
view_state = pdk.ViewState(
    latitude=37.7749295,
    longitude=-122.4194155,
    zoom=11,
    bearing=0,
    pitch=0,
    min_zoom=0,  # Workaround
    max_zoom=16,  # Workaround
)

# Render
r = pdk.Deck(
    layers=[layer],
    initial_view_state=view_state,
    tooltip={"text": "{name}\n{address}"},
    map_style=pdk.map_styles.ROAD,
)

import panel as pn

pn.extension("deckgl")
pn.pane.DeckGL(
    r, mapbox_api_key=MAPBOX_KEY, sizing_mode="stretch_both"
).servable()

@MarcSkovMadsen MarcSkovMadsen added has workaround type: bug Something isn't correct or isn't working labels Aug 23, 2023
@MarcSkovMadsen MarcSkovMadsen added this to the next milestone Aug 23, 2023
@diehl
Copy link
Author

diehl commented Aug 23, 2023

@MarcSkovMadsen Awesome! Just added the serialization code to my example and now the JSON spec step is not needed. Thank you sir for this workaround! Do you plan to roll the serialization code into Panel somehow so that it will not be necessary in the future when running code such as the above?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
has workaround type: bug Something isn't correct or isn't working
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants