Programmation Python avancée | Chapitre 11

Cette page, associée au livre Programmation Python avancée – Guide pour une pratique élégante et efficace aux éditions Dunod (ISBN 978-2-10-081598-2), contient les ressources complémentaires et le code source des exemples présentés.

View the Project on GitHub xoolive/python

« Retour

11. La visualisation interactive avec Altair et ipyleaflet

La place croissante que prend Jupyter dans l’écosystème Python et des technologies du web fait la part belle à des outils de visualisation interactive. Nous présentons ici deux de ces bibliothèques:

À l’instar de Pandas, une présentation complète d’Altair en quelques pages relève de la gageure. Elle ne remplace pas la riche documentation de la bibliothèque accessible sur le site https://altair-viz.org. Ce chapitre propose une simple introduction des possibilités de cette bibliothèque, basée sur un jeu de données1 rendu célèbre par le chercheur suédois Hans Rosling2. Ce fichier comprend, par année et par pays, des données de population, d’espérance de vie et de PIB par habitant (rapportées en équivalent en dollars de 2011).

Altair est une grammaire graphique, c’est-à-dire un langage qui décrit une visualisation de données avant de l’appliquer à un jeu de données particulier. Elle est construite autour de la bibliothèque Pandas, prend en paramètre des pd.DataFrame et produit, à l’aide des bibliothèques Javascript Vega Lite et D3.js, une visualisation de données sur le web. Elle peut également prendre en paramètre des URL vers des données ordonnées au format JSON, accessibles sur le Net.

Le point de départ de la bibliothèque sera alors un jeu de données caractérisé par le mot-clé anglais tidy (rangé): cela signifie que les données brutes ont déjà été prétraitées, filtrées, ordonnées pour produire des points qui s’approchent au plus près de la définition de la visualisation. On manipulera alors:

Il convient de garder en mémoire les limitations classiques actuelles des moteurs de rendus Javascript: à l’heure actuelle (2021), il faudra certainement se limiter à des visualisations qui manipulent un ordre de grandeur de 100 000 points.

Le fichier fourni sur la page web précédente comprend quelques incohérences, des valeurs manquantes (on reconstruit notamment la colonne continent), et on ne s’intéressera qu’aux points situés entre 1950 et 2015, avec des valeurs présentes de population: le code Pandas qui construit les données utilisées pour les visualisations de cette page est fourni ci-dessous:

data = pd.read_csv(
    "life-expectancy-vs-gdp-per-capita.csv",
    header=1,
    names=[
        "country", "country_code", "year", "population", "continent",
        "life_expectancy", "GDP_per_capita"
    ],
    parse_dates=["year"],
)
continents = data.query("continent == continent").groupby("country").agg({"continent": "max"})
data = (
    data.drop(columns="continent")
    .merge(continents.reset_index(), on="country")
    .query("'1950' <= year <= '2015' and population == population")
)

data.head()
country country_code year population life_expectancy GDP_per_capita continent
150 Afghanistan AFG 1950 7752000.0 27.638 2392.0 Asia
151 Afghanistan AFG 1951 7840000.0 27.878 2422.0 Asia
152 Afghanistan AFG 1952 7936000.0 28.361 2462.0 Asia
153 Afghanistan AFG 1953 8040000.0 28.852 2568.0 Asia
154 Afghanistan AFG 1954 8151000.0 29.350 2576.0 Asia

L’usage est d’importer la bibliothèque Altair sous l’alias alt:

>>> import altair as alt

11.1 Encodages et marques

Les visualisations Altair sont basées sur trois types de données:

Dans l’exemple suivant, un nuage de points .mark_point() sur les données réduites à l’année 2015, on associe l’abscisse x, l’ordonnée y et la couleur color chacune à une caractéristique (le PIB par habitant, l’espérance de vie et le continent). C’est la bibliothèque qui se charge d’interpréter la description pour fournir une visualisation conforme.

data_2015 = data.query('year == "2015"')

alt.Chart(data_2015).encode(
    x="GDP_per_capita",
    y="life_expectancy",
    color="continent"
).mark_point()

Les marques les plus fréquentes ont toutes un nom explicite, qui n’appelle pas nécessairement d’explication approfondie:

 
mark_point()
mark_circle()
mark_square()
mark_line()
mark_area()
mark_bar()
mark_tick()

Les canaux d’encodage les plus fréquents sont:

   
x abscisse
y ordonnée
couleur couleur de la marque
opacity transparence/opacité de la marque
shape forme de la marque
size taille de la marque
facet répétition du canal

Il est possible d’utiliser des arguments nommés pour les canaux sur le modèle x="x_data", ou d’utiliser les constructeurs Altair associés alt.X("x_data") en paramètres nommés ou non, qui permettent également de passer des arguments supplémentaires:

Altair utilise le type de chacune des features à partir des dtype Pandas. Il est possible de les spécifier néanmoins, et cette étape est nécessaire si les données sont passées par fichier:

data_france = data.query('country == "France"')

alt.Chart(data_france).encode(
    alt.X("year:T", title="année"),  # ①
    alt.Y(
        "population:Q",
        scale=alt.Scale(zero=False),  # ②
        axis=alt.Axis(format="~s")  # ③
    ),
).mark_line()

Un nuage de points sans encodage affiche un simple point. Bien que cette entrée ne renvoie pas d’erreur, elle n’est pas pertinente en soi.

alt.Chart(data).mark_point()

Pour un encodage de données nominales, une coordonnée est attribuée à chaque élément unique de la feature. Les plages de couleurs sont également choisies en fonction, pour distinguer clairement une catégorie d’une autre.

alt.Chart(data).encode(
    alt.Y("continent:N"),
    alt.Color("continent:N")
).mark_square()

11.2 Agrégation et composition

L’agrégation de données correspond à l’opération groupby() en Pandas. Altair permet de définir ce type d’opération à calculer sur les données préparées passées en paramètre. Le calcul est alors effectué par la bibliothèque Javascript de visualisation au lieu de l’être par Pandas. L’avantage principal est que le volume des données produites pour créer toutes les visualisations est réduit.

Dans l’exemple ci-dessous, la préparation de données équivalente avant visualisation serait, pour un calcul de valeur médiane:

data_2015.groupby("continent").agg({"GDP_per_capita": "median"})
continent GDP_per_capita
Africa 2954.0
Asia 11738.0
Europe 26240.0
North America 10358.5
Oceania 38890.5
South America 14117.5
alt.Chart(data_2015).encode(
    alt.X(
        "median(GDP_per_capita):Q",
        title="PIB par habitant médian en 2015", axis=alt.Axis(format="~s"),
    ),
    alt.Y("continent:N"),
    alt.Color("continent:N"),
).mark_bar(size=10)

D’autres opérateurs d’agrégation sont disponibles, notamment pour la somme sum, le produit product, la moyenne mean, le minimum min, le maximum max, le nombre d’éléments vides missing, ou le nombre d’éléments distincts distinct.

L’exemple suivant affiche le nombre de pays par continent. Chaque pays est représenté de nombreuses fois dans le fichier (une fois par année) mais l’opérateur distinct comprend cette nuance.

alt.Chart(data).encode(
    alt.X("distinct(country):N", title="Nombre de pays"),
    alt.Y("continent:N"),
    alt.Color("continent:N"),
).mark_bar(size=10)

Il est possible de produire des agrégations quel que soit le canal d’encodage. Dans la visualisation suivante, l’écart-type est encodé dans la couleur des barres. Comme le PIB par habitant est annoté comme type de données quantitatif, Altair choisit une table de couleur adaptée qui fait varier la saturation de la couleur, par opposition au type de données nominatif qui fournit une table de couleur faisant varier la teinte.

Cet exemple est aussi l’occasion de préciser deux autres options:

alt.Chart(data_2015).encode(
    alt.X(
        "mean(GDP_per_capita):Q",
        axis=alt.Axis(format="~s"), title="Moyenne du PIB par habitant",
    ),
    alt.Y("continent:N", sort="-x"),  # ①
    alt.Color(
        "stdev(GDP_per_capita):Q", title="Écart-type",
        scale=alt.Scale(domain=(0, 30e3))  # ②
    ),
).mark_bar(size=10)

L’Asie semble être le continent avec le plus d’inégalités de richesse. Un diagramme en boîte permet de visualiser différemment les données: les éléments atypiques sortent des boîtes à moustache et le canal d’encodage tooltip permet d’afficher le nom du pays quand on passe la souris sur le point. Le Qatar en Asie et la Norvège en Europe par exemple sont des pays au PIB par habitant très supérieur à celui des voisins.

alt.Chart(data_2015).encode(
    alt.X("GDP_per_capita:Q", axis=alt.Axis(format="~s"), title="PIB par habitant",),
    alt.Y("continent:N"),
    alt.Tooltip("country:N"),
).mark_boxplot(size=10)

Le tracé d’histogrammes est vu par Altair du point de vue d’une agrégation particulière où les échantillons sont répartis en classes (le mot-clé bin en anglais, déjà vu avec Matplotlib, puis la méthode d’agrégation sans argument count()): il faut donc préciser cette agrégation pour visualiser des distributions.

Une autre fonctionnalité permise par Altair est la composition de graphes:

Lors de composition de graphes, il est possible de factoriser des spécifications. Dans l’exemple suivant, le même graphe est affiché deux fois, la visualisation de droite ajoute un canal d’encodage de couleur ①. On notera également l’utilisation de la fonction .properties ② qui permet entre autres de spécifier la taille de la fenêtre.

base = (
    alt.Chart(data_2015)
    .encode(
        alt.X(
            "GDP_per_capita", bin=alt.Bin(maxbins=30),
            title="PIB par habitant", axis=alt.Axis(format="$~s"),
        ),
        alt.Y("count()", title="Nombre de pays"),
    )
    .mark_bar()
    .properties(width=280, height=200)  # ②
)

base | base.encode(alt.Color("continent"))  # ①

Il est également possible de changer de marque entre deux visualisations factorisées, ou de surcharger des encodages ou personnalisations précédemment spécifiées.

# Définition de la partie commune aux visualisations
base = (
    alt.Chart(data_2015)
    .encode(
        alt.X(
            "sum(population):Q",
            title="Population totale en 2015", axis=alt.Axis(format="~s"),
        ),
        alt.Color("continent:N", legend=None),
    )
    .mark_bar(size=10)
    .properties(width=280)
)

(
    base.encode(alt.Y("continent:N", title=None))
    | base.encode(alt.X("population:Q"), alt.Y("continent:N")).mark_point()
) & base.properties(width=680)

11.3 Transformation

Nous avons vu avec les méthodes d’agrégation que les visualisations peuvent appeler des calculs intermédiaires sur les données d’origine. Ces calculs peuvent être faits via Pandas avant de programmer une visualisation, ou au sein de la visualisation à l’aide de nombreuses fonctions de transformation Altair spécifiques.

Les fonctions de transformation (les mots-clés suivant le modèle .transform_) changent la structure des données d’entrée pour y ajouter de nouvelles colonnes, ou features, filtrer ou trier des lignes suivant un critère, ou opérer des jointures sur d’autres tables.

Les principales fonctions de transformation sont:

   
transform_aggregate() agrégation d’une colonne avec écrasement
transform_joinaggregate() agrégation d’une colonne dans une nouvelle colonne
transform_calculate() calcul d’une nouvelle grandeur
transform_density() calcul d’une estimation de densité
transform_window() calcul d’un critère par fenêtre (sous-ensemble de lignes)

Dans l’exemple suivant, on crée une nouvelle feature avec la population moyenne par pays dans l’intervalle d’années considéré, afin d’ordonner les pays avec le plus peuplé en moyenne en bas de l’affichage et le moins peuplé en haut. La transformation joinaggregate permet de conserver la feature population malgré le calcul de sa version agrégée.

La deuxième transformation filter permet de ne sélectionner que les pays d’Europe avec plus de 50 millions d’habitants en moyenne. Le paramètre datum fait référence au jeu de données embarqués dans le constructeur alt.Chart.

(
    alt.Chart(data)
    .encode(
        alt.X("year:T", title="année"),
        alt.Y("population:Q", axis=alt.Axis(format="~s")),
        alt.Color("country:N", title="pays"),
        alt.Order("mean_pop:Q", sort="descending"),
    )
    .mark_area()
    .transform_joinaggregate(mean_pop="mean(population)", groupby=["country"])
    .transform_filter({"and": ["datum.continent == 'Europe'", "datum.mean_pop > 50e6"]})
    .properties(width=400, height=200)
)

L’exemple suivant met en application toutes les notions vues jusqu’ici. On cherche à afficher les dix premiers pays suivant un critère donné sur le même jeu de données. Ici aucun prétraitement Pandas n’a été réalisé. Tout est spécifié dans Altair:

base = (
    alt.Chart(data)
    .mark_bar(size=10)
    .encode(alt.Y("country:N", sort="-x", title="pays"), alt.Color("continent:N"),)  # ①
    .transform_aggregate(  # ②
        most_recent_year="argmax(year)", groupby=["country", "continent"]
    )
    .transform_calculate(  # ③
        population="datum.most_recent_year.population",
        GDP_per_capita="datum.most_recent_year.GDP_per_capita",
    )
    .transform_window(  # ④
        rank_pop="rank(population)",
        sort=[alt.SortField("population", order="descending")],
    )
    .transform_window(
        rank_gdp="rank(GDP_per_capita)",
        sort=[alt.SortField("GDP_per_capita", order="descending")],
    )
    .properties(width=300, height=200)
)

(
    base.encode(alt.X("population:Q", axis=alt.Axis(format="~s"))).transform_filter(
        alt.datum.rank_pop <= 10  # ⑤
    )
    | base.encode(
        alt.X("GDP_per_capita:Q", axis=alt.Axis(format="$~s"), title="PIB par habitant")
    ).transform_filter(
        alt.datum.rank_gdp <= 10  # ⑤
    )
)

11.4 Interactivité

L’interactivité la plus simple est celle qui est induite par l’encodage de canal tooltip: au passage de la souris sur un point donné, un pop-up apparaît avec les informations spécifiées. La documentation montre de nombreux exemples d’interactivité, basés sur les mouvements de la souris, la sélection d’intervalles, ou d’autres.

L’exemple ci-dessous reprend le type de visualisation avec lequel Hans Rosling s’est illustré lors de plusieurs conférences TED: un point correspond à un pays, sa taille à sa population. Ici, on place en $x$ le PIB par habitant et en $y$ l’espérance de vie dans le pays.

On souhaite animer la visualisation par année pour pouvoir suivre la trajectoire de chacun de ces pays dans cette espace. Cette sélection se fait au moyen d’un widget où la poignée sélectionne l’année ①. La méthode selection_single réagit en attribuant au champ year la valeur positionnée sur le widget: la visualisation est alors mise à jour quand la valeur du champ change.

L’encodage est basique ②; le nom du pays s’affiche quand on passe la souris sur un point; le canal text servira à annoter certains points de manière permanente, directement sur la visualisation; l’axe des abscisses est choisi logarithmique. Au lieu de choisir la dernière année qui contient des données, on choisit les données de l’année sélectionnée par le widget. ③

La visualisation est alors constituée de deux couches: les cercles de couleur (par continent) à la taille proportionnelle à la population du pays (en échelle logarithmique); et une annotation textuelle pour certains pays dont la trajectoire reflète le cours de l’histoire (chute de l’URSS, Khmers rouges au Cambodge, fin de l’Apartheid en Afrique du Sud, essor économique spectaculaire de la Corée du Sud).

annotate_countries = [
    "South Africa", "United States", "France", "China", "Russia", "Nigeria",
    "Brazil", "South Korea", "Japan", "India", "Cambodia",
]

year_slider = alt.binding_range(min=1950, max=2015, step=1, name="year:")  # ①
year_selector = alt.selection_single(
    name="year_selection", fields=["year"], bind=year_slider, init={"year": 2000}
)

base = (
    alt.Chart(data)
    .encode(  # ②
        alt.X(
            "GDP_per_capita:Q", axis=alt.Axis(format="k"),
            scale=alt.Scale(type="log", domain=(100, 1e5)), title="PIB par habitant",
        ),
        alt.Y(
            "life_expectancy:Q",
            scale=alt.Scale(domain=(20, 90)), title="Espérance de vie",
        ),
        alt.Text("country:N"),
        alt.Tooltip("country:N"),
    )
    .transform_filter("datum.year == year_selection.year")  #  ③
    .properties(width=600, height=400)
)
(
    base.mark_circle()
    .encode(
        alt.Color("continent:N"),
        alt.Size("population:Q", scale=alt.Scale(domain=(5e6, 1e9), type="log")),
    )
    .add_selection(year_selector)
    + base.transform_filter(
        {"field": "country", "oneOf": annotate_countries}
    ).mark_text(size=14, align="right", xOffset=-10, font="Ubuntu")
)

11.5 Configuration

Il est possible de personnaliser l’affichage d’une visualisation Altair à trois niveaux:

La dernière méthode est souvent la manière privilégiée de procéder à des microajustements, par exemple sur la taille des polices, le positionnement des étiquettes.

base = (
    alt.Chart(data_france)
    .encode(
        alt.X("year:T", title="année"),
        alt.Y("population:Q", scale=alt.Scale(zero=False), axis=alt.Axis(format="~s")),
    )
    .mark_line()
)
(
    base.properties(title="Population française", height=200, width=300)
    .configure_axis(labelFontSize=12, titleFontSize=0, labelAngle=-30)
    .configure_line(size=3, color="#008f6b")
    .configure_title(anchor="start", fontSize=16, font="Fira Sans", color="#008f6b")
    .configure_view(stroke=None)
)

11.6 Coordonnées géographiques

Le support pour les structures de données géographiques dans Altair est encore jeune à l’heure où ces lignes sont écrites. Il est néanmoins possible de produire des cartes à partir de fichiers au format standardisé pour décrire des informations géographiques, les formats les plus courants étant GeoJSON et TopoJSON.

Altair fournit alors:

La difficulté consiste alors ici à avoir accès à des fonds de carte pour créer des visualisations de qualité. La bibliothèque fournit parmi les jeux de données officiels vega_datasets une carte du monde de haut niveau et une carte des États-Unis à bonne résolution.

Pour un public francophone, on trouvera à l’heure de l’écriture de ces lignes:

L’utilisation de la bibliothèque geopandas3 (qui ne sera pas détaillée dans cet ouvrage) facilite la manipulation des fichiers GeoJSON et TopoJSON sous forme de tableau Pandas dont les colonnes sont les métadonnées, et une colonne particulière, généralement nommée geometry, contient une structure qui représente la forme de l’objet en question.

import geopandas as gpd

github_url = "https://raw.githubusercontent.com/{user}/{repo}/master/{path}"

regions_fr = gpd.GeoDataFrame.from_file(
    github_url.format(
        user="gregoiredavid", repo="france-geojson",
        path="regions-version-simplifiee.geojson",
    )
)
departements_fr = gpd.GeoDataFrame.from_file(
    github_url.format(
        user="gregoiredavid", repo="france-geojson",
        path="departements-version-simplifiee.geojson",
    )
)

belgique = gpd.GeoDataFrame.from_file(
    github_url.format(user="arneh61", repo="Belgium-Map", path="Provincies.json",)
).assign(
    centroid_lon=lambda df: df.geometry.centroid.x,
    centroid_lat=lambda df: df.geometry.centroid.y,
)

La structure geopandas peut être passée en argument de alt.Chart et il est alors possible d’utiliser la marque géographique. Ici, on choisit le nom de la région en encodage de la couleur. Dans l’exemple de la carte de la Belgique, on ajoute le nom de chaque province au centroïde de la forme géométrique. Le nom de Bruxelles est enlevé (transform_filter) pour ne pas se chevaucher avec celui du Brabant Flamand. Enfin, la coordonnée en latitude du texte est volontairement bruitée pour éviter les chevauchements (transform_calculate); des méthodes de placement d’étiquettes textuelles sans chevauchement plus complexes existent mais sortent du cadre de cet ouvrage.

base = alt.Chart(belgique)
(
    base.mark_geoshape(stroke="white").encode(alt.Color("NAME_1", title="Région"))
    + (
        base.encode(
            alt.Longitude("centroid_lon"),
            alt.Latitude("centroid_lat"),
            alt.Text("NAME_2"),
        )
        .mark_text(color="black", font="Ubuntu", fontSize=12)
        .transform_filter("datum.NAME_2 != 'Bruxelles'")
        .transform_calculate(centroid_lat="datum.centroid_lat + .1 * (random() - .5)")
    )
)

Contrairement à Matplotlib, la projection par défaut choisie pour les cartes est la projection de Mercator. L’utilisation de la projection plate carrée qui associe la latitude à la coordonnée y et la longitude à la coordonnée x est moins directe. La figure suivante compare à ce titre les trois projections plate carrée, inadaptée pour la plupart des usages, Mercator, qui fonctionne par défaut dans la plupart des régions du monde, et Lambert 93, définie ici manuellement, qui est la projection standard en France. Un graticule (une grille de lignes isolatitudes et isolongitudes) est ajouté ① pour donner une meilleure perception des opérations de projection.

base = (
    alt.Chart(
        alt.graticule(step=[2, 2], extentMajor=([-5, 41], [11, 52]))  # ①
    ).mark_geoshape(fill="None", stroke="#008f6b")
    + alt.Chart(regions_fr).mark_geoshape(
        stroke="white", fill="#008f6b", strokeWidth=1.2
    )
).properties(width=250, height=250)

(
    base.project("identity", reflectY=True).properties(title="Plate Carrée")
    | base.project("mercator").properties(title="Mercator (par défaut)")
    | (
        base.project("conicConformal", rotate=[-3, -46.5], parallels=[49, 44])
        .properties(title="Lambert 93")
    )
).configure_title(font="Ubuntu", fontSize=15, anchor="start")

L’affichage de fonds de carte est rarement une fin en soi. L’intérêt est de pouvoir afficher des informations supplémentaires. On peut ajouter une couche (layer) à notre fond de carte, et utiliser les canaux d’encodage latitude et longitude. Nous illustrons ici cette utilisation avec l’exemple des toponymes au suffixe -acum : une colonne est ajoutée pour catégoriser les villes par suffixe, l’encodage color se charge ensuite de la visualisation.

(
    (
        alt.Chart(regions_fr)
        .mark_geoshape(stroke="grey", fill="white")
        .project("conicConformal", rotate=[-3, -46.5], parallels=[49, 44])  # Lambert 93
        + alt.Chart(
            pd.concat(
                [
                    villes.query(
                        f"nom.str.contains('.{fin}$')", engine="python"
                    ).assign(suffixe=f"-{fin}")
                    for fin in ["ac", "ach", "acq", "ay", "az", "ecques"]
                ]
            )
        )
        .mark_circle(opacity=0.5)
        .encode(
            alt.Latitude("latitude"),
            alt.Longitude("longitude"),
            alt.Color("suffixe"),
            alt.Tooltip("nom"),
        )
    )
    .properties(title="Le suffixe -acum dans les toponymes en France")
    .configure_legend(
        labelFont="Ubuntu", titleFont="Ubuntu", labelFontSize=13, titleFontSize=14
    )
    .configure_title(font="Ubuntu", fontSize=16, anchor="start")
    .configure_view(stroke=None)
)

L’interaction entre les données et les fonds de carte peut être plus marquée, comme dans les cartes choroplèthes qui associent une couleur à une région géographique. On peut ici reprendre les statistiques de population par département français  pour associer chaque mesure à un polygone affiché sur la carte.

Cette association se fait ici sur la base du numéro du département (dans la colonne departement) qui est présent dans le pd.DataFrame et dans le fichier GeoJSON des départements (dans la propriété code) à l’aide de la fonction .transform_lookup.

feature_list = ["altitude_max", "population"]

chart = (
    alt.Chart(departements_fr)
    .mark_geoshape(stroke="white")
    .encode(alt.Tooltip(["code", "nom"]))
    .transform_lookup(lookup="code", from_=alt.LookupData(stats, "departement", feature_list))
    .project("conicConformal", rotate=[-3, -46.5], parallels=[49, 44])
    .properties(width=220, height=200)
)
(
    chart.encode(alt.Color("altitude_max:Q")) | chart.encode(alt.Color("population:Q"))
).configure_view(stroke=None).resolve_scale(color="independent")

11.7 ipyleaflet

ipyleaflet4 est une bibliothèque Python spécifiquement conçue pour l’environnement Jupyter. Elle permet un portage de l’application Javascript Leaflet qui propose d’enrichir des widgets de cartes interactives sur le modèle de Google Maps ou OpenStreetMap. La bibliothèque met à disposition un widget particulier Map, à l’image des autres structures ipywidgets qu’il est possible d’enrichir et d’équiper de fonctions callbacks (des fonctions déclenchées automatiquement lors d’événements prédéfinis) pour l’interaction.

Une carte est alors initialisée sur des coordonnées géographiques avec un niveau de zoom. Dans l’exemple ci-dessous, on affiche les vingt communes les plus peuplées au bout d’un Marker. Ces éléments sont par défaut interactifs : un clic dessus ouvre une fenêtre où l’on peut insérer un nouveau widget au choix, ici un simple contenu enrichi avec le nom de la ville et sa population.

Dans cet exemple, on ajoute également un menu déroulant Dropdown avec la liste des villes affichées: la fonction de rappel récupère ici les coordonnées de la ville sélectionnée pour centrer la carte dessus. Enfin, il est également possible d’enrichir une carte avec des informations issues de fichiers au format GeoJSON, à l’image de ceux utilisés pour Altair. Le paramètre hover_style permet ici de configurer un comportement interactif simple où le style est mis à jour quand la souris passe au-dessus d’un élément.

from ipyleaflet import Map, Marker, GeoData
from ipywidgets import HTML, Layout, Dropdown

top_20 = villes.sort_values("population", ascending=False).head(20)

map_ = Map(center=(46.5, 3), zoom=5, layout=Layout(max_width="400px"))  # ①

for _, data in top_20.iterrows():
    marker = Marker(location=(data.latitude, data.longitude), draggable=False)  # ②
    marker.popup = HTML(f"<b>{data.nom}</b>: {data.population:_} habitants")  # ③
    map_.add_layer(marker)

def on_click(info):
    ville = top_20.set_index("nom").loc[info["new"]]
    map_.center = (ville.latitude, ville.longitude)
    map_.zoom = 8

dropdown = Dropdown(description="Ville:", options=sorted(top_20.nom))
dropdown.observe(on_click, names="value")

geodata = GeoData(
    geo_dataframe=departements_fr,
    style={"color": "#008f6b", "opacity": 1, "fillOpacity": 0.1, "weight": 1},
    hover_style={"color": "white", "fillOpacity": 0.4, "weight": 3, "zorder": 2},
)
map_.add_layer(geodata)

display(dropdown, map_)

En quelques mots…

Pour aller plus loin