justanotherguy
justanotherguy

Reputation: 83

Multiple opacities and widths in Mapbox - Plotly for Python

This is a question that derives from a previous one, which can be found here: Multiple opacities in Mapbox - Plotly for Python

I was having some issues regarding having multiple opacities for multiple traces on a Mapbox map, each opacity associated with a particular value of the dataframe.

After following Rob's helpful answer (see link above), which led to the solution on the first topic, I found myself in another problem, as I want now kind of twist on his code.

While my first approach was to only have variations on the opacity of the traces, I would now need to have both variations on the opacity and the width of each trace, corresponding each of them to a different value on a particular dataframe.

I proceed to copy-paste the first part of Rob's answer, where he declares some functions and creates a dataframe:

import requests
import geopandas as gpd
import plotly.graph_objects as go
import itertools
import numpy as np
import pandas as pd
from pathlib import Path

# get geometry of london underground stations
gdf = gpd.GeoDataFrame.from_features(
    requests.get(
        "https://raw.githubusercontent.com/oobrien/vis/master/tube/data/tfl_stations.json"
    ).json()
)

# limit to zone 1 and stations that have larger number of lines going through them
gdf = gdf.loc[gdf["zone"].isin(["1","2","3","4","5","6"]) & gdf["lines"].apply(len).gt(0)].reset_index(
    drop=True
).rename(columns={"id":"tfl_id", "name":"id"})

# wanna join all valid combinations of stations...
combis = np.array(list(itertools.combinations(gdf.index, 2)))

# generate dataframe of all combinations of stations
gdf_c = (
    gdf.loc[combis[:, 0], ["geometry", "id"]]
    .assign(right=combis[:, 1])
    .merge(gdf.loc[:, ["geometry", "id"]], left_on="right", right_index=True, suffixes=("_start_station","_end_station"))
)


gdf_c["lat_start_station"] = gdf_c["geometry_start_station"].apply(lambda g: g.y)
gdf_c["long_start_station"] = gdf_c["geometry_start_station"].apply(lambda g: g.x)
gdf_c["lat_end_station"] = gdf_c["geometry_end_station"].apply(lambda g: g.y)
gdf_c["long_end_station"] = gdf_c["geometry_end_station"].apply(lambda g: g.x)

gdf_c = gdf_c.drop(
    columns=[
        "geometry_start_station",
        "right",
        "geometry_end_station",
    ]
).assign(number_of_journeys=np.random.randint(1,10**5,len(gdf_c)))

gdf_c
f = Path.cwd().joinpath("SO.csv")
gdf_c.to_csv(f, index=False)

# there's an requirement to start with a CSV even though no sample data has been provided, now we're starting with a CSV
df = pd.read_csv(f)

# makes use of ravel simpler...
df["none"] = None

My particular problem starts here: when trying to create two "loops" (one for opacity, and one for width), I thought I could do the following:

BINS_FOR_OPACITY=10
opacity_a = np.geomspace(0.001,1, BINS_FOR_OPACITY)
BINS_FOR_WIDTH=10
width_a = np.geomspace(1,3, BINS_FOR_WIDTH)

fig = go.Figure()

# Note the double "for" statement that follows

for opacity, d in df.groupby(pd.cut(df["number_of_journeys"], bins=BINS_FOR_OPACITY, labels=opacity_a)):
    for width, d in df.groupby(pd.cut(df["number_of_journeys"], bins=BINS_FOR_WIDTH, labels=width_a)):
        fig.add_traces(
            go.Scattermapbox(
                name=f"{d['number_of_journeys'].mean():.2E}",
                lat=np.ravel(d.loc[:,[c for c in df.columns if "lat" in c or c=="none"]].values),
                lon=np.ravel(d.loc[:,[c for c in df.columns if "long" in c or c=="none"]].values),
                line_width=width
                line_color="blue",
                opacity=opacity,
                mode="lines+markers",
        )
    )

[Rob's original answer had only the first for statement, and not the second one]

However, the above is clearly not working, as it is making much more traces than it should do (I really can't explain why, but I guess it might be because of the double loop forced by the two for statements).

It ocurred to me that some kind of solution could be hidding in the pd.cut part, as I would need something like a double cut, but couldn't find a way to properly doing it.

I also managed to create a Pandas series by:

widths = pd.cut(df.["size"], bins=BINS_FOR_WIDTH, labels=width_a)

and iterating over that series, but got the same result as before (an excess of traces).

To emphasize and clarify myself, I don't need to have only multiple opacities or multiple widths, but I need to have them both and at the same time, which is what's causing me some troubles.

I am deeply grateful for any help

Upvotes: 0

Views: 112

Answers (1)

Rob Raymond
Rob Raymond

Reputation: 31226

  • given you are using same column to define width and opacity you only need one loop
  • for width I think linear space is better than geometric space, hence have created width_a as below
  • both opacity and width will move in step as it's cutting same column by same number of bins

solution excluding data sourcing

BINS = 10
opacity_a = np.geomspace(0.001, 1, BINS)
width_a = np.linspace(0.1, 2, BINS)
fig = go.Figure()
for (opacity, width), d in df.groupby(
    [
        pd.cut(df["number_of_journeys"], bins=BINS, labels=opacity_a),
        pd.cut(df["number_of_journeys"], bins=BINS, labels=width_a),
    ]
):
    fig.add_traces(
        go.Scattermapbox(
            name=f"{d['number_of_journeys'].mean():.2E}",
            lat=np.ravel(
                d.loc[:, [c for c in df.columns if "lat" in c or c == "none"]].values
            ),
            lon=np.ravel(
                d.loc[:, [c for c in df.columns if "long" in c or c == "none"]].values
            ),
            line_color="blue",
            line_width=width,
            opacity=opacity,
            mode="lines+markers",
        )
    )

fig.update_layout(
    mapbox={
        "style": "carto-positron",
        "center": {"lat": 51.520214996769255, "lon": -0.097792388774743},
        "zoom": 9,
    },
    margin={"l": 0, "r": 0, "t": 0, "b": 0},
)

Upvotes: 1

Related Questions