You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Seaborn多网格图中用mplcursors显示完整DataFrame行注释

问题:Seaborn多维度散点图(分面+色调)搭配mplcursors时Tooltip索引错误

问题描述

  • 需求:绘制包含分面(col/row)、色调(hue)、x/y轴的多维散点图,实现鼠标悬停显示数据点的完整属性Tooltip
  • 异常:添加全部视觉修饰符后,mplcursors返回的sel.index是子图内的局部索引,而非原DataFrame的全局索引,导致Tooltip显示错误;仅保留部分修饰符(如仅x/y轴)时功能正常

复现代码

import seaborn as sns
import matplotlib.pyplot as plt
import mplcursors


df = sns.load_dataset("tips")

sns.relplot(data=df, x="total_bill", y="tip", hue="day", col="time", row="sex")


def show_hover_panel(get_text_func=None):
    cursor = mplcursors.cursor(
        hover=2,  # Transient
        annotation_kwargs=dict(
            bbox=dict(
                boxstyle="square,pad=0.5",
                facecolor="white",
                edgecolor="#ddd",
                linewidth=0.5,
            ),
            linespacing=1.5,
            arrowprops=None,
        ),
        highlight=True,
        highlight_kwargs=dict(linewidth=2),
    )

    if get_text_func:
        cursor.connect(
            event="add",
            func=lambda sel: sel.annotation.set_text(get_text_func(sel.index)), # <- 返回的是子图局部索引,非全局索引
        )

    return cursor


def on_add(index):
    item = df.iloc[index] 
    parts = [
        f"total_bill: {item.total_bill}",
        f"tip: {item.tip}",
        f"day: ${item.day}",
        f"time: ${item.time}",
        f"sex: ${item.sex}",
    ]

    return "\n".join(parts)


show_hover_panel(on_add)

plt.show()

问题原因

Seaborn的relplot是分面绘图函数,会将原DataFrame按col/row/hue规则拆分为多个子集,每个子图仅绘制对应子集的数据。此时mplcursors返回的sel.index是当前子图内数据点的局部索引,而非原DataFrame的全局索引,直接用df.iloc[index]会取到错误的行数据。

解决方法

核心是获取每个数据点对应的原DataFrame全局索引,而非子图局部索引,以下两种方案可选:

方案1:从绘图对象提取全局索引(高效推荐)

利用relplot返回的FacetGrid对象,获取每个子图散点数据对应的全局索引,绑定到绘图元素上:

import seaborn as sns
import matplotlib.pyplot as plt
import mplcursors

df = sns.load_dataset("tips")
# 保存FacetGrid对象,用于获取子图数据
g = sns.relplot(data=df, x="total_bill", y="tip", hue="day", col="time", row="sex")

def show_hover_panel():
    cursor = mplcursors.cursor(
        hover=2,
        annotation_kwargs=dict(
            bbox=dict(
                boxstyle="square,pad=0.5",
                facecolor="white",
                edgecolor="#ddd",
                linewidth=0.5,
            ),
            linespacing=1.5,
            arrowprops=None,
        ),
        highlight=True,
        highlight_kwargs=dict(linewidth=2),
    )

    # 遍历所有子图,为散点绑定全局索引
    for ax in g.axes.flat:
        scatter = ax.collections[0]
        # 获取当前子图数据对应的原DataFrame全局索引
        global_indices = scatter.get_offsets()._offsets.index
        # 将全局索引存储到散点对象的gid属性中
        scatter.set_gid(global_indices)

    def on_add(sel):
        # 通过子图局部索引获取对应的全局索引
        global_index = sel.artist.get_gid()[sel.index]
        item = df.iloc[global_index]
        parts = [
            f"total_bill: {item.total_bill}",
            f"tip: {item.tip}",
            f"day: {item.day}",
            f"time: {item.time}",
            f"sex: {item.sex}",
        ]
        sel.annotation.set_text("\n".join(parts))

    cursor.connect("add", on_add)
    return cursor

show_hover_panel()
plt.show()

方案2:坐标匹配全局索引(兼容性强)

为原DataFrame添加全局索引列,通过鼠标悬停点的坐标匹配对应行的全局索引:

import seaborn as sns
import matplotlib.pyplot as plt
import mplcursors
import numpy as np

df = sns.load_dataset("tips")
# 添加全局索引列,确保每个数据点有唯一标识
df["global_idx"] = df.index
g = sns.relplot(data=df, x="total_bill", y="tip", hue="day", col="time", row="sex")

def show_hover_panel():
    cursor = mplcursors.cursor(
        hover=2,
        annotation_kwargs=dict(
            bbox=dict(
                boxstyle="square,pad=0.5",
                facecolor="white",
                edgecolor="#ddd",
                linewidth=0.5,
            ),
            linespacing=1.5,
            arrowprops=None,
        ),
        highlight=True,
        highlight_kwargs=dict(linewidth=2),
    )

    def on_add(sel):
        # 获取悬停点的坐标
        x, y = sel.target
        # 获取当前子图的数据集
        ax_data = sel.artist.get_offsets()._offsets
        # 匹配坐标(考虑浮点精度误差),找到对应行的局部索引
        match_mask = (np.isclose(ax_data["total_bill"], x)) & (np.isclose(ax_data["tip"], y))
        local_idx = np.where(match_mask)[0][0]
        # 获取全局索引
        global_index = ax_data["global_idx"].iloc[local_idx]
        item = df.iloc[global_index]
        parts = [
            f"total_bill: {item.total_bill}",
            f"tip: {item.tip}",
            f"day: {item.day}",
            f"time: {item.time}",
            f"sex: {item.sex}",
        ]
        sel.annotation.set_text("\n".join(parts))

    cursor.connect("add", on_add)
    return cursor

show_hover_panel()
plt.show()

说明

  • 方案1直接从绘图对象提取全局索引,效率更高,适合大多数场景
  • 方案2通过坐标匹配,适合无法直接从绘图对象获取索引的特殊场景

内容的提问来源于stack exchange,提问作者diyer0001

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.12 02:35:00