0

I had a geodataframe of gps points "poyline_gdf":

| geometry | 
| -------- | 
| POINT (7.30161 52.56024)    | 
| POINT (7.30007 52.55877)    | 
| ...     |

I wanted to make a linestring from these points and draw them on the map. For visualization, I used folium (found some tutorial). I draw the points by this code:

def add_markers(mapobj, gdf):
    coords = []
    for i, row in gdf.iterrows():
        coords.append([row.geometry.y, row.geometry.x])
    for coord in coords:
        folium.CircleMarker(location = coord,
                            radius = 2.5, 
                            fill = True,
                            fill_color = '#F50057',
                            fill_opacity = 0.75,
                            color = 'whitesmoke',
                            weight = 0.5).add_to(mapobj)
    return mapobj

f = folium.Figure(height = 400)
m = folium.Map([52.303333,8.02], zoom_start = 15, tiles='Cartodb dark_matter')
m.add_to(f)

add_markers(m, poyline_gdf)

Then, I found method to make linestrings from these points:

def make_lines(gdf, df_out, i, geometry = 'geometry'):
    geom0 = gdf.loc[i][geometry]
    geom1 = gdf.loc[i + 1][geometry]

    
    start, end = [(geom0.x, geom0.y), (geom1.x, geom1.y)]
    line = LineString([start, end])
    
    # Create a DataFrame to hold record
    data = {'id': i,
            'geometry': [line]}
    df_line = pd.DataFrame(data, columns = ['id', 'geometry'])
    
    # Add record DataFrame of compiled records
    df_out = pd.concat([df_out, df_line])
    return df_out

df = pd.DataFrame(columns = ['id', 'geometry'])

# Loop through each row of the input point GeoDataFrame
x = 1
while x < len(polyline_gdf) - 1:
    df = make_lines(polyline_gdf, df, x)
    x = x + 1
    
crs = {'init': 'epsg:4326'}
gdf_line = GeoDataFrame(df, crs=crs)
gdf_line  = gdf_line.reset_index().drop(['index','id'],axis=1)

And I got this geodataframe of Linestrings "gdf_line":

| geometry| 
| -------- | 
| LINESTRING (7.30007 52.55877, 7.29891 52.55521)   | 
| LINESTRING (7.29891 52.55521, 7.29502 52.55436)   | 
| ...   |

And I could show it on the map:

folium.GeoJson(gdf_line).add_to(m)
m

I got this line on the map: LineStrings on the map

I did some my calculations and added new column to dataframe - "coverage", which contains numeric values between 0 and 1. And now, my dataframe looks like this:

| geometry| coverage|
| -------- | ---------|
| LINESTRING (7.30007 52.55877, 7.29891 52.55521)   | 0.86|
| LINESTRING (7.29891 52.55521, 7.29502 52.55436)   | 0.32|
|...|...|

I want to make the same visualization of LineStrings as in picture above, but the colors of each LineString should change based on values in "coverage" column. For instance, if values are more than 0.5 closer to 1 (0.5-1), they will change from light blue to dark blue. And, if values are less than 0.5 closer to 0 (0.5-0), they will change from dark red to light red. Any help will be appreciated. If there is a code for another visualization library, that will also work for me.

3 Answers3

1
  • approach, generate a geopandas data frame of LINESTRINGs gdf2
  • simulate coverage on gdf2
  • use discrete color mapping for lines, use continuous color mapping for markers. Use px.line_mapbox() for lines, pd.scatter_mapbox() for markers
  • key technique is preparation of data. https://plotly.com/python/lines-on-mapbox/#lines-on-mapbox-maps-from-geopandas is used but refactored to utilise pandas techniques

sample data gdf2

geometry coverage
LINESTRING (6.186320428094177 49.46380280211451, 6.658229607783568 49.20195831969157) 0.506365
LINESTRING (6.658229607783568 49.20195831969157, 8.099278598674744 49.01778351500333) 0.692759
LINESTRING (7.593676385131062 48.33301911070372, 7.466759067422231 47.62058197691181) 0.0902211
LINESTRING (8.099278598674744 49.01778351500333, 7.593676385131062 48.33301911070372) 0.692394
LINESTRING (7.466759067422231 47.62058197691181, 7.192202182655507 47.44976552997102) 0.798098
LINESTRING (7.192202182655507 47.44976552997102, 6.736571079138059 47.54180125588285) 0.0847655
LINESTRING (6.736571079138059 47.54180125588285, 6.768713820023606 47.2877082383037) 0.17487
LINESTRING (6.768713820023606 47.2877082383037, 6.037388950229001 46.72577871356187) 0.438581
LINESTRING (6.037388950229001 46.72577871356187, 6.022609490593538 46.27298981382047) 0.157142
LINESTRING (6.022609490593538 46.27298981382047, 6.500099724970426 46.42967275652944) 0.207318

solution

import shapely.wkt
import shapely.geometry
import geopandas as gpd
import pandas as pd
import numpy as np
import plotly.express as px

# make sample data as describe in question... points with coverage
# just happens to be france boundary...
ls = shapely.wkt.loads(
    "LINESTRING (6.186320428094177 49.46380280211451, 6.658229607783568 49.20195831969157, 8.099278598674744 49.01778351500333, 7.593676385131062 48.33301911070372, 7.466759067422231 47.62058197691181, 7.192202182655507 47.44976552997102, 6.736571079138059 47.54180125588285, 6.768713820023606 47.2877082383037, 6.037388950229001 46.72577871356187, 6.022609490593538 46.27298981382047, 6.500099724970426 46.42967275652944, 6.843592970414505 45.99114655210061, 6.802355177445605 45.70857982032864, 7.096652459347837 45.33309886329589, 6.749955275101655 45.02851797136758, 7.007562290076635 44.25476675066136, 7.549596388386107 44.12790110938481, 7.435184767291872 43.69384491634922, 6.52924523278304 43.12889232031831, 4.556962517931424 43.3996509873116, 3.100410597352663 43.07520050716705, 2.985998976258458 42.47301504166986, 1.826793247087153 42.34338471126569, 0.7015906103638941 42.79573436133261, 0.3380469091905809 42.57954600683955, -1.502770961910528 43.03401439063043, -1.901351284177764 43.42280202897834, -1.384225226232985 44.02261037859012, -1.193797573237418 46.01491771095486, -2.225724249673846 47.06436269793822, -2.963276129559603 47.57032664650795, -4.491554938159481 47.95495433205637, -4.592349819344776 48.68416046812699, -3.295813971357802 48.90169240985963, -1.616510789384961 48.64442129169454, -1.933494025063311 49.77634186461574, -0.98946895995536 49.34737580016091, 1.338761020522696 50.12717316344526, 1.6390010921385 50.9466063502975, 2.513573032246143 51.14850617126183, 2.658422071960274 50.79684804951575, 3.123251580425688 50.78036326761455, 3.588184441755658 50.37899241800356, 4.286022983425084 49.90749664977255, 4.799221632515724 49.98537303323637, 5.674051954784829 49.5294835475575, 5.897759230176348 49.44266714130711, 6.186320428094177 49.46380280211451)"
)

# start with points...
gdf = gpd.GeoDataFrame(geometry=[shapely.geometry.Point(p) for p in ls.coords])

# linestrings, pairs or points, A to B, B to C, ....
gdf2 = pd.concat(
    [
        gdf.groupby(g).agg(
            {
                "geometry": lambda s: shapely.geometry.LineString(s.values),
            }
        )
        for g in [gdf.index // 2, np.roll((gdf.index // 2).values, 1)]
    ]
).sort_index().reset_index(drop=True)

# simulate coverage for a LINESTRING
gdf2["coverage"] = np.random.uniform(0,1, len(gdf2))

# prep data frame for plotting... sequences of co-ordinates delimited by None/NaN
# using: https://stackoverflow.com/questions/30885005/pandas-series-of-lists-to-one-series
df = pd.DataFrame(
    {
        col: gdf2.geometry.apply(lambda x: [p for p in x.xy[n]] + [None])
        .apply(pd.Series)
        .stack(dropna=False)
        .reset_index(drop=True)
        for n, col in enumerate(["lon", "lat"])
    }
).assign(
    coverage=np.repeat(gdf2["coverage"].values, 3), # assumes each linestring is two points
    coverage_bin=lambda d: pd.cut(
        d["coverage"], bins=[0, 0.25, 0.5, 0.75, 1], labels=False, include_lowest=True
    ),
)

# lines with discrete colors based of coverage_bin
figl = px.line_mapbox(
    df,
    lat="lat",
    lon="lon",
    color="coverage_bin",
    color_discrete_map={0: "red", 1: "palevioletred", 2: "skyblue", 3: "blue"},
    hover_data={"coverage":":.2f"}
).update_layout(
    mapbox={
        "style": "carto-positron",
        "zoom": 4,
    },
    margin={"l": 0, "r": 0, "t": 0, "r": 0},
)

# markers based on continuous value of coverage
figs = px.scatter_mapbox(
    df,
    lat="lat",
    lon="lon",
    color="coverage",
    color_continuous_scale=[
        (0, "red"),
        (0.33, "palevioletred"),
        (0.66, "skyblue"),
        (1, "blue"),
    ],
).update_layout(
    mapbox={
        "style": "carto-positron",
        "zoom": 4,
    },
    margin={"l": 0, "r": 0, "t": 0, "r": 0},
)

# bring lines and markers together
figs.add_traces(figl.data).update_layout(showlegend=False)


enter image description here

Rob Raymond
  • 29,118
  • 3
  • 14
  • 30
  • I've been trying to understand this line of code for quite a while, but it just doesn't make sense to me, even after rebuilding the statement and breaking it down into its component parts - I dont get it. What does GeoPandas with gdf.groupby(g). g is a list comprising the floor-divided indices of the dataframe. The function (intended to group the dataframe by columns) seems to build pairs of adjacent rows whereby it does not care about the actual values of the provided indices, e.g. no index references point 46, yet it creates lines starting and ending at point 46. – StephanH Jun 15 '23 at 12:37
0
  • your question doesn't really provide usable sample data. Have generated a set of POINTs from a LINESTRING (France boundary)
  • it's simple to achieve your mapping requirements with Plotly Express. The color requirements are really about creating a discrete values from a continuous series. For this I've used https://pandas.pydata.org/docs/reference/api/pandas.cut.html
  • generally IMHO it's simpler to create lines from a sequence of POINTs rather than a LINESTRING. I have effectively provided both ways in the two examples of plotting. LINESTRINGs are used as additional GEOJSON layers in second solution

sample data

import shapely.wkt
import shapely.geometry
import geopandas as gpd
import pandas as pd
import numpy as np
import plotly.express as px

# make sample data as describe in question... points with coverage
# just happens to be france boundary...
ls = shapely.wkt.loads(
    "LINESTRING (6.186320428094177 49.46380280211451, 6.658229607783568 49.20195831969157, 8.099278598674744 49.01778351500333, 7.593676385131062 48.33301911070372, 7.466759067422231 47.62058197691181, 7.192202182655507 47.44976552997102, 6.736571079138059 47.54180125588285, 6.768713820023606 47.2877082383037, 6.037388950229001 46.72577871356187, 6.022609490593538 46.27298981382047, 6.500099724970426 46.42967275652944, 6.843592970414505 45.99114655210061, 6.802355177445605 45.70857982032864, 7.096652459347837 45.33309886329589, 6.749955275101655 45.02851797136758, 7.007562290076635 44.25476675066136, 7.549596388386107 44.12790110938481, 7.435184767291872 43.69384491634922, 6.52924523278304 43.12889232031831, 4.556962517931424 43.3996509873116, 3.100410597352663 43.07520050716705, 2.985998976258458 42.47301504166986, 1.826793247087153 42.34338471126569, 0.7015906103638941 42.79573436133261, 0.3380469091905809 42.57954600683955, -1.502770961910528 43.03401439063043, -1.901351284177764 43.42280202897834, -1.384225226232985 44.02261037859012, -1.193797573237418 46.01491771095486, -2.225724249673846 47.06436269793822, -2.963276129559603 47.57032664650795, -4.491554938159481 47.95495433205637, -4.592349819344776 48.68416046812699, -3.295813971357802 48.90169240985963, -1.616510789384961 48.64442129169454, -1.933494025063311 49.77634186461574, -0.98946895995536 49.34737580016091, 1.338761020522696 50.12717316344526, 1.6390010921385 50.9466063502975, 2.513573032246143 51.14850617126183, 2.658422071960274 50.79684804951575, 3.123251580425688 50.78036326761455, 3.588184441755658 50.37899241800356, 4.286022983425084 49.90749664977255, 4.799221632515724 49.98537303323637, 5.674051954784829 49.5294835475575, 5.897759230176348 49.44266714130711, 6.186320428094177 49.46380280211451)"
)
gdf = gpd.GeoDataFrame(geometry=[shapely.geometry.Point(p) for p in ls.coords]).assign(
    coverage=lambda d: np.linspace(0, 1, len(d))
)
geometry coverage
5 POINT (7.192202182655507 47.44976552997102) 0.106383
22 POINT (1.826793247087153 42.34338471126569) 0.468085
0 POINT (6.186320428094177 49.46380280211451) 0
24 POINT (0.3380469091905809 42.57954600683955) 0.510638
25 POINT (-1.502770961910528 43.03401439063043) 0.531915
16 POINT (7.549596388386107 44.12790110938481) 0.340426
28 POINT (-1.193797573237418 46.01491771095486) 0.595745
13 POINT (7.096652459347837 45.33309886329589) 0.276596
40 POINT (2.658422071960274 50.79684804951575) 0.851064
34 POINT (-1.616510789384961 48.64442129169454) 0.723404

discrete colors

# discrete colors for markers and lines
px.line_mapbox(
    lat=gdf.geometry.y,
    lon=gdf.geometry.x,
    color=pd.cut(
        gdf["coverage"], bins=[0, 0.25, 0.5, 0.75, 1], labels=False, include_lowest=True
    ),
    color_discrete_map={0: "red", 1: "palevioletred", 2: "skyblue", 3: "blue"},
).update_traces(mode="lines+markers").update_layout(
    mapbox={
        "style": "carto-positron",
        "zoom": 4,
    },
    margin={"l": 0, "r": 0, "t": 0, "r": 0},
)

enter image description here

continuous colors for markers, discrete for lines

# aggregate to lines based on coverage...
gdf2 = gdf.groupby(
    pd.cut(
        gdf["coverage"], bins=[0, 0.25, 0.5, 0.75, 1], labels=False, include_lowest=True
    )
).agg({"geometry": lambda s: shapely.geometry.LineString(s.values)})

# continuous colors for markers and discrete for lines
px.scatter_mapbox(
    gdf,
    lat=gdf.geometry.y,
    lon=gdf.geometry.x,
    color="coverage",
    color_continuous_scale=[
        (0, "red"),
        (0.33, "palevioletred"),
        (0.66, "skyblue"),
        (1, "blue"),
    ],
    hover_data={"coverage":":.2f"}
).update_layout(
    mapbox={
        "style": "carto-positron",
        "zoom": 4,
        "layers": [
            {
                "source": gdf2.loc[i, "geometry"].__geo_interface__,
                "type": "line",
                "color": {0: "red", 1: "palevioletred", 2: "skyblue", 3: "blue"}[i],
            }
            for i in gdf2.index
        ],
    },
    margin={"l": 0, "r": 0, "t": 0, "r": 0},
)

enter image description here

Rob Raymond
  • 29,118
  • 3
  • 14
  • 30
  • Thanks for the answer. The thing that in my case points don't have coverage. Only last geodatafame has coverage. The one with LineStrings, so one LineString joins two Points and the coverage is only for this line. I tried to modify your code to LineStrings, I will show it below with the sample of data – Tarazali Ryskulov Nov 10 '21 at 05:18
  • I've reworked and added as a new answer. I will delete this answer. – Rob Raymond Nov 10 '21 at 12:32
0

I thought that if I put the whole LineString, instead of lat,lng, it might work:

px.line_mapbox(
    gdf['geometry'],
    color=pd.cut(
        gdf["coverage"], bins=[0, 0.25, 0.5, 0.75, 1], labels=False, include_lowest=True
    ),
    color_discrete_map={0: "red", 1: "palevioletred", 2: "skyblue", 3: "blue"},
).update_traces(mode="lines+markers").update_layout(
    mapbox={
        "style": "carto-positron",
        "zoom": 4,
    },
    margin={"l": 0, "r": 0, "t": 0, "r": 0},
)

Below, I want to show the dataframe which I am trying to plot: Dataframe-gdf