Python Dash Leaflet cannot customize marker/icon

I have a python dash app in which I use dash-leaflet to generate a map. For my airports overlay, I am trying to add an airplane icon from Font Awesome or Awesome Markers, however it is not working. So basically the geojson_data should appear like it does now. However when geojson_airport checked it should have a marker of a plane from font awesome or awesome markers

Find code snippet below:

# GeoJson with all data
geojson_data = {
    "type": "FeatureCollection",
    "features": [
        {
            "type": "Feature",
            "geometry": {
                "type": "Point",
                "coordinates": [row["Longitude"], row["Latitude"]]
        }
        for index, row in df.iterrows()
    ]
}

# GeoJson airport data
geojson_airport = {
    "type": "FeatureCollection",
    "features": [
        {
            "type": "Feature",
            "geometry": {
                "type": "Point",
                "coordinates": [row['Longitude'], row['Latitude']]
            },
        }
        for _, row in airports.iterrows()
    ]
}

# Create map
dl.Map(
                            center=[51.505, -0.09],
                            zoom=6,
                            children=[
                                dl.LayersControl(
                                    [
                                        # Default map
                                        dl.BaseLayer(
                                            dl.TileLayer(
                                                url="https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png",
                                                attribution="&copy; <a href='https://www.openstreetmap.org/copyright'>OpenStreetMap</a> contributors",
                                                opacity=0.6,
                                            ),
                                            name="Default Map",
                                            checked=True,
                                        ),
                                        # Add overlay for airport
                                        dl.Overlay(
                                            dl.GeoJSON(
                                                data=geojson_airport,
                                                cluster=False),
                                            name="Airports",
                                            checked=False,
                                        ),
                                    ]
                                    + [
                                        # GeoJSON data
                                        dl.Overlay(
                                            dl.GeoJSON(
                                                data=geojson_data,
                                                cluster=True,
                                            ),
                                            name="Locations",
                                            checked=True,
                                        )
                                    ]
                                ),
                            ],
                        ),
                    ),

You have no dl.Marker(position=[57, 10], icon=custom_icon) in you dl.Map. I’d check out the repo I made for dash-leaflet as I created anew component within the repo that would look good with an airport overlay map.

Specifically the RotatedMarker which allows you to dynamically change the rotation of a marker as it follows a polyline.

from dash import Dash, html
from dash.dependencies import Input, Output
from dash_leaflet import RotatedMarker, TileLayer
from tests.stubs import app_stub

# Define test selector
selector = ".leaflet-marker-icon"

# Create a rotated marker component
component = RotatedMarker(
    position=[56, 10],
    id="rotated-marker",
    rotationAngle=45,  # 45 degree rotation
    rotationOrigin='center center'  # rotate around center
)

# Create the test app
app = app_stub(components=[component, TileLayer()])

if __name__ == "__main__":
    app.run_server(debug=True, port=8051)

looks like:

You can download this .tar.gz file of this leaflet plugin I built directly on my website and just pip install .tar.gz file to get it setup in your project.

Outside of that, also would be worth looking at the documentation of leaflet marker for a basic marker icon setup.

https://www.dash-leaflet.com/components/ui_layers/marker

@PipInstallPython I’ve tried adding the dl.Marker from the dash-leaflet docs example and it works fine. The problem is that its not working when I try to put the marker inside the overlay

# overlay for airport
dl.Overlay(
    dl.GeoJSON(
        data=geojson_airport,
        cluster=False),
    name="Airports",
    checked=False,
),

How can I insert custom marker inside here so that when checked=True all airport markers will display with a specific airplane marker?

This is how I did it with a geojson its a bit more complicated but I used javascript:


point_to_layer_js = assign(
    """
function(feature, latlng){
    const iconData = feature.properties.icon_data;
    const flag = L.icon({
        iconUrl: iconData.url, 
        iconSize: [35, 35],
        tooltipAnchor: [20, 0]
    });
    const marker = L.marker(latlng, {icon: flag});
    marker.bindTooltip('<img src="' + feature.properties.image + '" style="width: 30vw; height: 20vh; max-width: 250px; max-height: 250px;">');
    marker.on('click', function(e) {
        var event = new CustomEvent('marker_click', {detail: feature.properties.pk});
        window.dispatchEvent(event);
    });
    return marker;
}
"""
)

dl.Map(

                    [
                        dl.TileLayer(
                            url="https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}",
                        ),
                        dl.GeoJSON(
                            data=geojson_dict,
                            id="locations_layer",
                            cluster=True,
                            zoomToBoundsOnClick=True,
                            superClusterOptions=dict(radius=40),
                            hideout=dict(
                                m_type_colors=m_type_colors,
                                circleOptions=dict(fillOpacity=0.5, stroke=False, radius=3),
                                min=0,
                            ),
                            pointToLayer=point_to_layer_js,
                            clusterToLayer=cluster_to_layer,
                        ),
                        dl.EasyButton(
                            icon="fa-search",
                            title="Search Map",
                            id="search_map_display_btn",
                            n_clicks=1,
                        ),
                        dl.EasyButton(
                            icon="fa-list",
                            title="Show / Hide Ledger",
                            id="show_or_hide_ledger_btn",
                            n_clicks=1,
                        ),
                        legend
                    ],
                    style={
                        "position": "absolute",
                        "width": "100%",
                        "height": "92%",
                        "left": "0",
                        "z-index": 0,
                    },
                    attributionControl=False,
                    className="stylecursor",
                    center=[27.94093, -97.20840],
                    zoom=10,
                    id="map",
                )

This is what I used for

Not all the code I included is needed, but gives you a point to work from focus on the pointToLayer=point_to_layer_js section.

1 Like

works great! thanks a lot!

@PipInstallPython trying now to color polygons in different colors however I don’t know how to do it. I tried following one example but it didnt work. I am assuming it also requires some JS code. Are you able to help? So basically i want each line to be of different colors. colors can be random

shapefile_path = r"/London"
underground = gpd.read_file(shapefile_path)
geojson_underground = underground.__geo_interface__
for feature in geojson_underground['features']:
    feature['properties']['popup'] = f"""
        <span style='font-size: 18px;'>
            {feature['properties'].get('Name', 'No Line Name available')}
        </span><br><br>
    """


# Underground Lines
dl.Overlay(dl.GeoJSON(
        data=geojson_underground,
    ),
    name="Underground Lines",
    checked=False,
),

This isn’t and is very complicated depending on the extent of what you need. The basic approach is all drawings are in this format:

{
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "properties": {
        "type": "polyline",
        "_leaflet_id": 156,
        "color": "#000000",
        "children": "None",
        "content": "",
        "icon": null,
        "emoji": "",
        "children_type": "None"
      },
      "geometry": {
        "type": "LineString",
        "coordinates": [
          [
            -0.07347106933593751,
            51.51547989904435
          ],
          [
            -0.03759384155273438,
            51.52242070889328
          ]
        ]
      }
    }
  ]
}

So all you would need to do is setup all the drawings in this format and change the color. This callback might be useful reference when partnered with the edit control to see whats going on:

@app.callback(
    Output("actions-content", "children"),
    [Input("edit_control", "geojson"),
     Input("apply-color-button", "n_clicks"),
     Input("emoji-picker", "value")]
)
def display_action_data(geojson_data, n_clicks, emoji_value):
    if not geojson_data or not geojson_data.get('features'):
        return "No features drawn"

    display_geojson = {
        'type': 'FeatureCollection',
        'features': []
    }

    for feature in geojson_data['features']:
        new_feature = {
            'type': 'Feature',
            'properties': {
                'type': feature['properties']['type'],
                '_leaflet_id': feature['properties'].get('leafletId'),
                'color': feature['properties'].get('color')
            },
            'geometry': feature['geometry']
        }

        # Add emoji property for markers
        if feature['properties']['type'] == 'marker' and 'emoji' in feature['properties']:
            new_feature['properties']['emoji'] = feature['properties']['emoji']

        # Add additional properties if present
        for prop in ['radius', 'mRadius', 'bounds']:
            if prop in feature['properties']:
                new_feature['properties'][f'_{prop}'] = feature['properties'][prop]

        display_geojson['features'].append(new_feature)

    return json.dumps(display_geojson, indent=2)

You’ll need to trim some of the code out like the emoji but their are some useful loops and references in that callback like the Input("edit_control", "geojson") and the for feature in geojson_data['features'] loop.

This is another function that would be useful for generating the random color:

def generate_random_color() -> str:
    """Generate a random color in hexadecimal format."""
    # Using HSV color space for more visually distinct colors
    hue = random.random()  # Random hue between 0 and 1
    saturation = 0.7 + random.random() * 0.3  # High saturation (0.7-1.0)
    value = 0.8 + random.random() * 0.2  # High value/brightness (0.8-1.0)

    # Convert HSV to RGB
    h = hue * 6
    i = int(h)
    f = h - i
    p = value * (1 - saturation)
    q = value * (1 - f * saturation)
    t = value * (1 - (1 - f) * saturation)

    if i == 0:
        r, g, b = value, t, p
    elif i == 1:
        r, g, b = q, value, p
    elif i == 2:
        r, g, b = p, value, t
    elif i == 3:
        r, g, b = p, q, value
    elif i == 4:
        r, g, b = t, p, value
    else:
        r, g, b = value, p, q

    # Convert to hex
    return f"#{int(r * 255):02x}{int(g * 255):02x}{int(b * 255):02x}"

which Is used in a custom polyline component I built but would be similar to a regular polyline:

# Create markers and paths with optimized rendering
markers_and_paths = []
# Store colors to ensure uniqueness
used_colors = set()

for trip in trip_data:
    # Generate unique color for each path
    while True:
        base_color = generate_random_color()
        if base_color not in used_colors:
            used_colors.add(base_color)
            break

    pulse_color = generate_pulse_color(base_color)

    # Add AntPath with reduced complexity for better performance
    markers_and_paths.append(
        dl.AntPath(
            positions=[[coord['lat'], coord['lng']] for coord in trip['coordinates']],
            id=f'path-{trip["id"]}',
            color=base_color,
            pulseColor=pulse_color,
            delay=2000,  # Increased delay for smoother animation
            weight=6,
            dashArray=[4, 12],  # Adjusted for better visibility
            # opacity=0.6,  # Added transparency for less visual clutter
        )
    )
    # Add marker with popup information
    markers_and_paths.append(
        dl.RotatedMarker(
            position=[trip['coordinates'][0]['lat'], trip['coordinates'][0]['lng']],
            icon=taxi_icon,
            rotationOrigin='center',
            rotationAngle=0,
            id=f'marker-{trip["id"]}',

        )
    )

Looks like this when you have it setup correctly.

Hope this helps, kinda difficult topic to explain and it can get pretty complex depending on how extensive you need this type of functionality implemented.

@PipInstallPython thanks a lot for the help! really appreciate it.

One thing im trying to do now is the colorbar with dots on the map. i am trying to follow the GeoJSON tutorial however when i run the code i can only see the colorbar without the dots on the map based on density like below. Do you know what I am doing wrong? Code snippet below.

My output:

Expected output:

 # Create some dummy geojson data
dummy_data = {
    "type": "FeatureCollection",
    "features": [
        {
            "type": "Feature",
            "geometry": {"type": "Point", "coordinates": [random.uniform(-125, -66), random.uniform(24, 49)]},
            "properties": {
                "city": f"City {i}",
                "density": random.randint(100, 3500)
            }
        }
        for i in range(10)
    ]
}

# Define colorscale and range
colorscale = ['red', 'yellow', 'green', 'blue', 'purple']  # rainbow
vmin = 0
vmax = 3500

# Define the colorbar
colorbar = dl.Colorbar(colorscale=colorscale, width=20, height=150, min=vmin, max=vmax, unit='/km2')

# JS
on_each_feature = assign("""function(feature, layer, context){
    layer.bindTooltip(`${feature.properties.city} (${feature.properties.density})`)
}""")

point_to_layer = assign("""function(feature, latlng, context){
    const {min, max, colorscale, circleOptions, colorProp} = context.hideout;
    const csc = chroma.scale(colorscale).domain([min, max]);  // chroma lib to construct colorscale
    circleOptions.fillColor = csc(feature.properties[colorProp]);  // set color based on color prop
    return L.circleMarker(latlng, circleOptions);  // render a simple circle marker
}""")

# Create the Dash app
app = Dash(__name__, prevent_initial_callbacks=True)

# Create the map layout
app.layout = html.Div([
    dl.Map([
        dl.TileLayer(),
        dl.GeoJSON(data=dummy_data,
                   zoomToBounds=True,
                   pointToLayer=point_to_layer,
                   onEachFeature=on_each_feature,
                   hideout=dict(colorProp='density', circleOptions=dict(fillOpacity=1, stroke=False, radius=5),
                                min=vmin, max=vmax, colorscale=colorscale)),
        colorbar
    ], style={'height': '100vh'}, center=[37.7749, -122.4194], zoom=5),
])