Plotly Choropleth地图:缺失值区域处理、多列数据提示及年份动画实现技术问询
Hey, let's work through each of your questions with practical code snippets and straightforward explanations:
1. Show Country Names on Hover for Missing Value Regions
The problem here is that Plotly's Choropleth only generates hover data for countries present in your dataset. To fix this, we can add a secondary Scattergeo trace that covers every country (including those with missing data) — we'll just hide the visible elements of this trace so it only contributes hover text.
Here's the adjusted code:
import plotly.graph_objects as go import geopandas as gpd # Get a full list of ISO3 country codes and names using GeoPandas' built-in dataset world = gpd.read_file(gpd.datasets.get_path('naturalearth_lowres')) full_countries = world[['name', 'iso_a3']].rename(columns={'name':'Zone', 'iso_a3':'Code zone (ISO3)'}) full_countries = full_countries.dropna(subset=['Code zone (ISO3)']) # Your original choropleth trace choropleth_trace = go.Choropleth( locations = world_map_df_sous_nutrition["Code zone (ISO3)"], z = round((world_map_df_sous_nutrition["Proportion pop en sous-nutrition"]),2), text = world_map_df_sous_nutrition["Zone"], colorscale = "earth", autocolorscale = False, reversescale = True, marker_line_color = "white", marker_line_width = .2, colorbar_tickprefix = "%", colorbar_title = "Proportion de personnes en sous nutrition" ) # Scattergeo trace to add hover text for all countries scatter_trace = go.Scattergeo( locations = full_countries["Code zone (ISO3)"], text = full_countries["Zone"], mode = 'text', textfont = {'color':'rgba(0,0,0,0)'}, # Make text invisible hoverinfo = 'text', marker = {'opacity':0} # Hide markers entirely ) # Combine both traces in the figure fig = go.Figure(data=[choropleth_trace, scatter_trace]) # Your existing layout code remains the same fig.update_layout( title_text="L'état de la sous-nutrition dans le monde en 2017", geo=dict( landcolor = 'lightgray', showland = True, showcountries = True, countrycolor = 'gray', countrywidth = 0.5, showframe=False, showcoastlines=False, projection_type='equirectangular' ), annotations = [dict( x=0.55, y=0.1, xref='paper', yref='paper', text='Source: FAO', showarrow = False )], margin={"r":0,"t":0,"l":0,"b":0}, ) fig.show()
Now when you hover over any country (even missing data regions), you'll see the country name pop up.
2. Replicate GeoPandas Missing Value Styles in Plotly
Plotly doesn't support hatch patterns directly in Choropleth traces, but we can combine multiple traces to mimic most of the GeoPandas style:
- A base Choropleth trace for countries with valid data
- A second Choropleth trace for missing countries, styled with light gray fill and red borders
- A custom legend entry to label missing regions
Here's the code:
import plotly.graph_objects as go import geopandas as gpd # Get full country list world = gpd.read_file(gpd.datasets.get_path('naturalearth_lowres')) full_countries = world[['name', 'iso_a3']].rename(columns={'name':'Zone', 'iso_a3':'Code zone (ISO3)'}) full_countries = full_countries.dropna(subset=['Code zone (ISO3)']) # Split your data into present and missing values present_df = world_map_df_sous_nutrition.dropna(subset=["Proportion pop en sous-nutrition"]) missing_df = full_countries[~full_countries["Code zone (ISO3)"].isin(present_df["Code zone (ISO3)"])] # Trace for countries with valid data present_trace = go.Choropleth( locations = present_df["Code zone (ISO3)"], z = round(present_df["Proportion pop en sous-nutrition"],2), text = present_df["Zone"], colorscale = "earth", autocolorscale = False, reversescale = True, marker_line_color = "white", marker_line_width = .2, colorbar_tickprefix = "%", colorbar_title = "Proportion de personnes en sous nutrition", name = "Data Available" ) # Trace for missing countries missing_trace = go.Choropleth( locations = missing_df["Code zone (ISO3)"], z = [0]*len(missing_df), # Dummy value to lock color text = missing_df["Zone"] + "<br>Missing values", colorscale = [[0, 'lightgray'], [1, 'lightgray']], # Fixed light gray fill autocolorscale = False, marker_line_color = "red", marker_line_width = 1, showscale = False, # Hide extra colorbar name = "Missing Values" ) fig = go.Figure(data=[present_trace, missing_trace]) fig.update_layout( title_text="L'état de la sous-nutrition dans le monde en 2017", geo=dict( showland=False, # Disable default land since we use custom traces showcountries=False, showframe=False, showcoastlines=False, projection_type='equirectangular' ), annotations = [dict( x=0.55, y=0.1, xref='paper', yref='paper', text='Source: FAO', showarrow = False )], margin={"r":0,"t":0,"l":0,"b":0}, legend=dict( title="Status", x=0.1, y=0.1 ) ) fig.show()
Note: Plotly doesn't support hatch patterns in choropleths yet, so this is the closest we can get without complex SVG overlays.
3. Add Multiple Column Values to Hover & Switch Coloring Modes
Adding Multiple Columns to Hover
Simply modify the text parameter in your Choropleth trace to include all columns you want to display. Use <br> to create line breaks for readability:
text = ( world_map_df_sous_nutrition["Zone"] + "<br>Sous-nutrition: " + round(world_map_df_sous_nutrition["Proportion pop en sous-nutrition"],2).astype(str) + "%" + "<br>Population: " + world_map_df_sous_nutrition["Population"].astype(str) )
Plotly will automatically show this combined text when you hover over a country.
Switching Coloring Modes
You can add a dropdown menu to let users switch between different columns for coloring. Here's how:
import plotly.graph_objects as go # Define columns you want to switch between color_modes = [ {"label": "Sous-nutrition", "col": "Proportion pop en sous-nutrition", "unit": "%"}, {"label": "Population", "col": "Population", "unit": ""} ] # Initial trace with first column initial_mode = color_modes[0] fig = go.Figure(data=go.Choropleth( locations = world_map_df_sous_nutrition["Code zone (ISO3)"], z = round(world_map_df_sous_nutrition[initial_mode["col"]],2), text = ( world_map_df_sous_nutrition["Zone"] + "<br>" + initial_mode["label"] + ": " + round(world_map_df_sous_nutrition[initial_mode["col"]],2).astype(str) + initial_mode["unit"] ), colorscale = "earth", autocolorscale = False, reversescale = True, marker_line_color = "white", marker_line_width = .2, colorbar_tickprefix = initial_mode["unit"], colorbar_title = initial_mode["label"] )) # Create dropdown buttons buttons = [] for mode in color_modes: button = dict( label=mode["label"], method="update", args=[ {"z": [round(world_map_df_sous_nutrition[mode["col"]],2)]}, { "colorbar": {"tickprefix": mode["unit"], "title": mode["label"]}, "data[0].text": [ world_map_df_sous_nutrition["Zone"] + "<br>" + mode["label"] + ": " + round(world_map_df_sous_nutrition[mode["col"]],2).astype(str) + mode["unit"] ] } ] ) buttons.append(button) fig.update_layout( title_text="L'état de la sous-nutrition dans le monde en 2017", geo=dict( landcolor = 'lightgray', showland = True, showcountries = True, countrycolor = 'gray', countrywidth = 0.5, showframe=False, showcoastlines=False, projection_type='equirectangular' ), annotations = [dict( x=0.55, y=0.1, xref='paper', yref='paper', text='Source: FAO', showarrow = False )], margin={"r":0,"t":0,"l":0,"b":0}, updatemenus=[dict( buttons=buttons, direction="down", x=0.1, y=1.1 )] ) fig.show()
This adds a dropdown at the top of the figure that updates the coloring, hover text, and colorbar when users select a different metric.
4. Animated Choropleth Map by Year
Plotly supports animated choropleths using the frame parameter. First, make sure your data is in long format (one row per country per year). Then use this code:
import plotly.graph_objects as go import geopandas as gpd # Assume your data is in long format: columns = ["Code zone (ISO3)", "Zone", "Year", "Proportion pop en sous-nutrition"] # Get full country list (same as before) world = gpd.read_file(gpd.datasets.get_path('naturalearth_lowres')) full_countries = world[['name', 'iso_a3']].rename(columns={'name':'Zone', 'iso_a3':'Code zone (ISO3)'}) full_countries = full_countries.dropna(subset=['Code zone (ISO3)']) # Get unique sorted years years = sorted(world_map_df_long["Year"].unique()) # Create frames for each year frames = [] for year in years: year_data = world_map_df_long[world_map_df_long["Year"] == year] # Merge with full countries to ensure all regions are covered year_data_merged = full_countries.merge(year_data, on="Code zone (ISO3)", how="left") frame = go.Frame( data=[go.Choropleth( locations = year_data_merged["Code zone (ISO3)"], z = round(year_data_merged["Proportion pop en sous-nutrition"],2), text = ( year_data_merged["Zone_x"] + "<br>Année: " + str(year) + "<br>Sous-nutrition: " + year_data_merged["Proportion pop en sous-nutrition"].fillna("Missing").astype(str) + "%" ), colorscale = "earth", autocolorscale = False, reversescale = True, marker_line_color = "white", marker_line_width = .2, colorbar_tickprefix = "%", colorbar_title = "Proportion de personnes en sous nutrition" )], name=str(year) ) frames.append(frame) # Initial figure (first year) initial_year = years[0] initial_data = world_map_df_long[world_map_df_long["Year"] == initial_year] initial_data_merged = full_countries.merge(initial_data, on="Code zone (ISO3)", how="left") fig = go.Figure( data=[go.Choropleth( locations = initial_data_merged["Code zone (ISO3)"], z = round(initial_data_merged["Proportion pop en sous-nutrition"],2), text = ( initial_data_merged["Zone_x"] + "<br>Année: " + str(initial_year) + "<br>Sous-nutrition: " + initial_data_merged["Proportion pop en sous-nutrition"].fillna("Missing").astype(str) + "%" ), colorscale = "earth", autocolorscale = False, reversescale = True, marker_line_color = "white", marker_line_width = .2, colorbar_tickprefix = "%", colorbar_title = "Proportion de personnes en sous nutrition" )], frames=frames, layout=go.Layout( title_text="Evolution de la sous-nutrition dans le monde", geo=dict( landcolor = 'lightgray', showland = True, showcountries = True, countrycolor = 'gray', countrywidth = 0.5, showframe=False, showcoastlines=False, projection_type='equirectangular' ), annotations = [dict( x=0.55, y=0.1, xref='paper', yref='paper', text='Source: FAO', showarrow = False )], margin={"r":0,"t":0,"l":0,"b":0}, updatemenus=[dict( type="buttons", showactive=True, buttons=[dict( label="Play", method="animate", args=[None, {"frame": {"duration": 500, "redraw": True}, "fromcurrent": True}] ), dict( label="Pause", method="animate", args=[[None], {"frame": {"duration": 0, "redraw": False}, "mode": "immediate"}] )] )], sliders=[dict( steps=[dict( method="animate", args=[[str(year)], {"frame": {"duration": 500, "redraw": True}, "mode": "immediate"}], label=str(year) ) for year in years], active=0, currentvalue={"prefix": "Année: "} )] ) ) fig.show()
This creates an animated map with play/pause buttons and a year slider, letting users watch the data evolve over time.
内容的提问来源于stack exchange,提问作者ElMeTeOr

