Basics polyline color change problem of dash_leaflet

Hi Everone!

I started learning dash a week ago, and I think it is the most highest-level of data analysing. I make a geospatial map application in dash with dash leaflet. But my simple problem is that I cant change the color of polyline with a dropdown or any other components. Did anyone find the same problem? Is there any solution or may I do something wrong? There is a simple example.:

import dash
from dash import dcc, html, callback
from dash.dependencies import Input, Output
import dash_leaflet as dl

import dash
from dash import dcc, html, callback
from dash.dependencies import Input, Output
import dash_leaflet as dl


app = dash.Dash(__name__)


antpath = dl.Polyline(
    id='my-polyline',
    positions=[[51.508, -0.11], [51.503, -0.06], [51.51, -0.047]],
    color="red"
)


app.layout = html.Div(
    children=[

        html.Div(
            children=[
                dcc.Dropdown(
                    id='color-dropdown',
                    options=[
                        {'label': 'Blue', 'value': '#0000FF'},
                        {'label': 'Green', 'value': '#008000'},
                        {'label': 'Red', 'value': '#FF0000'},
                        {'label': 'Yellow', 'value': '#FFFF00'}
                    ],
                    value='#0000FF',  
                    style={'width': '200px'}
                ),
            ],
            style={'padding': '10px'}
        ),
        
    
        html.Div(
            id='map-container',
            children=[
                dl.Map(
                    id='map',
                    center=[51,0],
                    zoom=7,
                    style={'height': '100vh', 'width': '100%', 'background':'lightblue'},  
                    zoomControl=False, 
                    children=[
                        dl.TileLayer(
                            url='https://tiles.stadiamaps.com/tiles/osm_bright/{z}/{x}/{y}{r}.png', 
                            maxZoom=20, 
                            attribution='&copy; <a href="https://stadiamaps.com/">Stadia Maps</a>',
                        ),
                        antpath
                    ]
                ),
            ],
            style={'width': '100%', 'height': '100vh'}
        ),
    ]
)


@app.callback( #There is no update. 
    Output('my-polyline', 'color'),
    [Input('color-dropdown', 'value')]
)
def update_color(selected_color):
    return selected_color

if __name__ == '__main__':
    app.run_server(debug=True)

I’d look at:

I forked leaflet beacuse of some similar issues I had in development.

think in the public released leaflet you’ll need to update the id=map children and not the Output('my-polyline', 'color')

Yeah, you are the guy, who made the great Youtube videos!! I hoped that you would reply me. Thanks for the vids and super website, that helping me a lot at the beginning.
After all, I modified my code about your advice, but it also does not work. Althrough your antpath change color method is good. The not elegant way is I 'm using your antpath instead of polylines. Other ways: make a folium map and reload it every time (but I don’t want ot save anything in a static folder), or modify the html code of dash. But it’s frustrating me, that I cannot do a simple dynamically color changing method.

Any idea?

Its possible, I made progress getting the functionality within the edit control so you can draw within edit control and select a new color and it would get reflected on the last thing you drew on the map. Which is within that new leaflet fork I developed.

Alternatively you can use the official leaflet package and mirror the map and this code can be a good starting point for how to change colors of a polyline with that method:

from dash import *
import dash_leaflet as dl
from dash_extensions.javascript import assign
from dash_emoji_mart import DashEmojiMart
from dash_iconify import DashIconify
import dash_mantine_components as dmc

register_page(__name__, path="/cartographer", name="Cartographer", description="Map building page.")

point_to_layer_cartographer = assign("""function(feature, latlng, context){
    const p = feature.properties || {};
    const defaultRadius = 10;  // Default radius value
    const defaultColor = '#3388ff';  // Default color (Leaflet's default blue)
    const color = p.color || defaultColor;  // Use color from properties if available, otherwise use default

    const options = {
        color: color,
        fillColor: color,
        fillOpacity: 0.2,
        weight: 3
    };

    switch (feature.geometry.type) {
        case 'Point':
            if (p.type === 'circlemarker') {
                const radius = p._radius || defaultRadius;
                return L.circleMarker(latlng, {...options, radius: radius});
            } else if (p.type === 'circle') {
                const radius = p._mRadius || defaultRadius;
                return L.circle(latlng, {...options, radius: radius});
            } else {
                return L.marker(latlng);
            }
        case 'LineString':
            return L.polyline(feature.geometry.coordinates.map(c => [c[1], c[0]]), options);
        case 'Polygon':
            return L.polygon(feature.geometry.coordinates[0].map(c => [c[1], c[0]]), options);
        default:
            console.log('Unsupported geometry type:', feature.geometry.type);
            return L.marker(latlng);
    }
}""")

clientside_callback(
    ClientsideFunction(
        namespace="clientside",
        function_name="resize"
    ),
    Output("map", "id"),
    Input("tabs", "value")  # We'll add this id to the Tabs component
)

custom = [
    {
        'id': 'custom',
        'name': 'Custom',
        'emojis': [
            {
                'id': 'alien_face',
                'name': 'Alien Face',
                'short_names': ['alien_face'],
                'keywords': ['alien', 'face'],
                'skins': [{'src': '/assets/emoji/faces/alien_face.png'}],
                'native': '',
                'unified': 'custom',
            },
            {
                'id': 'concern_face',
                'name': 'Concern Face',
                'short_names': ['concern_face'],
                'keywords': ['concern', 'face'],
                'skins': [{'src': '/assets/emoji/faces/concern_face.png'}],
                'native': '',
                'unified': 'custom',
            },
            {
                'id': 'cowboy_face',
                'name': 'Cowboy Face',
                'short_names': ['cowboy_face'],
                'keywords': ['cowboy', 'face'],
                'skins': [{'src': '/assets/emoji/faces/cowboy_face.png'}],
                'native': '',
                'unified': 'custom',
            },
            {
                'id': 'crazy_face',
                'name': 'Crazy Face',
                'short_names': ['crazy_face'],
                'keywords': ['crazy', 'face'],
                'skins': [{'src': '/assets/emoji/faces/crazy_face.png'}],
                'native': '',
                'unified': 'custom',
            },
            {
                'id': 'daw_face',
                'name': 'Daw Face',
                'short_names': ['daw_face'],
                'keywords': ['daw', 'face'],
                'skins': [{'src': '/assets/emoji/faces/daw_face.png'}],
                'native': '',
                'unified': 'custom',
            },
            {
                'id': 'eye_roll_face',
                'name': 'Eye Roll Face',
                'short_names': ['eye_roll_face'],
                'keywords': ['eye', 'roll', 'face'],
                'skins': [{'src': '/assets/emoji/faces/eye_roll_face.png'}],
                'native': '',
                'unified': 'custom',
            },
            {
                'id': 'eye_rolling_shocked_face',
                'name': 'Eye Rolling Shocked Face',
                'short_names': ['eye_rolling_shocked_face'],
                'keywords': ['eye', 'rolling', 'shocked', 'face'],
                'skins': [{'src': '/assets/emoji/faces/eye_rolling_shocked_face.png'}],
                'native': '',
                'unified': 'custom',
            },
            {
                'id': 'faint_face',
                'name': 'Faint Face',
                'short_names': ['faint_face'],
                'keywords': ['faint', 'face'],
                'skins': [{'src': '/assets/emoji/faces/faint_face.png'}],
                'native': '',
                'unified': 'custom',
            },
            {
                'id': 'fight_face',
                'name': 'Fight Face',
                'short_names': ['fight_face'],
                'keywords': ['fight', 'face'],
                'skins': [{'src': '/assets/emoji/faces/fight_face.png'}],
                'native': '',
                'unified': 'custom',
            },
            {
                'id': 'happy_cry_face',
                'name': 'Happy Cry Face',
                'short_names': ['happy_cry_face'],
                'keywords': ['happy', 'cry', 'face'],
                'skins': [{'src': '/assets/emoji/faces/happy_cry_face.png'}],
                'native': '',
                'unified': 'custom',
            },
            {
                'id': 'hungry_face',
                'name': 'Hungry Face',
                'short_names': ['hungry_face'],
                'keywords': ['hungry', 'face'],
                'skins': [{'src': '/assets/emoji/faces/hungry_face.png'}],
                'native': '',
                'unified': 'custom',
            },
            {
                'id': 'medic_face',
                'name': 'Medic Face',
                'short_names': ['medic_face'],
                'keywords': ['medic', 'face'],
                'skins': [{'src': '/assets/emoji/faces/medic_face.png'}],
                'native': '',
                'unified': 'custom',
            },
            {
                'id': 'money_face',
                'name': 'Money Face',
                'short_names': ['money_face'],
                'keywords': ['money', 'face'],
                'skins': [{'src': '/assets/emoji/faces/money_face.png'}],
                'native': '',
                'unified': 'custom',
            },
            {
                'id': 'nerd_face',
                'name': 'Nerd Face',
                'short_names': ['nerd_face'],
                'keywords': ['nerd', 'face'],
                'skins': [{'src': '/assets/emoji/faces/nerd_face.png'}],
                'native': '',
                'unified': 'custom',
            },
            {
                'id': 'open_face',
                'name': 'Open Face',
                'short_names': ['open_face'],
                'keywords': ['open', 'face'],
                'skins': [{'src': '/assets/emoji/faces/open_face.png'}],
                'native': '',
                'unified': 'custom',
            },
            {
                'id': 'rip_face',
                'name': 'RIP Face',
                'short_names': ['rip_face'],
                'keywords': ['rip', 'face'],
                'skins': [{'src': '/assets/emoji/faces/rip_face.png'}],
                'native': '',
                'unified': 'custom',
            },
            {
                'id': 'shocked_face',
                'name': 'Shocked Face',
                'short_names': ['shocked_face'],
                'keywords': ['shocked', 'face'],
                'skins': [{'src': '/assets/emoji/faces/shocked_face.png'}],
                'native': '',
                'unified': 'custom',
            },
            {
                'id': 'sick_face',
                'name': 'Sick Face',
                'short_names': ['sick_face'],
                'keywords': ['sick', 'face'],
                'skins': [{'src': '/assets/emoji/faces/sick_face.png'}],
                'native': '',
                'unified': 'custom',
            },
            {
                'id': 'smile_face',
                'name': 'Smile Face',
                'short_names': ['smile_face'],
                'keywords': ['smile', 'face'],
                'skins': [{'src': '/assets/emoji/faces/smile_face.png'}],
                'native': '',
                'unified': 'custom',
            },
            {
                'id': 'snicker_face',
                'name': 'Snicker Face',
                'short_names': ['snicker_face'],
                'keywords': ['snicker', 'face'],
                'skins': [{'src': '/assets/emoji/faces/snicker_face.png'}],
                'native': '',
                'unified': 'custom',
            },
            {
                'id': 'soft_smile_face',
                'name': 'Soft Smile Face',
                'short_names': ['soft_smile_face'],
                'keywords': ['soft', 'smile', 'face'],
                'skins': [{'src': '/assets/emoji/faces/soft_smile_face.png'}],
                'native': '',
                'unified': 'custom',
            },
            {
                'id': 'sour_face',
                'name': 'Sour Face',
                'short_names': ['sour_face'],
                'keywords': ['sour', 'face'],
                'skins': [{'src': '/assets/emoji/faces/sour_face.png'}],
                'native': '',
                'unified': 'custom',
            },
            {
                'id': 'startled_face',
                'name': 'Startled Face',
                'short_names': ['startled_face'],
                'keywords': ['startled', 'face'],
                'skins': [{'src': '/assets/emoji/faces/startled_face.png'}],
                'native': '',
                'unified': 'custom',
            },
            {
                'id': 'whoa_face',
                'name': 'Whoa Face',
                'short_names': ['whoa_face'],
                'keywords': ['whoa', 'face'],
                'skins': [{'src': '/assets/emoji/faces/whoa_face.png'}],
                'native': '',
                'unified': 'custom',
            },
        ],
    },
]

# Create example app.
layout = html.Div([
    html.Div(dmc.ColorPicker(id="color-picker", format="rgba", value="rgba(41, 96, 214, 1)"), id="color-picker-container"),

    html.Div(DashEmojiMart(
        id='dash_emoji_input',
        custom=custom,
        autoFocus=False,
        categories=['frequent', 'people', 'nature', 'foods', 'activity', 'places', 'objects', 'symbols', 'flags',
                    'custom'],
        dynamicWidth=False,
        emojiButtonColors=[],
        emojiButtonRadius="100%",
        emojiButtonSize=36,
        emojiSize=24,
        emojiVersion=14,
        exceptEmojis=[],
        icons="auto",
        locale="en",
        maxFrequentRows=4,
        navPosition="top",
        noCountryFlags=False,
        noResultsEmoji="cry",
        perLine=9,
        previewEmoji="point_up",
        previewPosition="bottom",
        searchPosition="sticky",
        set="native",
        skin=1,
        skinTonePosition="preview",
        theme="dark",
    ), id="emoji_mart_"),
dmc.Card(
    children=[
        dmc.CardSection(
dmc.Tabs(
                    [
                        dmc.TabsList(
                            [
                                dmc.TabsTab("draw", value="draw_map"),
                                dmc.TabsTab("preview", value="preview_map"),
                            ]
                        ),
                        dmc.TabsPanel(
                            dl.Map(
                                center=[27.94093, -97.20840],
                                zoom=4,
                                children=[
                                    dl.TileLayer(url="https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}"),
                                    dl.FeatureGroup([
                                        dl.EditControl(
                                            id="edit_control",
                                            position='topright',
                                            draw={
                                                'polyline': {'shapeOptions': {'color': '#3388ff'}},
                                                'polygon': {'shapeOptions': {'color': '#3388ff'}},
                                                'circle': {'shapeOptions': {'color': '#3388ff'}},
                                                'rectangle': {'shapeOptions': {'color': '#3388ff'}},
                                            },
                                        )
                                    ]),
                                    dl.EasyButton(icon="fa-icons", title="Search Map", id="pick_an_icon", n_clicks=1),
                                    dl.EasyButton(icon="fa-palette", title="color selector", id="color_selector_icon", n_clicks=1)
                                ],
                                style={'width': '100%', 'height': '50vh', "display": "inline-block"},
                                id="map"
                            ),
                            value="draw_map"
                        ),
                        dmc.TabsPanel(
                            dl.Map(
                                center=[27.94093, -97.20840],
                                zoom=4,
                                children=[
                                    dl.TileLayer(url="https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}"),

                                ],
                                style={'width': '100%', 'height': '50vh', "display": "inline-block"},
                                id="mirror"
                            ),
                            value="preview_map"
                        ),
                    ],
                    id="tabs",  # Add this id
                    color="red",
                    orientation="horizontal",
                    value="draw_map",
                )
        ),
html.H3("Actions:", id="actions"),
    html.Div(id="map-center-output"),  # Added for center coordinates display
        ],  withBorder=True,
    shadow="sm",
    radius="md",
    w='80vw',),


])


@callback(Output("emoji_mart_", "style"),
          Input("pick_an_icon", "n_clicks"))
def trigger_mode(n_clicks):
    print('trigger_mode')
    if n_clicks % 2 == 0:
        print(n_clicks)
        return {"position": "absolute", "left": "45px", "top": "20vh", "zIndex": "1000"}
    else:
        return {"display": "none"}


@callback(Output("color-picker-container", "style"),
            Input("color_selector_icon", "n_clicks"))
def trigger_color_mode(n_clicks):
    print('trigger_color_mode')
    print(n_clicks % 2)
    if n_clicks % 2 == 0:
        return {"position": "absolute", "left": "45px", "top": "20vh", "zIndex": "1000"}
    else:
        return {"display":"none"}


# Add the center tracking callback
def calculate_center(bounds):
    lat_center = (bounds[0][0] + bounds[1][0]) / 2
    lon_center = (bounds[0][1] + bounds[1][1]) / 2
    return [lat_center, lon_center]

@callback(
    Output("map-center-output", "children"),
    Input("map", "bounds"),
    Input("map", "zoom"),
)
def update_center(bounds, zoom):
    if not bounds or zoom is None:
        return "Map center: Not available"
    try:
        center = calculate_center(bounds)
        return f"Map center: Lat {center[0]:.5f}, Lon {center[1]:.5f}, Zoom: {zoom:.1f}"
    except Exception as e:
        print(f"Error in update_center: {e}")
        print(f"bounds: {bounds}, zoom: {zoom}")
        return "Map center: Error in data format"

# Copy data from the edit control to the geojson component.
@callback(
    # Output("geojson", "data"),
    Output("mirror", "children"),
    Output('actions', "children"),
    Input("edit_control", "geojson"),
    Input("color-picker", "value"),
    Input("dash_emoji_input", "value"),
    State("mirror", "children")
)
def mirror(edit_geojson, color, emoji, current_children):
    ctx = callback_context
    if not ctx.triggered:
        return dash.no_update, dash.no_update
    print("Mirror triggered")
    input_id = ctx.triggered[0]['prop_id'].split('.')[0]

    map_context = []

    map_context.append(dl.TileLayer(url="https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}"),)

    if edit_geojson and edit_geojson.get('features'):
        for feature in edit_geojson['features']:
            if 'properties' not in feature:
                feature['properties'] = {}
            feature['properties']['color'] = color

            if feature['properties']['type'] == 'polyline':
                map_context.append(
                    dl.Polyline(positions=[(coord[1], coord[0]) for coord in feature['geometry']['coordinates']],
                                color=color, weight=5)
                )
            elif feature['properties']['type'] == 'circle':
                center = feature['geometry']['coordinates']
                map_context.append(
                    dl.Circle(center=(center[1], center[0]),
                              radius=feature['properties'].get('_mRadius', 1000),  # Use _mRadius for actual meters
                              color=color)
                )
            elif feature['properties']['type'] == 'circlemarker':
                center = feature['geometry']['coordinates']
                map_context.append(
                    dl.CircleMarker(center=(center[1], center[0]),
                                    radius=feature['properties'].get('_radius', 10),
                                    color=color)
                )
            elif feature['properties']['type'] in ['polygon', 'rectangle']:
                map_context.append(
                    dl.Polygon(positions=[(coord[1], coord[0]) for coord in feature['geometry']['coordinates'][0]],
                               color=color, weight=5)
                )
            elif feature['properties']['type'] == 'marker':
                coordinates = feature['geometry']['coordinates']
                print("testing emoji selector")
                print(emoji)
                if emoji:
                    custom_icon = dict(
                        iconUrl=f'{emoji}',
                        iconSize=[25, 25],
                        # iconAnchor=[22, 94],
                        # popupAnchor=[-3, -76]
                    )
                    map_context.append(
                        dl.Marker(position=(coordinates[1], coordinates[0]), icon=custom_icon, children=[dl.Tooltip(content="This is <b>html<b/>!")])
                    )
                else:
                    custom_icon = dict(
                        iconUrl='/assets/emoji/faces/alien_face.png',
                        iconSize=[25, 25],
                    )
                    map_context.append(
                        dl.Marker(position=(coordinates[1], coordinates[0]), icon=custom_icon)
                    )

    print(map_context)

    return map_context, f"{edit_geojson}"

Your last option would be to fork the dash-leaflet repo and design the functionality your are looking for by refining the dash-leaflet component.

Thanks a lot, but it seems too complicate at the beginnings, so I gave up this. But the popups and tooltips on the polyline also doesn’t work, so i don’t know what can I do. I supposed that my Python package has issues, or also this feature of original Dash Leaflet package is wrong.

Yeah, I’ve never been able to get popups working with Polyline either, issue originates from the original dash leaflet would be cool to support that, but atm polylines are mostly limited to draw a line and set the color of the polyline