emilhe / dash-leaflet

MIT License
213 stars 37 forks source link

GeoJSON layer #14

Closed hhoeflin closed 4 years ago

hhoeflin commented 4 years ago

Hi,

great package, I love leaflet for its versatility and having it available in dash is great! One of the layers currently not supported is GeoJSON. Any plans to implement? I know time is limited ...

Thanks!

emilhe commented 4 years ago

Thanks! I have actually just started looking into it last weekend, but there is still some work to do. Are there any particular features that you need?

hhoeflin commented 4 years ago

Thanks for the quick answer. Straight GeoJSON, maybe with the ability to specify color, opacity, inner color, line color, line thickness, etc for each feature. When looking into leaflet, there seems to be the "style" property which however expects a function, so I guess this would have to be mapped to some other way of handling it for dash?

On Wed, May 6, 2020 at 12:25 PM Emil Haldrup Eriksen < notifications@github.com> wrote:

Thanks! I have actually just started looking into it last weekend, but there is still some work to do. Are there any particular features that you need?

— You are receiving this because you authored the thread. Reply to this email directly, view it on GitHub https://github.com/thedirtyfew/dash-leaflet/issues/14#issuecomment-624565638, or unsubscribe https://github.com/notifications/unsubscribe-auth/AASGMYVEVQEU3WM3WQSTD73RQE3IRANCNFSM4M2JKNTQ .

emilhe commented 4 years ago

Yes, that is in fact one of the key challenges in writing the geojson wrapper. AFAIK, there is no direct way of passing function handlers to the javascript layer. But i would love to be proved wrong :)

My current design approach is to create an options dict, which holds a style dict (among other things). The user can then set an options dict on the geojson level, which will be the default for all features. Additionally, a featureOptions dict with the feature id as key and a options dict can be used to assign options, such as the style, for each feature. Here is a small example demonstrating the current syntax,

import json
import dash
import dash_html_components as html
import dash_leaflet as dl

with open("assets/us-states.json", 'r') as f:
    statesData = json.load(f)

def get_color(d):
    color_limits = {1000: '#800026', 500: '#BD0026', 200: '#E31A1C', 100: '#FC4E2A', 50: '#FD8D3C', 20: '#FEB24C',
                    10: '#FED976', -1: '#FFEDA0'}
    for key in color_limits:
        if d > key:
            return color_limits[key]

def get_style(d):
    return dict(fillColor=get_color(d), weight=2, opacity=1, color='white', dashArray='3', fillOpacity=0.7)

# Bind per-feature style information.
featureOptions = {}
for item in statesData["features"]:
    featureOptions[item["id"]] = dict(style=get_style(item["properties"]["density"]),
                                      popupContent="{:.3f} people/mi2".format(item["properties"]["density"]))
# Create geojson.
options = dict(hoverStyle=dict(weight=5, color='#666', dashArray=''), zoomToBoundsOnClick=True)
geojson = dl.GeoJSON(data=statesData, id="geojson", options=options, featureOptions=featureOptions)
# Create app.
app = dash.Dash()
app.layout = html.Div([
    dl.Map(children=[dl.TileLayer(url="https://a.tile.openstreetmap.org/{z}/{x}/{y}.png")] + [geojson],
           style={'width': "100%", 'height': "100%"}, center=[39, -98], zoom=4, id="map", maxZoom=18),
],  style={'width': '100%', 'height': '50vh', 'margin': "auto", "display": "block"})

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

The us-states.json file contains the data used in the Leaflet choropleth guide,

https://leafletjs.com/examples/choropleth/

If you would like to try it out, the preliminary component is available in dash-leaflet==0.0.10rc2, and you can download the json data here. If you have any suggestions to a better design approach and/or syntax, please let me know.

hhoeflin commented 4 years ago

good to hear that this is on the roadmap. I also don't have easier options. But from looking at the examples of functions for "style" presented in leaflet, they usually show how to map something from a feature property. Like in the answer to this example: https://gis.stackexchange.com/questions/158227/leaflet-geojson-problem-with-style

So what I thought that there could be a default dict with default values, and maybe a "mapping" that defines which feature properties should be used for which style-values (then you would natively use values specified in the geojson, and don't have to style it from the outside using ids.

Happy to talk more!

hhoeflin commented 4 years ago

One advantage with the suggested mapping approach would also be that it would then be possible to update the 'data' property. As the styling would be a pre-configured styling function (with the mapping as a bound parameter), the reset geo-json data would be styled correctly.

Using the options approach, the 'data' and the 'options' prop would always have to be reset at the same time, otherwise features with unknown ids would not be styled correctly.

emilhe commented 4 years ago

My first implementation was along the lines that you are suggesting, i simply looked for a "style" property on each feature and applied the style if it was there. However, i didn't like this approach for two main reasons,

1) In most cases, the style is not part of the original geojson data. Hence you would have to create the styles in Python and add them to the geojson data prior to creating the GeoJSON object for Dash Leaflet. While this is perfectly possible, i find the approach of separation of data and style in separate properties (the current implementation) more clean; the data is provided as-is and the style is calculated on the side. On the other hand, the linking using ids is not a perfect solution from a syntax point of view, so if it has not been for (2) i would probably have kept this implementation. The syntax issue could maybe be helped somewhat be hiding the id mapping behind a convenience wrapper function in Python.

2) If the style is provided as part of the GeoJSON object, it is not possible to update the styles without updating the data property. Hence if you have a large geojson file loaded on the map, and you want to change the color of a feature from Python, you would have to send the complete geojson file from the server to the client again. In some cases, i imagine that this would be a deal breaker. By separating the data and style in two properties, it would be possible only to send the styles across the wire, which might be significantly faster in some cases.

emilhe commented 4 years ago

Could you elaborate what you mean by a "pre-configured styling function"? To my knowledge it is not possible to pass functions from Dash to javascript. But if there is some way, that would be great; it would simply things a lot :)

hhoeflin commented 4 years ago

Sure - and please excuse me if this makes no sense. I have little to no javascript-knowledge, so maybe this doesn't work.

As far as I understand it, it is correct that dash cannot pass a function. What I think would be an option is to add an additional prop to leaflet (e.g. _stylingmapping, which when changed triggers a change in the style prop-function, in javascript. This styling function is implemented in javascript directly, and takes as parameters the 'feature' as well as a 'styling_mapping' and then just returns the feature-properties with the remapped names as defined in 'styling_mapping'.

I can only do this in equivalent python-code, which would be

def my_style(feature, style_mapping):
    output = {}
    for key, value in style_mapping.items():
        output[key] = feature.properties[value]
    return output

and with a change of the _stylemapping prop, the style prop gets reset to

partial(my_stile, style_mapping=style_mapping_prop)

Hope this makes sense. More sophistication could be added to automatically emit a default value if the requested property happens to be absent.

hhoeflin commented 4 years ago

Furthermore, I agree with your reasoning, I can see good applications for both approaches. For me, I do create my geojson data on the fly in a server. I can easily add the styling information. But I find your explanation perfectly reasonable.

hhoeflin commented 4 years ago

Another maybe a bit whacky idea would be to allow sending a true styling function as a string into a prop, converting the string to a javascript function on the javascript side and then set this into the 'style' prop. Then things like

function(feature) {
        switch (feature.properties.party) {
            case 'Republican': return {color: "#ff0000"};
            case 'Democrat':   return {color: "#0000ff"};
        }
    }

should be possible. Does this work?

emilhe commented 4 years ago

It makes complete sense - the logic your are suggesting is conceptually similar to what I am doing in the Javascript layer ;)

The main drawback of your suggested solution, as compared to a separate style property, is that all styles must be computed in advance. This is a problem if e.g. a polygon color depends on a user input; as I understand, your approach would then require the whole geojson data to be sent from server to client each time the user input changes.

Haha, I was also thinking about just passing JavaScript function strings. While this is definite possible (Javascript has an eval function; it evaluates strings as code), it would be kind of hacky IMO, and it would require the user to write Javascript (which I guess a lot of Dash users would not appreciate).

A variation of this option could be to allow the user to specify a JavaScript function in an external file; this would make it easier to write the code (you can use an IDE), and less hacky since the eval function is not required. However, it still requires JavaScript knowledge. Therefore, I think this would only be a viable as an optional option; to enable all Dash users to use the component, there should be a Python-only option available.

hhoeflin commented 4 years ago

Yeah, I agree with the hackyness of javascript functions as strings. Although now there is precedent - https://dash.plotly.com/clientside-callbacks

Clientside-callbacks as implemented in dash are nothing but javascript functions as strings in python and passed to the client. So this would just be a simple extension of the clientside-javascript function paradigm. And I agree it would not be appreciated if it was the only way to style it. But what if it was an additional option. Maybe your "id" idea through the options, and alternatively a javascript function could be passed as string (the latter overriding the former).

emilhe commented 4 years ago

I have decided not to pursue the java script approach for now, it might be added later as an option. Instead, i have created a new express module, which holds convenience functions, one of which addresses the issue discussed here. Hence, you can now construct a GeoJSON object like this,

from dash_leaflet import express as dlx

....

def get_style(feature):
    color = get_color_somehow(feature)
    return dict(fillColor=color, weight=2, opacity=1, color='white', dashArray='3', fillOpacity=0.7)

geojson = dlx.geojson(data, style=get_style)

It is thus possible to use a syntax that follows the leaflet component closely, even though the implementation is somewhat different.

hhoeflin commented 4 years ago

Thanks, that looks pretty good! And then I pass the whole geojson object into the leaflet layer, correct?

emilhe commented 4 years ago

Yes, in the code above ‘data’ is the raw geojson data, and the ‘geojson’ variable is a GeoJSON object, which can be added to the map (like layers, markers, etc.)

garikhgh commented 4 years ago

Hi. nice feature. How to make use of this new feature in callbacks, how to get clicked state id or state name?

emilhe commented 4 years ago

Thanks! I have just released version 0.0.11 which includes the GeoJSON component. It supports click events and hoover events via the properties featureClick and featureHover.

emilhe commented 4 years ago

I have just created a documentation page, which container a number of examples including one demonstrating the usage of the new GeoJSON component,

https://dash-leaflet.herokuapp.com/#us_states

garikhgh commented 4 years ago

great. good idea. are you planning to post all the docs what you create.

emilhe commented 4 years ago

I intend to keep the documentation up to date with the main features. At some point i might also add an API doc, but until then, you can see the documentation of the individual component properties in the Python source files (in PyCharm you can just to the component source by clicking "ctrl"+"b", other editors probably have similar functionality).

boukepieter commented 3 years ago

This does not seem to work anymore. I'm using version 0.1.13, has the usage changed?

emilhe commented 3 years ago

Could you elaborate on what is not working? And what versions are you comparing? There has been significant development on the component since this issue was created.

Celtics33 commented 3 years ago

This does not seem to work anymore. I'm using version 0.1.13, has the usage changed?

There is no featureOptions argument that I found either.

emilhe commented 3 years ago

Yes, the syntax changed from 0.1.0 and onwards :). And yes, it is possible to apply different colors per feature. It requires writing a small JavaScript function though.

Celtics33 commented 3 years ago

Yes, the syntax changed from 0.1.0 and onwards :). And yes, it is possible to apply different colors per feature. It requires writing a small JavaScript function though.

Thank you, I figured out the js script that was needed!

yampi67 commented 3 years ago

Hello, I am new to dash leaflet. I have the same Celtics33's problem . What .js do I need to have? and where should I put it? in the assets folder?

Thanks

emilhe commented 3 years ago

@yampi67 Yes, you can put it in the asserts folder. Or you could link it as an external asset, that is up to you.