Scaling Markers with Zoom in Plotly's Scattermapbox - plotly-python

Does anyone know if it is possible to specify a mapping that varies the size of markers in a Plotly scattermapbox visualization as one varies the zoom level? I'd like to layer a scattermapbox visualization over a densitymapbox visualization and have the scatter plot be invisible at larger scales but come into view as one zooms in.
Thanks!

you can specify minzoom on layers
below example shows a density mapbox that are replaced by red markers after zooming in past zoom 4
this clearly works where markers and density items are the same. If different, best that you update question with sample data
import plotly.express as px
import pandas as pd
import geopandas as gpd
import shapely.geometry
import json
df = pd.DataFrame(
data=(
[
[32.4087249155, -100.9509696428, "2013-01-01", 1],
[31.5201976084, -102.1030942593, "2013-01-01", 1],
[31.434573418, -102.0592907601, "2013-01-01", 1],
[31.2635930582, -101.95341361, "2013-01-01", 1],
[31.4287233847, -102.0253840388, "2013-01-01", 1],
[31.4872286706, -101.5455598032, "2021-01-01", 1],
[31.5439162579, -101.4833865708, "2021-01-01", 1],
[31.5439362581, -101.4833065695, "2021-01-01", 1],
[31.7980713977, -102.0937650441, "2021-01-01", 1],
[32.02050082, -103.31736372, "2021-01-01", 1],
]
),
columns=["Latitude", "Longitude", "Date", "Count"],
)
fig = px.density_mapbox(
df,
lat="Latitude",
lon="Longitude",
z="Count",
radius=10,
zoom=3,
)
# fig = go.Figure(go.Scattermapbox())
fig.update_layout(
mapbox_layers=[
{
# "below": "traces",
"circle": {"radius": 10},
"color":"red",
"minzoom": 4,
"source": gpd.GeoSeries(
df.loc[:, ["Longitude", "Latitude"]].apply(
shapely.geometry.Point, axis=1
)
).__geo_interface__,
},
],
mapbox_style="carto-positron",
)

Related

create legend for markercolor and size

I've created the following figure:
With following code:
matplotlib.rcParams.update({'font.size': 10})
fig = plt.figure(figsize=(16, 9), dpi=300, facecolor='white')
ax = plt.subplot(111, projection=ccrs.PlateCarree())
ax.set_extent(extent)
# cartopy layers
country_10m = cartopy.feature.NaturalEarthFeature('cultural', 'admin_0_countries', '10m')
ax.add_feature(country_10m, edgecolor='w', linewidth=0.75, facecolor='#EEEFEE', label='country border')
ax.coastlines(resolution='10m', color='#EEEFEE', linewidth=0.75)
ax.imshow(np.tile(np.array([[[191, 210, 217]]], dtype=np.uint8), [2, 2, 1]), origin='lower', transform=cartopy.crs.PlateCarree(), extent=extent)
ax.scatter(gdf_ldb.x, gdf_ldb.y, c= gdf_ldb.Color, s= gdf_ldb.Markersize, zorder=30)
# ax.scatter(gdf_ports_filt.longitude, gdf_ports_filt.latitude, s= 10, color= 'k', zorder= 30)
ax.gridlines(crs=ccrs.PlateCarree(), draw_labels=True, linewidth=1, color='gray', alpha=0.5, linestyle='-')
ax.text(-0.08, 0.5, 'latitude [°]', va='bottom', ha='center',rotation='vertical', rotation_mode='anchor',transform=ax.transAxes);
ax.text(0.5, -0.09, 'longitude [°]', va='bottom', ha='center', rotation='horizontal', rotation_mode='anchor', transform=ax.transAxes);
How do I create a legend for the markersize as well for the color, so like this:
With x, x1, and x2 representing the values of the markersizes.
gdf_ldb looks like:
x y Type Color Markersize geometry
prograding_feature_polygon_29 12.857701 56.648035 Updrift grey 3.0 POINT (12.85770 56.64804)
prograding_feature_polygon_57 17.781445 54.808079 Updrift grey 3.0 POINT (17.78144 54.80808)
prograding_feature_polygon_58 17.438390 54.754518 Updrift grey 3.0 POINT (17.43839 54.75452)
prograding_feature_polygon_63 4.708077 52.880322 Updrift grey 3.0 POINT (4.70808 52.88032)
prograding_feature_polygon_72 3.953364 51.842299 Updrift grey 3.0 POINT (3.95336 51.84230)
... ... ... ... ... ... ...
retreating_feature_polygon_2018 -10.148432 53.415224 Double Updrift grey 3.0 POINT (-10.14843 53.41522)
retreating_feature_polygon_2019 -9.954510 54.197329 Double Updrift grey 3.0 POINT (-9.95451 54.19733)
retreating_feature_polygon_2119 15.095564 37.389535 Double Updrift grey 3.0 POINT (15.09556 37.38953)
retreating_feature_polygon_2120 14.317893 37.025026 Double Updrift grey 3.0 POINT (14.31789 37.02503)
retreating_feature_polygon_2121 13.952111 37.101009 Updrift grey 3.0 POINT (13.95211 37.10101)
Thanks in advance,
Dante
The key is to capture the artist (PathCollection in this case) returned by the scatter command. That has a method to retrieve the legend items manually, and it has keywords to distinguish between size and color (default). The num keyword can be used to reduce the amount of items returned, which is useful in the case of a (semi)continuous property as the size can be.
The example below plots two separate legends for both properties. You can also combine the handles and labels of both and plot them in a single legend if needed.
import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle
import cartopy.crs as ccrs
import cartopy
import numpy as np
lons = np.random.randint(-170, 170, 100)
lats = np.random.randint(-80, 80, 100)
sizes = np.random.rand(100) * 100 + 5
colors = np.random.randint(0, 3, 100)
fig, ax = plt.subplots(
figsize=(8,4), dpi=86, facecolor='w',
subplot_kw=dict(projection=ccrs.PlateCarree()),
)
fig.subplots_adjust(left=0, right=1, bottom=0, top=1)
ax.add_feature(cartopy.feature.LAND, ec='none', fc='#EEEFEE', label='country border')
ax.add_feature(cartopy.feature.BORDERS, ec='w', fc='k', lw=0.75, label='country border')
ax.coastlines(resolution='10m', color='#EEEFEE', lw=0.75)
m = ax.scatter(lons, lats, s=sizes, c=colors, zorder=5, label="points")
l1 = ax.legend(
*m.legend_elements(prop="colors", num="auto"), title="Colors", framealpha=1,
loc="upper right", bbox_to_anchor=(0.88, 0.8, 0.12, 0.2), mode="expand",
)
ax.add_artist(l1) # prevent overwriting with second legend
l2 = ax.legend(
*m.legend_elements(prop="sizes", num=5), title="Sizes", framealpha=1,
loc="upper right", bbox_to_anchor=(0.88, 0.55, 0.12, 0.2), mode="expand",
)
The documentation about this shows some variations on this:
https://matplotlib.org/stable/gallery/lines_bars_and_markers/scatter_with_legend.html#automated-legend-creation
The answer by Rutger Kassies is excellent for many use cases. However, he mentions that One can also combine the handles and labels of both and plot them in a single legend if needed.
Here I offer another answer that shows the steps to create the single legend manually. Inside the single legend, 2 groups of sub legends are created and arranged as needed.
With single legend, you don't need to find the values of bbox_to_anchor for the second (or third and so on) to position them properly.
With manual creation of items into a single legend, you have full control of the items' you need in the legend. However, it need some extra coding to achieve the goal.
import matplotlib.pyplot as plt
import matplotlib.lines as mlines
import cartopy.crs as ccrs
import cartopy.feature as cfeature
# For `Categories` symbol
# Each item of legends requires 3 properties: color/text/marker_shape
color_V = ["green", "orange", "purple", "red", "cyan", "magenta"]
text_V = ["cat_4", "cat_9", "cat_13", "cat_15", "cat_19", "cat_33"]
marker_V = ["o", "o", "o", "o", "o", "o"]
len_V = len(color_V)
# For `Size/values` symbol
color_S = ["gray", "gray", "gray", "gray"]
sizes_S = [4, 8, 12, 16] #increasing values ...
text_S = ["4", "8", "12", "16"] #cover `sizes1` below
marker_S = ["o", "o", "o", "o"] #use disk shape
len_S = len(color_S)
# Demo data locations and attributes
xs = [23,12,4,25,24,52,17,33]
ys = [41,12,32,15,35,21,23,43]
colors1 = ["green", "orange", "purple", "red", "cyan", "magenta", "green", "orange"]
#texts1 = ["4", "9", "13", "15", "19", "33", "4", "9"]
markers1 = ["o", "o", "o", "o", "o", "o", "o", "o"]
sizes1 = [10,16,9,12,7,4,2,6]
len1 = len(xs)
all_patches = [] #for items in a single legend
# Create figure and `ax` for map plotting
# This form can create a single axes or an array of axes
fig, ax = plt.subplots(nrows=1, ncols=1, figsize=(8,6), subplot_kw={'projection': ccrs.PlateCarree()})
# All steps of plots will be done on `ax`
# [1] Add an invisible object as a spacer in the legend box
#rect = mpatches.Rectangle([0, 0], 0.01, 0.01, ec="none", color="lightgray")
all_patches.append(mlines.Line2D([0, 0], [1, 0], color="none"))
# Explicitly defining the elements in the legend
# [2] Add proxied text: 'Categories' to the legend
line = mlines.Line2D([0, 0], [1, 0], lw=.5, alpha=0.9, color="none")
line.set_label('Categories') # Title for 1st group of symbols in the legend
all_patches.append(line)
# [3] Plot (on the axes) `none` data point and
# save the output patches for `Categories` group
patches_V = [ ax.plot([],[], marker=marker_V[i], ms=8, ls="", color=color_V[i], \
label="{:s}".format(text_V[i]) )[0] \
for i in range(len_V) ]
all_patches += patches_V
# [4] Add an invisible object as a spacer in the legend box
all_patches.append(mlines.Line2D([0, 0], [1, 0], color="none"))
# [5] Add proxied text: 'Sizes' to the legend
x, y = ([0, 1], [0, 0])
line = mlines.Line2D([0, 0], [1, 0], lw=.5, alpha=0.9, color="none")
line.set_label('Sizes') # Title for 2nd group of symbols in the legend
all_patches.append(line)
# [6] Create patches for `Sizes` group
patches_S = [ ax.plot([],[], marker=marker_S[i], ms=sizes_S[i], ls="", \
color=color_S[i], \
label="{:s}".format(text_S[i]) )[0] for i in range(len_S) ]
all_patches += patches_S
# Plot point data using the demo data
for i in range(len1):
ax.plot(xs[i], ys[i], marker=markers1[i], ms=sizes1[i], color=colors1[i])
ax.set_extent([0, 80, 0, 60])
# Plot the legend in the upper-right corner
combined_legend = ax.legend(handles=all_patches,
bbox_to_anchor=(1, 1),
title="The Legend",
loc='upper right',
ncol=1,
numpoints=1,
facecolor="lightgray",
fontsize = 10,
title_fontsize= 12,
labelspacing = 0.55,
shadow=True)
# Draw some basemap features
ax.coastlines(lw=0.3, color="k")
ax.add_feature(cfeature.LAND)
ax.add_feature(cfeature.OCEAN)
plt.title("Legend for Categories and Sizes")
plt.show()
The output map:

How to plot a time series with ordinal levels in Plotly Python

I have time series data recorded at discrete ordinal levels (e.g. 0, 1, 2), and I'd like to plot them with meaningful names (e.g. low, medium, high).
Currently I have:
import pandas as pd
import plotly.express as px
df = pd.DataFrame({
"x": ["2022-01-01", "2022-01-02", "2022-01-03", "2022-01-04"],
"y": [2, 1, 2, 0],
})
fig = px.line(x=df.x, y=df.y, line_shape="hv")
fig.show()
which produces:
But I'd like something like:
This feels like the easiest way:
import pandas as pd
import plotly.express as px
df = pd.DataFrame({
"x": ["2022-01-01", "2022-01-02", "2022-01-03", "2022-01-04"],
"y": [2, 1, 2, 0],
})
fig = px.line(x=df.x, y=df.y, line_shape="hv")
fig.update_yaxes(
ticktext=["Low", "Medium", "High"],
tickvals=[0, 1, 2],
)
fig.show()
Result:
In Plotly language this falls under the "categorical" umbrella.
If the order needs tweaked, the categoryarray and categoryorder can also be set with update_yaxes.
https://plotly.com/python/reference/layout/yaxis/#layout-yaxis-categoryarray
https://plotly.com/python/reference/layout/yaxis/#layout-yaxis-categoryorder

Align bar and line plot on x axis without the use of rank and pointplot

Please note, I've looked at other questions like question and my problem is different and not a duplicate!
I would like to have two plots, with the same x axis in matplotlib. I thought this should be achieved via constrained_layout, but apparently this is not the case. Here is an example code.
import pandas as pd
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt
import matplotlib.gridspec as grd
x = np.arange(0, 30, 0.001)
df_line = pd.DataFrame({"x": x, "y": np.sin(x)})
df_bar = pd.DataFrame({
"x_bar": [1, 7, 10, 20, 30],
"y_bar": [0.0, 0.3, 0.4, 0.1, 0.2]
})
fig = plt.subplots(constrained_layout=True)
gs = grd.GridSpec(2, 1, height_ratios=[3, 2], wspace=0.1)
ax1 = plt.subplot(gs[0])
sns.lineplot(data=df_line, x=df_line["x"], y=df_line["y"], ax=ax1)
ax1.set_xlabel("time", fontsize="22")
ax1.set_ylabel("y values", fontsize="22")
plt.yticks(fontsize=16)
plt.xticks(fontsize=16)
plt.setp(ax1.get_legend().get_texts(), fontsize="22")
ax2 = plt.subplot(gs[1])
sns.barplot(data=df_bar, x="x_bar", y="y_bar", ax=ax2)
ax2.set_xlabel("time", fontsize="22")
ax2.set_ylabel("y values", fontsize="22")
plt.yticks(fontsize=16)
plt.xticks(fontsize=16)
this leads to the following figure.
However, I would like to see the corresponding x values of both plot aligned. How can I achieve this? Note, I've tried to use the following related question. However, this doesn't fully apply to my situation. First with the high number of x points (which I need in reality) point plots is make the picture to big and slow for loading. On top, I can't use the rank method as my categories for the barplot are not evenly distributed. They are specific points on the x axis which should be aligned with the corresponding point on the lineplot
x = np.arange(0, 30, 0.001)
df_line = pd.DataFrame({"x": x, "y": np.sin(x)})
df_bar = pd.DataFrame({
"x_bar": [1, 7, 10, 20, 30],
"y_bar": [0.0, 0.3, 0.4, 0.1, 0.2]
})
fig, (ax1, ax2) = plt.subplots(2,1)
ax1.plot(df_line['x'], df_line['y'])
for i in range(len(df_bar['x_bar'])):
ax2.axvline(x=df_bar['x_bar'][i], ymin=0, ymax=df_bar['y_bar'][i])
Output:
---edit---
I incorporated #mozway advice for linewidth:
lw = (300/ax1.get_xlim()[1])
ax2.axvline(x=df_bar['x_bar'][i], ymin=0, ymax=df_bar['y_bar'][i], solid_capstyle='butt', lw=lw)
Output:
or:

Customizing legend with scatterplot

I struggle with customizing the legend of my scatterplot. Here is a snapshot :
And here is a code sample :
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
sns.set()
my_df = pd.DataFrame([[5, 3, 1], [2, 1, 2], [3, 4, 1], [1, 2, 1]],
columns=["DUMMY_CT", "FOO_CT", "CI_CT"])
g = sns.scatterplot("DUMMY_CT", "FOO_CT", data=my_df, size="CI_CT")
g.set_title("Number of Baz", weight="bold")
g.set_xlabel("Dummy count")
g.set_ylabel("Foo count")
g.get_legend().set_title("Baz count")
Also, I work in a Jupyter-lab notebook with Python 3, if it helps.
The red thingy issue
First things first, I wish to hide the name of the CI_CT variable (contoured in red on the picture). After exploring the whole documentation for this afternoon, I found the get_legend_handlers_label method (see here), which produces the following :
>>> g.get_legend_handles_labels()
([<matplotlib.collections.PathCollection at 0xfaaba4a8>,
<matplotlib.collections.PathCollection at 0xfaa3ff28>,
<matplotlib.collections.PathCollection at 0xfaa3f6a0>,
<matplotlib.collections.PathCollection at 0xfaa3fe48>],
['CI_CT', '0', '1', '2'])
Where I can spot my dear CI_CT string. However, I'm unable to change this name or to hide it completely. I found a dirty way, that basically consists in not using efficiently the dataframe passed as a data parameter. Here is the scatterplot call :
g = sns.scatterplot("DUMMY_CT", "FOO_CT", data=my_df, size=my_df["CI_CT"].values)
Result here :
It works, but is there a cleaner way to achieve this?
The green thingy issue
Displaying a 0 level in this legend is incorrect, since there is no zero value in the column CI_CT of my_df. It is therefore misleading for the readers, who might assume the smaller dots represents a value of 0 or 1. I wish to setup a defined scale, in the way one can do it for the x and y axis. However, I cannot achieve it. Any idea?
TL;DR : A broader question that could solve everything
Those adventures make me wonder if there is a way to handle the data you can pass to the scatterplots with hue and size parameters in a clean, x-and-y-axis way. Is it actually possible?
Please pardon my English, please let me know if the question is too broad or uncorrectly labelled.
The "green thing issue", namely that there is one more legend entry than there are sizes, is solved by specifying legend="full".
g = sns.scatterplot(..., legend="full")
The "red thing issue" is more tricky. The problem here is that seaborn misuses a normal legend label as a headline for the legend. An option is indeed to supply the values directly instead of the name of the column, to prevent seaborn from using that column name.
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
sns.set()
my_df = pd.DataFrame([[5, 3, 1], [2, 1, 2], [3, 4, 1], [1, 2, 1]],
columns=["DUMMY_CT", "FOO_CT", "CI_CT"])
g = sns.scatterplot("DUMMY_CT", "FOO_CT", data=my_df, size=my_df["CI_CT"].values, legend="full")
g.set_title("Number of Baz", weight="bold")
g.set_xlabel("Dummy count")
g.set_ylabel("Foo count")
g.get_legend().set_title("Baz count")
plt.show()
If you really must use the column name itself, a hacky solution is to crawl into the legend and remove the label you don't want.
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
sns.set()
my_df = pd.DataFrame([[5, 3, 1], [2, 1, 2], [3, 4, 1], [1, 2, 1]],
columns=["DUMMY_CT", "FOO_CT", "CI_CT"])
g = sns.scatterplot("DUMMY_CT", "FOO_CT", data=my_df, size="CI_CT", legend="full")
g.set_title("Number of Baz", weight="bold")
g.set_xlabel("Dummy count")
g.set_ylabel("Foo count")
g.get_legend().set_title("Baz count")
#Hack to remove the first legend entry (which is the undesired title)
vpacker = g.get_legend()._legend_handle_box.get_children()[0]
vpacker._children = vpacker.get_children()[1:]
plt.show()
I finally managed to get the result I wish, but the ugly way. It might be useful to someone, but I would not advise to do this.
The solution to fix the scale into the legend consists of moving all the CI_CT column values to the negatives (to keep the order and the consistency of markers size). Then, the values displayed in the legend are corrected accordingly to the previous data changes (inspiration from here).
However, I did not find any better way to make the "CI_CT" text desapear in the legend without leaving an atrociously huge blank space.
Here is the sample of code and the result.
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
sns.set()
my_df = pd.DataFrame([[5, 3, 1], [2, 1, 2], [3, 4, 1], [1, 2, 1]], columns=["DUMMY_CT", "FOO_CT", "CI_CT"])
# Substracting the maximal value of CI_CT for each value
max_val = my_df["CI_CT"].agg("max")
my_df["CI_CT"] = my_df.apply(lambda x : x["CI_CT"] - max_val, axis=1)
# scatterplot declaration
g = sns.scatterplot("DUMMY_CT", "FOO_CT", data=my_df, size=my_df["CI_CT"].values)
g.set_title("Number of Baz", weight="bold")
g.set_xlabel("Dummy count")
g.set_ylabel("Foo count")
g.get_legend().set_title("Baz count")
# Correcting legend values
l = g.legend_
for t in l.texts :
t.set_text(int(t.get_text()) + max_val)
# Restoring the DF
my_df["CI_CT"] = my_df.apply(lambda x : x["CI_CT"] + max_val, axis=1)
I'm still looking for a better way to achieve this.

matplotlib collection linewidth mapping?

I'm creating some GIS-style plots in matplotlib of road networks and the like, so I'm using LineCollection to store and represent all of the roads and color accordingly. This is working fine, I color the roads based on a criteria and the following map:
from matplotlib.colors import ListedColormap,BoundaryNorm
from matplotlib.collections import LineCollection
cmap = ListedColormap(['grey','blue','green','yellow','orange','red','black'])
norm = BoundaryNorm([0,0.5,0.75,0.9,0.95,1.0,1.5,100],cmap.N)
roads = LineCollection(road_segments, array=ratios, cmap=cmap, norm=norm)
axes.add_collection(roads)
This works fine, however I would really like to have linewidths defined in a similar manner to the color map - ranging from 0.5 to 5 for each color
Does anyone know of a clever way of doing this?
The linewidths keyword.
import matplotlib.pyplot as plt
from matplotlib.collections import LineCollection
axes = plt.axes()
roads = LineCollection([
[[0, 0], [1, 1]],
[[0, 1], [1, 0]]
],
colors=['black', 'red'],
linewidths=[3, 8],
)
axes.add_collection(roads)
plt.show()
HTH