Skip to content

Commit

Permalink
feat: Adding maps (#71)
Browse files Browse the repository at this point in the history
Closes #29, closes #28, closes #27, closes #26, and closes #21

Adds basic capabilities for all maps with the exception of choropleth
I originally was trying to include choropleths as well but they're a bit
more complicated and I already have spent a bit too long on this for the
moment. The other maps are basically free to implement.

There is a graphical issue with density mapbox that would be good for
@mattrunyon to look at when adding the defaults. The data is there but
not being drawn.
Here's some code to recreate the issue:
```
import deephaven.plot.express as dx
from deephaven import time_table
import random 
import pandas as pd
import deephaven.pandas as dhpd

df = pd.read_csv('https://raw.githubusercontent.com/plotly/datasets/master/earthquakes-23k.csv')

t = dhpd.to_table(df)

fig = dx.density_mapbox(t,  lat='Latitude', lon='Longitude', z='Magnitude', radius=10,
                        center=dict(lat=0, lon=180), zoom=0,
                        mapbox_style="stamen-terrain")
```
I see a ton of errors in the console, some along the line of "The layer
'plotly-trace-layer-ea2295-heatmap' does not exist in the map's style
and cannot be styled."

Additionally, geo plots have the snapping issue when dragging when new
data loads in that 3d plots had. Using the same imports:
```
sourceh = time_table("PT1S").update(formulas=[
    "X = (float) random.uniform(-90, 90)", 
    "Y = (float) random.uniform(-180, 180)", 
    "Z = (float)random.gauss(3, 3)",
    "l1 = i % 20",
    "l2 = i % 30",
    ])

def update(fig):
    fig.update_geos(showland=True, landcolor="gray")

figs = dx.scatter_geo(
    sourceh, 
    lat="X", 
    lon="Y", 
    by="l1", 
    size="Z",
    color_discrete_sequence=["salmon", "lemonchiffon"],
    projection="natural earth",
    unsafe_update_figure=update
)
```

---------

Co-authored-by: Matthew Runyon <[email protected]>
  • Loading branch information
jnumainville and mattrunyon authored Nov 6, 2023
1 parent f885ea0 commit 77507b9
Show file tree
Hide file tree
Showing 7 changed files with 1,003 additions and 4 deletions.
5 changes: 5 additions & 0 deletions plugins/plotly-express/src/deephaven/plot/express/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@
pie,
layer,
make_subplots,
scatter_geo,
scatter_mapbox,
density_mapbox,
line_geo,
line_mapbox,
)

from .data import data_generators
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@
# in other cases, the color would already be calculated so this argument
# would not be set
"color",
"lat",
"lon",
"locations",
}
DATA_ARGS.update(DATA_LIST_ARGS)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@
from .pie import pie
from ._layer import layer
from .subplots import make_subplots
from .maps import scatter_geo, scatter_mapbox, density_mapbox, line_geo, line_mapbox
734 changes: 734 additions & 0 deletions plugins/plotly-express/src/deephaven/plot/express/plots/maps.py

Large diffs are not rendered by default.

22 changes: 20 additions & 2 deletions plugins/plotly-express/src/js/src/PlotlyExpressChartModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,8 +193,26 @@ export class PlotlyExpressChartModel extends ChartModel {
}
}

has3D(): boolean {
return this.data.some(({ type }) => type != null && type.includes('3d'));
shouldPauseOnUserInteraction(): boolean {
return (
this.hasScene() || this.hasGeo() || this.hasMapbox() || this.hasPolar()
);
}

hasScene(): boolean {
return this.data.some(d => 'scene' in d && d.scene != null);
}

hasGeo(): boolean {
return this.data.some(d => 'geo' in d && d.geo != null);
}

hasMapbox(): boolean {
return this.data.some(({ type }) => type?.includes('mapbox'));
}

hasPolar(): boolean {
return this.data.some(({ type }) => type?.includes('polar'));
}

getPlotWidth(): number {
Expand Down
10 changes: 8 additions & 2 deletions plugins/plotly-express/src/js/src/PlotlyExpressChartPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,14 @@ function PlotlyExpressChartPanel(props: PlotlyExpressChartPanelProps) {
}, [dh, fetch]);

useEffect(
function handle3DTicks() {
if (!model || !containerRef.current || !model.has3D()) {
function handleSceneTicks() {
// Plotly scenes and geo views reset when our data ticks
// Pause rendering data updates when the user is manipulating a scene
if (
!model ||
!containerRef.current ||
!model.shouldPauseOnUserInteraction()
) {
return;
}

Expand Down
232 changes: 232 additions & 0 deletions plugins/plotly-express/test/deephaven/plot/express/plots/test_maps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
import unittest

from ..BaseTest import BaseTestCase


class MapTestCase(BaseTestCase):
def setUp(self) -> None:
from deephaven import new_table
from deephaven.column import int_col

self.source = new_table(
[
int_col("lat", [1, 2, 2, 3, 3, 3, 4, 4, 5]),
int_col("lon", [1, 2, 2, 3, 3, 3, 4, 4, 5]),
int_col("z", [1, 2, 2, 3, 3, 3, 4, 4, 5]),
]
)

def test_basic_scatter_geo(self):
import src.deephaven.plot.express as dx
from deephaven.constants import NULL_INT

chart = dx.scatter_geo(self.source, lat="lat", lon="lon").to_dict(self.exporter)
plotly, deephaven = chart["plotly"], chart["deephaven"]

# pop template as we currently do not modify it
plotly["layout"].pop("template")

expected_data = [
{
"featureidkey": "id",
"geo": "geo",
"hovertemplate": "lat=%{lat}<br>lon=%{lon}<extra></extra>",
"lat": [NULL_INT],
"legendgroup": "",
"lon": [NULL_INT],
"marker": {"color": "#636efa", "symbol": "circle"},
"mode": "markers",
"name": "",
"showlegend": False,
"type": "scattergeo",
}
]

self.assertEqual(plotly["data"], expected_data)

expected_layout = {
"geo": {"domain": {"x": [0.0, 1.0], "y": [0.0, 1.0]}, "fitbounds": False},
"legend": {"tracegroupgap": 0},
"margin": {"t": 60},
}

self.assertEqual(plotly["layout"], expected_layout)

def test_basic_scatter_mapbox(self):
import src.deephaven.plot.express as dx
from deephaven.constants import NULL_INT

chart = dx.scatter_mapbox(self.source, lat="lat", lon="lon").to_dict(
self.exporter
)
plotly, deephaven = chart["plotly"], chart["deephaven"]

# pop template as we currently do not modify it
plotly["layout"].pop("template")

expected_data = [
{
"hovertemplate": "lat=%{lat}<br>lon=%{lon}<extra></extra>",
"lat": [NULL_INT],
"legendgroup": "",
"lon": [NULL_INT],
"marker": {"color": "#636efa"},
"mode": "markers",
"name": "",
"showlegend": False,
"subplot": "mapbox",
"type": "scattermapbox",
}
]

self.assertEqual(plotly["data"], expected_data)

expected_layout = {
"legend": {"tracegroupgap": 0},
"mapbox": {
"center": {"lat": NULL_INT, "lon": NULL_INT},
"domain": {"x": [0.0, 1.0], "y": [0.0, 1.0]},
"style": "open-street-map",
"zoom": 8,
},
"margin": {"t": 60},
}

self.assertEqual(plotly["layout"], expected_layout)

def test_basic_line_geo(self):
import src.deephaven.plot.express as dx
from deephaven.constants import NULL_INT

chart = dx.line_geo(self.source, lat="lat", lon="lon").to_dict(self.exporter)
plotly, deephaven = chart["plotly"], chart["deephaven"]

# pop template as we currently do not modify it
plotly["layout"].pop("template")

expected_data = [
{
"featureidkey": "id",
"geo": "geo",
"hovertemplate": "lat=%{lat}<br>lon=%{lon}<extra></extra>",
"lat": [NULL_INT],
"legendgroup": "",
"line": {"color": "#636efa", "dash": "solid"},
"lon": [NULL_INT],
"marker": {"symbol": "circle"},
"mode": "lines",
"name": "",
"showlegend": False,
"type": "scattergeo",
}
]

self.assertEqual(plotly["data"], expected_data)

expected_layout = {
"geo": {"domain": {"x": [0.0, 1.0], "y": [0.0, 1.0]}, "fitbounds": False},
"legend": {"tracegroupgap": 0},
"margin": {"t": 60},
}

self.assertEqual(plotly["layout"], expected_layout)

def test_basic_line_mapbox(self):
import src.deephaven.plot.express as dx
from deephaven.constants import NULL_INT

chart = dx.line_mapbox(self.source, lat="lat", lon="lon").to_dict(self.exporter)
plotly, deephaven = chart["plotly"], chart["deephaven"]

# pop template as we currently do not modify it
plotly["layout"].pop("template")

expected_data = [
{
"hovertemplate": "lat=%{lat}<br>lon=%{lon}<extra></extra>",
"lat": [NULL_INT],
"legendgroup": "",
"line": {"color": "#636efa"},
"lon": [NULL_INT],
"mode": "lines",
"name": "",
"showlegend": False,
"subplot": "mapbox",
"type": "scattermapbox",
}
]

self.assertEqual(plotly["data"], expected_data)

expected_layout = {
"legend": {"tracegroupgap": 0},
"mapbox": {
"center": {"lat": NULL_INT, "lon": NULL_INT},
"domain": {"x": [0.0, 1.0], "y": [0.0, 1.0]},
"style": "open-street-map",
"zoom": 8,
},
"margin": {"t": 60},
}

self.assertEqual(plotly["layout"], expected_layout)

def test_basic_density_mapbox(self):
import src.deephaven.plot.express as dx
from deephaven.constants import NULL_INT

chart = dx.density_mapbox(self.source, lat="lat", lon="lon", z="z").to_dict(
self.exporter
)
plotly, deephaven = chart["plotly"], chart["deephaven"]

# pop template as we currently do not modify it
plotly["layout"].pop("template")

expected_data = [
{
"coloraxis": "coloraxis",
"hovertemplate": "lat=%{lat}<br>lon=%{lon}<br>z=%{z}<extra></extra>",
"lat": [NULL_INT],
"lon": [NULL_INT],
"name": "",
"radius": 30,
"subplot": "mapbox",
"z": [-2147483648],
"type": "densitymapbox",
}
]

self.assertEqual(plotly["data"], expected_data)

expected_layout = {
"coloraxis": {
"colorbar": {"title": {"text": "z"}},
"colorscale": [
[0.0, "#0d0887"],
[0.1111111111111111, "#46039f"],
[0.2222222222222222, "#7201a8"],
[0.3333333333333333, "#9c179e"],
[0.4444444444444444, "#bd3786"],
[0.5555555555555556, "#d8576b"],
[0.6666666666666666, "#ed7953"],
[0.7777777777777778, "#fb9f3a"],
[0.8888888888888888, "#fdca26"],
[1.0, "#f0f921"],
],
},
"legend": {"tracegroupgap": 0},
"mapbox": {
"center": {"lat": NULL_INT, "lon": NULL_INT},
"domain": {"x": [0.0, 1.0], "y": [0.0, 1.0]},
"style": "open-street-map",
"zoom": 8,
},
"margin": {"t": 60},
}

self.assertEqual(plotly["layout"], expected_layout)


if __name__ == "__main__":
unittest.main()

0 comments on commit 77507b9

Please sign in to comment.