Black Lives Matter. Please consider donating to Black Girls Code today.

Choropleth map in dash

Another newbie question.

I am trying to add something similar to the map below that I have made with javascript, leaflet and mapbox using Dash

I have tried combining the Dash example https://plot.ly/dash/gallery/uber-rides/ with the county level choropleth example https://plot.ly/python/county-level-choropleth/ with no luck. Here is a snippet of my code

layout=Layout(
autosize=True,
height=750,
margin=Margin(l=0, r=0, t=0, b=0),
showlegend=False,
mapbox=dict(
layers=[
dict(
sourcetype=‘geojson’,
source=‘http://cityinsight-interface.ssg.coop/sites/all/data/moncton/testing1.geojson’,
type=‘fill’,
color='rgba(163,22,19,0.8)'
)
],
accesstoken=mapbox_access_token,
center=dict(
lat=latInitial, # 40.7272
lon=lonInitial # -73.991251
),
style=‘dark’,
bearing=bearing,
zoom=zoom
),…
I added the layers dict to the layout of the Uber example (for now I did not worry about matching up the lat and log and just navigated the map to my location to see if the layer appeared)

Is it possible to have a choropleth map that has coloured zones that you can hover over to display information without points? If so can you give me some tips on how to do this.

Here is the url to the json file I am using for the layers - http://cityinsight-interface.ssg.coop/sites/all/data/moncton/testing1.geojson.

@chris-ssg Here is an example of a map with two filled regions, as for a choropleth: https://plot.ly/~empet/14398
If you want to hide the points, just color them with the same color used in the filled region, and set a small size. Then the point will not be displayed, but on hover its lon and lat will be shown:

 data=[ dict(type='scattermapbox',
            lat= lat,
            lon=lon,
            mode='markers',
            text=regions,
            marker=dict(size=0.5, color= '#a490bd'),
            showlegend=False,
            hoverinfo='text'
            )]
layers=[dict(sourcetype = 'geojson',
                   source =shape,
                   below="water", 
                   type = 'fill',   
                   color = '#a490bd',
                   opacity=0.8
   )      ]
1 Like

Thanks empet. I will give this a try

Is it possible to have the fill color be determined by a value in the json file for each choropleth polygon similarly to what is done in this example https://plot.ly/dash/gallery/uber-rides/?

I understand it can be done for the markers.

Yes it is possible (https://plot.ly/~empet/14400), but for each polygon in the initial geojson file you should define a dict having the same structure as a geojson file. The number of layers is then equal to the number of such dicts:

layers=[dict(sourcetype = 'geojson',
                    source =sources[k],#sources[k] is a geojson type dict
                    below="water", 
                    type = 'fill',   
                   color = facecolor[k],#facecolor is a list of colors, one for each polygon
                   opacity=0.8
                   ) for k in range(len(sources))]

The color, facecolor[k], assigned to the k^th polygon is chosen according to a value, val, associated to that polygon/county. You should define a function that maps each val to a color:
val-->nval=(val-min(vals))/(max(vals)-min(vals)) in [0,1]-->plotly color code of the corresponding color in a matplotlib colormap cmap.
More precisely, to the normalized value, nval, one associates the matplotlib color, cmap(nval), that must be converted to a Plotly color.

1 Like

Hi empet, thanks for the response. I have been trying to get this working but I seem to be having some problems.

I have a dataframe that I am using to inform the map layers and I am trying to loop through the rows of the dataframe as follows but nothing shows up on the map.

dict(
sourcetype=‘geojson’,
source=df.iloc[[index]].to_json(),
type=‘fill’,
color=’#527fa5
) for index, row in df.iterrows()

Here is a copy of a subset of the dataframe that I am trying to use.

zoneID,geometry,scenarioNames,time,Values,Colors
126,“POLYGON ((-64.69587606703536 46.11647015103159, -64.70656143382793 46.12350078478621, -64.70617664316991 46.12372011829556, -64.70492668797078 46.12443258118601, -64.70080852466211 46.12677966648526, -64.70044494770887 46.12666486151662, -64.70012689975604 46.12656930527012, -64.69982109857978 46.12649809271233, -64.69972299481954 46.12647609974862, -64.69929978508412 46.1263812252891, -64.69882501241818 46.12630130303811, -64.69837709629179 46.12624721527086, -64.69825007009452 46.12623187607441, -64.69772659134379 46.12618135721869, -64.69724530642895 46.12616770729807, -64.69680287315963 46.126160714567, -64.69656292181128 46.12616667987563, -64.69602993206183 46.12617992713921, -64.69264358279902 46.12641126278303, -64.6907074992998 46.12654347841128, -64.69057169866677 46.12626777586706, -64.69057043199641 46.12626520557077, -64.69048557189258 46.12627357257339, -64.68844001284305 46.1264752286896, -64.68832619310804 46.12648644775686, -64.68815743625559 46.12611677755164, -64.68726721471185 46.12416662188806, -64.68703096636678 46.12364906320003, -64.68672676589641 46.12298262438362, -64.68898935977973 46.12137233500832, -64.69038757587468 46.12037725564999, -64.69221239148236 46.11907849358341, -64.69243590676363 46.11891940647094, -64.69325180958272 46.11833867580128, -64.69356385850809 46.1181155119969, -64.69373410194753 46.11799448182592, -64.69539599662349 46.11681180472976, -64.69587606703536 46.11647015103159))”,BAU,2016.0,0.09036830957,#d2edf1
127,“POLYGON ((-64.70656143382793 46.12350078478621, -64.69587606703536 46.11647015103159, -64.6967622688471 46.11583944872042, -64.69684700408114 46.11577914206764, -64.6991192152895 46.11419705833731, -64.69962515484778 46.113868333925, -64.69963107711679 46.11386527804408, -64.69965217978194 46.11385301364088, -64.69982535312678 46.11375621154573, -64.69987790189414 46.11372683754356, -64.70006469395881 46.11362242159112, -64.70033091374131 46.11347365303565, -64.70033028183258 46.11347319312681, -64.70035663040535 46.11345876184406, -64.70042118655357 46.11342333772103, -64.70048572793226 46.11338836325432, -64.70055025454292 46.1133538384439, -64.70061545703946 46.11331842442257, -64.70068064476726 46.11328346005683, -64.70074581772798 46.11324894534675, -64.70083204870991 46.11320351505344, -64.70083141680007 46.11320305514732, -64.70104129204795 46.11309568804208, -64.7011419384305 46.11304463613566, -64.7015485044152 46.11283734009756, -64.70261249758238 46.11234751804589, -64.70296888649987 46.11220383481729, -64.70397534768689 46.11179805832804, -64.70521603977397 46.11136363846796, -64.70649864460425 46.11099240477454, -64.70665427252551 46.11040455291133, -64.71024838152888 46.10967586135291, -64.7118632121187 46.11344066530698, -64.71216371755256 46.11380750309941, -64.71394025360003 46.11525294869347, -64.71694694011696 46.11769904556979, -64.71808093054712 46.11863925957265, -64.71766943605678 46.11872213857654, -64.71719766433928 46.11878897245406, -64.71681010838928 46.11884387506954, -64.71503331444298 46.11909556113299, -64.71489006822766 46.11912160513889, -64.71447780105999 46.11920310539968, -64.71408520120893 46.11932005609519, -64.71378505629568 46.11943551621697, -64.71331291720087 46.11966459178458, -64.71279260881245 46.11995939239151, -64.70779978858023 46.12279489221967, -64.70656143382793 46.12350078478621))”,BAU,2016.0,0.11410351870000003,#d2edf1
128,“POLYGON ((-64.72014977295197 46.10816097930303, -64.72786056095322 46.11410190294202, -64.72767074091109 46.11426324847796, -64.7273311609146 46.11455188548722, -64.72693337789606 46.11488998817421, -64.7266576807326 46.11509550966576, -64.72635346726379 46.11529152325321, -64.72616251481264 46.11545208652409, -64.72612895184309 46.11543570318732, -64.72612428641018 46.11543862523165, -64.72612351671981 46.1154391018635, -64.72577772566819 46.11565482015236, -64.72565365520033 46.1157322119951, -64.72489741753789 46.11618023109318, -64.72468901821783 46.11630368931214, -64.72419237427475 46.11659771420578, -64.72332146421998 46.1171169967263, -64.72285818569557 46.11737402970388, -64.7228100590539 46.11740073057425, -64.72255215902976 46.11753342081053, -64.72220727855705 46.11766545499609, -64.72193315221823 46.1177507134009, -64.72169522070969 46.11782002395593, -64.72138091130329 46.11789318161111, -64.72036935806268 46.1181150058098, -64.719294240549 46.1183653326452, -64.71886977199431 46.11846116263728, -64.71843298122785 46.11855977685424, -64.71843210007768 46.11855997543842, -64.71808093054712 46.11863925957265, -64.71694694011696 46.11769904556979, -64.71394025360003 46.11525294869347, -64.71216371755256 46.11380750309941, -64.7118632121187 46.11344066530698, -64.71024838152888 46.10967586135291, -64.71044647682866 46.10963487558242, -64.7117975724564 46.10936061699702, -64.71256504253726 46.10920481973635, -64.71260459468074 46.10906236421255, -64.7128645460155 46.10910513645654, -64.71421019414342 46.10930036141121, -64.71497534053361 46.10914408231554, -64.71499962932481 46.10919305481525, -64.71536582126008 46.10911937903419, -64.7178540545456 46.10861872528995, -64.71882947725342 46.10842244445878, -64.71907868718566 46.10837729924714, -64.71954698774078 46.10828272106114, -64.72014977295197 46.10816097930303))”,BAU,2016.0,1.665109807,#d2edf1

Here are message the server is returning when I run my code.

127.0.0.1 - - [02/Aug/2017 13:11:05] “GET /%7B%22type%22:%20%22FeatureCollection%22,%20%22features%22:%20[%7B%22type%22:%20%22Feature%22,%20%22properties%22:%20%7B%22zoneID%22:%20126,%20%22Colors%22:%20%22 HTTP/1.1” 200 -
127.0.0.1 - - [02/Aug/2017 13:11:05] “GET /%7B%22type%22:%20%22FeatureCollection%22,%20%22features%22:%20[%7B%22type%22:%20%22Feature%22,%20%22properties%22:%20%7B%22zoneID%22:%20127,%20%22Colors%22:%20%22 HTTP/1.1” 200 -
127.0.0.1 - - [02/Aug/2017 13:11:05] “GET /%7B%22type%22:%20%22FeatureCollection%22,%20%22features%22:%20[%7B%22type%22:%20%22Feature%22,%20%22properties%22:%20%7B%22zoneID%22:%20128,%20%22Colors%22:%20%22 HTTP/1.1” 200 -

I am sure I am missing something small. Again any help you can provide would be greatly appreciated.
Thanks,
Chris

Hey @cris-ssg, when you convert your DataFrame to json you don’t get a geojson type structure.

This is the conversion of the first row:
'{"zoneID":{"0":126},"geometry":{"0":"POLYGON ((-64.69587606703536 46.11647015103159, -64.70656143382793 46.12350078478621,...
Here https://en.wikipedia.org/wiki/GeoJSON you can see how the description of a Polygon should look like.

Thanks empet,

I was getting that format, the problem was that the variable was a string and not a dictionary .

Thanks again for your help,

Chris

Here’s a short Jupyter notebook tutorial for creating US county-level choropleth maps in Python:

image

1 Like

It’s more of a pain to code, but you can also create county choropleth maps in Python using scattermapbox. These choropleths load and zoom faster compared to the SVG versions above. Here’s a Dash app that uses one:

https://opioid-epidemic.herokuapp.com/

Code here: https://github.com/plotly/dash-opioid-epidemic-demo/blob/master/app.py

1 Like

For my first choropleth maps in Dash I created a separate layer for each area, but ran into severe performance issues as the number of layers increased. Roughly 10 were OK, more than 50 would freeze my browser for about 20 seconds.

Creating a single layer for each color in the map works well. Requires some preprocessing of data, and as empet mentioned you have to create each source as a geojson type dict. I’ll post a full example next week when I have time, but here is basically what I did. I also start my layers with an outline so you can see boundaries, totally optional:

outline=dict(
    sourcetype = 'geojson',
    source=geojson_converted_to_a_dict,
    below="water", 
    type = 'line',   
    color = 'black',
    width=5
)

layers = [outline]

# Dictionary with each color as an empty list
layers_by_color = {c: [] for c in colors}

for obj in my_objects:
   color = figure_out_color_for_this_object()
   layers_by_color[color].append(obj.geojson_as_object)

# obj.geojson_as_object has keys for type and coordiantes. Type is 'Pologon' or 'Multipolygon'

for color in colors:
    geojson_dict = dict(type="FeatureCollection",features=layers_by_color[color])
    layers.append(dict(
        sourcetype = 'geojson',
        source=geojson_dict,
        type='fill',
        opacity=1,
        color=color
    ))

2 Likes

Is there a way to make one of these maps, but instead of showing map traces, just use a static JPEG image background?

I ran into severe performance issues with layers too.

You say that your best solution is to sample the values of the cells of the choropleth to around 10 and regroup similar cells.
Is that much faster to plot discontinued layers than to plot several layers?

My idea instead was to grab the real boundaries of the map and plot only the data that fall within the boundary box. However, I couldn’t find a way to retrieve the boundaries apart from estimating it from the center lat lon and zoom. At lower zooms, you would regroup cells since you loose the resolution anyway.

EDIT: I tried the solution of merging patchs of similar values. It is indeed much faster. Yet, if I still want to have some hoverinfo, I am still forced to draw as many points as I have patches initially which keep the graph super slow.

I updated my posts above with the latest recommended ways to create county-level choropleth maps for Dash apps.

@Rom1 check out this for faster county-level choropleth performance:

https://opioid-epidemic.herokuapp.com/

Code here: https://github.com/plotly/dash-opioid-epidemic-demo/blob/master/app.py

3 Likes

@jack Thanks for the solution and the code.
Do you know how many points you have in the graph? I’d guess around 3 000. In that case, with my data and solution, it’s also quite fast. I start to run into problems when having more than 10 000 cells and associate points.

I also group the layers under 20 bins with similar values. If I have only the layers shown, that’s still quite fast. I really struggle having as many points over. All in all, I believe our solutions are similar, unless I am missing a key element.

By the way, I really like the opacity slider solution.

Thanks

1 Like

Hey @jack,

I tried your app and I love it! However, when I change your json source with mine the map doesnt update colors anymore. Same for the legend. But the graph on the side has no issue. My geojson respects the exact structure as yours. If you have any lead on this please let me know.

Thanks!

@Rom1 Sorry I missed this. It is about 3000 points (one for each county in the US).

@dimayv is working on fills for scattergl, which would provide another pathway besides mapboxgl for creating WebGL choropleths. It will be a few weeks until this is done and I’ll try to remember to update this thread with the results for choropleth maps.

@Lamedz hard to say without seeing the code. I do remember that mapboxgl had an issue with some geojson files that I tried, even though they passed geojson linters. I don’t think I ever got to the bottom of this unfortunately though, if that is even the issue you’re encountering.

Hey @jack,

Basically I get the first display of the data, however the map is not interactive. So I see the map with the initial data inserted but when I change colors or legends it doesnt work.

Here is the code with an excerpt of geojson. What do you think?

@app.callback(
Output(‘county-choropleth’, ‘figure’),

[dash.dependencies.Input('button-1', 'n_clicks')],
    state=[
    State('opacity-slider', 'value'),
    State('colorscale-picker', 'colorscale'),
    State('hide-map-legend', 'values'),
    State('county-choropleth', 'figure')])

def display_map(nclick,opacity, colorscale, map_checklist, figure):
cm = dict(zip(BINS, colorscale))
df_2015=pd.read_csv(‘df.csv’)
df_2015.coordinates=df_2015.coordinates.apply(lambda x: ast.literal_eval(x))
data = [dict(
lat = df_lat_lon['Latitude '],
lon = df_lat_lon[‘Longitude’],
text = df_lat_lon[‘Hover’],
type = ‘scattermapbox’,
hoverinfo = ‘text’,
marker = dict(size=5, color=‘white’, opacity=0)
)]

annotations = [dict(
    showarrow = False,
    align = 'right',
    text = '',
    x = 0.95,
    y = 0.95,
)]

for i, bin in enumerate(reversed(BINS)):
    color = cm[bin]
    annotations.append(
        dict(
            arrowcolor = color,
            text = bin,
            x = 0.95,
            y = 0.85-(i/20),
            ax = -60,
            ay = 0,
            arrowwidth = 5,
            arrowhead = 0,
            bgcolor = '#EFEFEE'
        )
    )

if 'hide_legend' in map_checklist:
    annotations = []

if 'layout' in figure:
    lat = figure['layout']['mapbox']['center']['lat']
    lon = figure['layout']['mapbox']['center']['lon']
    zoom = figure['layout']['mapbox']['zoom']
else:
    lat = 38.72490,
    lon = -95.61446,
    zoom = 2.5

layout = dict(
    mapbox = dict(
        layers = [],
        accesstoken = mapbox_access_token,
        style = 'light',
        center=dict(lat=lat, lon=lon),
        zoom=zoom
    ),
    hovermode = 'closest',
    margin = dict(r=0, l=0, t=0, b=0),
    annotations = annotations,
    dragmode = 'lasso'
)

for i,bin in enumerate(BINS):
   df=df_2015.loc[int(i*len(df_2015)/len(BINS)):int((i+1)*len(df_2015)/len(BINS))].reset_index()   
   l={"type": "FeatureCollection",
       "crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:OGC:1.3:CRS84" } },'features':df['coordinates'].tolist()}
   geo_layer = dict(
       sourcetype = 'geojson',
       source = l,
       type = 'fill',
       color = cm[bin],
       opacity = opacity
   )
   layout['mapbox']['layers'].append(geo_layer)

fig = dict(data=data, layout=layout)
return fig

Here is a part of the geojson
{‘crs’: {‘properties’: {‘name’: ‘urn:ogc:def:crs:OGC:1.3:CRS84’},
‘type’: ‘name’},
‘features’: [{‘geometry’: {‘coordinates’: [[[-100.093183, 35.181919],
[-100.000075, 35.181919],
[-100.000075, 35.028564],
[-100.000075, 34.743763],
[-100.416322, 34.74924],
[-100.542292, 34.74924],
[-100.536815, 35.181919],
[-100.093183, 35.181919]]],
‘type’: ‘Polygon’},
‘properties’: {‘County’: ‘Collingsworth County,TX’,
‘FIPS’: ‘48087’,
‘FIPS State’: 48,
‘ST’: ‘TX’,
‘State’: ‘Texas’,
‘id’: ‘48087’,
‘name’: ‘Collingsworth’},
‘type’: ‘Feature’},
{‘geometry’: {‘coordinates’: [[[[-84.286733, 32.750157],
[-84.204579, 32.689911],
[-84.001932, 32.531079],
[-84.051224, 32.520126],
[-84.253871, 32.372248],
[-84.36341, 32.399633],
[-84.390795, 32.416064],
[-84.445564, 32.563941],
[-84.286733, 32.750157]]],
[[[-84.051224, 32.520126],
[-84.007409, 32.520126],
[-84.018363, 32.503695],
[-84.051224, 32.520126],
[-84.051224, 32.520126]]]],
‘type’: ‘MultiPolygon’},
‘properties’: {‘County’: ‘Taylor County,GA’,
‘FIPS’: ‘13269’,
‘FIPS State’: 13,
‘ST’: ‘GA’,
‘State’: ‘Georgia’,
‘id’: ‘13269’,
‘name’: ‘Taylor’},
‘type’: ‘Feature’}}