Dash-leaflet creating callback to allow user adjust map color class breaks

Hi, I have a road network data, and the segment has an attribute called OBJ which I want to display on the map. I am able to create an initial map with 5 color classes; however, I would like to create some user-defined inputs so that those color class breaks can be changed later and the map will be re-rendered accordingly

As shown below, I create a function that assign varying color and weight to segment (5 color classes with 4 class breaks)

from dash_extensions.javascript import arrow_function,assign

breaks = [200,500,1000,2000]
colorramp = ["#488f31","#a8bb59","#ffe792","#f2975a","#de425b"]

color_schema_5c = assign("""function(geoJsonFeature){
    const aobj = geoJsonFeature.properties.OBJ;
    if (aobj <= %d) {
        return {"color": "%s", "weight": "0.53"};
    }
    else if (aobj > %d && aobj <= %d) {
        return {"color": "%s", "weight": "1.07"};
    }
    else if (aobj > %d && aobj <= %d) {
        return {"color": "%s", "weight": "2.67"};
    }
    else if (aobj > %d && aobj <= %d) {
        return {"color": "%s", "weight": "3.3"};
    }
    else {
        return {"color": "%s", "weight": "4"};
    }
}"""% (breaks[0],colorramp[0],breaks[0],breaks[1],colorramp[1],breaks[1],breaks[2],colorramp[2],breaks[2],breaks[3],colorramp[3],colorramp[4])
)

The created function is used in the style parameter of GeoJSON to add the initial layer

dl.GeoJSON(data=roaddata, id="road",style=color_schema_5c  )

Since I intend to allow user to change the class breaks, I also define the class inputs

dcc.Input(value=f"{breaks[0]}",type= 'number',min=0,max=8252,step=1,id="class_break_1"),
dcc.Input(value=f"{breaks[1]}",type= 'number',min=0,max=8252,step=1,id="class_break_2"),
dcc.Input(value=f"{breaks[2]}",type= 'number',min=0,max=8252,step=1,id="class_break_3"),
dcc.Input(value=f"{breaks[3]}",type= 'number',min=0,max=8252,step=1,id="class_break_4")

I suppose the next step is to define a callback function. I attempt to write the callback function but map just does not get re-rendered as I change the input box value (error such as “No match for [dashExtensions.default.function1] in the global window object.” is thrown)

Any suggestion on how such a callback function should be wrote (like what output should be modified/returned by the function; whether style can be modified in this case directly or maybe the map has to be re-created?) or perhaps is there any other potential solution? I know ipyleaflet has a substitute method, but does not see similar method in dash-leaflet

I think the answer to this is broadly along these lines:

The style function you’ve written will probably accept an additional parameter context. (onEachFeature and pointToLayer definitely do, but I’ve not tried it with style)

The hideout property of your GeoJSON layer can be accessed (from within your JavaScript function) as context.hideout

The hideout property can be updated in a callback (and can be a Python dictionary~JavaScript object)

So if you store your color scheme in hideout, you can both update it from callbacks and access it in your style function. A change to hideout should trigger re-rendering. (In a clientside callback you may need to clone the hideout object - see this thread (Dash-leaflet - Problem updating hideout in a clientside callback))

2 Likes

I like this approach and it would be ideal to maintain the geojson as the route of changing style. I took a different approach which seems to have some benefits and some limitations.

My approach consists of basically setting up two maps, one for drawing and one for final display.

I started with returning the Input(“edit_control”, “geojson”) as an output to a H3 for debugging. Then I created a decoder for edit_control geojson and turned them into a list of map objects like:

    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)
                    )

Connected a dmc color picker and the dash-emoji-picker. And was able to setup dynamic color for drawn objects and custom emoji’s for markers.

1 Like