☰
matplotlib交互式数据光标实现——mplcursors 与 TaoToken 统一 Key 通道的图表标注实践
2026/10/2 6:21:57 网站建设 项目流程

1. 为什么我放弃了静态标注,改用 mplcursors 做交互式数据光标

如果你用 matplotlib 画过带几十条曲线的折线图,大概率经历过这种尴尬:图交给同事,对方问「这条线在 x=37 的时候 y 是多少」,你只能把图放大、眯着眼估读,或者临时改脚本print一遍。静态图的信息密度是固定的,而人看图的动作是动态的——想点哪里、想看哪个点,应该由鼠标决定。

mplcursors 就是解决这个问题的轻量方案。它是 matplotlib 的第三方扩展包,灵感来自更老的 mpldatacursor,但把 API 做得更底层、更灵活。装上之后,你只需要一行mplcursors.cursor(lines),鼠标悬停或点击数据点时就会弹出标注框,显示坐标值;右键取消,按d键切换开关。它适合谁?适合所有用 Python 做数据分析、需要把图表交付给他人查看、又不想上 Plotly/Dash 那套重前端方案的人。

这篇要讲的不只是「怎么弹个框」。我会把 hover 触发、标注框定制、多子图联动这三件事拆开讲透,给出可直接复制的配置片段和回调函数。同时还有一个容易被忽略的工程问题:很多图表生成脚本里会调用大模型做数据摘要或标题润色,凭证散落在各个脚本里,改一次 Key 要翻十个文件。我会演示怎么用 TaoToken 的统一 Key/API 通道把这些调用凭证集中管理,让绘图脚本只管画图。最后用本地运行截图和光标响应日志验证交互效果。

先说清楚 mplcursors 和 mpldatacursor 的核心差异,这决定了你后面怎么写代码。mpldatacursor 的自定义主要靠给datacursor()传formatter参数,格式化逻辑被绑死在调用处;mplcursors 引入了Selection对象(底层是 namedtuple),选中数据点后通过connect("add", callback)注册回调,回调只接收一个sel参数。这意味着绘图逻辑和光标逻辑可以彻底分离——你可以在一个独立模块里写所有标注规则,主脚本只负责cursor()一下。这种解耦在子图多、标注规则复杂的场景下优势非常明显。

安装没什么坑,一条命令:

pip install mplcursors

如果你用的是 conda 环境,conda install -c conda-forge mplcursors也行。装完import mplcursors不报错就说明就绪。下面从最基础的 hover 触发开始,逐步加复杂度。

2. TaoToken 前置:把图表脚本里的模型调用凭证收拢到一条通道

在讲配置之前,先解决一个真实痛点。我手上有一批自动化报表脚本,画完图之后会调用模型生成一段「本期数据要点」的文字,贴在图表下方。早期每个脚本里都硬编码了api_key = "sk-xxx",后来 Key 轮换,我改了七个文件还漏了一个,导致某天的日报直接报 401。从那以后我把所有模型调用统一走 TaoToken 的 API 通道。

TaoToken 在这里扮演的角色是「统一 Key 通道」:你的绘图脚本、数据摘要脚本、标题润色脚本,全部指向同一个 Base URL,用同一把 Key。轮换时只改一处环境变量,所有脚本自动生效。官网入口是 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 端点固定为 https://taotoken.net/api (这个地址不加 UTM 参数,直接用于代码里的base_url)。

具体怎么落地?我推荐用环境变量而不是写死在代码里。在 shell 配置文件里加两行:

export TAOTOKEN_API_KEY="你的Key" export TAOTOKEN_BASE_URL="https://taotoken.net/api"

然后绘图脚本里这样读:

import os from openai import OpenAI client = OpenAI( api_key=os.environ["TAOTOKEN_API_KEY"], base_url=os.environ["TAOTOKEN_BASE_URL"], )

注意base_url后面不要手动拼/v1,SDK 会自己处理路径。这一点我踩过坑:早期我写成https://taotoken.net/api/v1,结果请求路径变成/api/v1/v1/chat/completions,直接 404。统一用https://taotoken.net/api就好。

如果你需要按项目隔离 Key,可以在 TaoToken 控制台里创建多个 Key,分别命名为report-prod、notebook-dev之类,然后不同脚本读不同的环境变量名。控制台地址是 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。Key 的创建和管理在 API Keys 页面: https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。

这里要强调一个原则:TaoToken 是凭证与请求的统一出口,不是编辑器替代品,也不是让你把生产数据库直连出去的东西。它的价值在于「一处配置、多处复用」,让绘图脚本里的模型调用不再成为维护负担。配置好之后,你的 matplotlib 脚本结构会变成:数据准备 → 绘图 → mplcursors 交互层 → 可选的模型摘要调用。前三个是本地纯计算,第四个才走网络,职责清晰。

对于长期跑批量报表的场景,可以考虑 Coding Plan,把模型调用的配额和计费集中管理: https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。如果只是想先验证模型能不能通,用模型对话页面手动发一条消息最快: https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。

3. 可复制配置:hover 触发、标注框定制与多子图联动

这一节是核心,我给三段可直接跑的代码。第一段讲 hover 触发和基础定制,第二段讲标注框样式,第三段讲多子图联动。每段都可以单独复制到.py文件里运行。

3.1 hover 触发与 Selection 对象

默认的mplcursors.cursor(lines)是点击触发。改成悬停触发只需要传hover=True:

import matplotlib.pyplot as plt import numpy as np import mplcursors np.random.seed(42) x = np.linspace(0, 10, 50) fig, ax = plt.subplots(figsize=(9, 5)) lines = [] for i, label in enumerate(["alpha", "beta", "gamma"]): y = np.sin(x + i) + np.random.normal(0, 0.1, len(x)) line, = ax.plot(x, y, marker="o", markersize=4, label=label) lines.append(line) ax.legend() ax.set_title("Hover over a point to inspect") cursor = mplcursors.cursor(lines, hover=True) @cursor.connect("add") def on_add(sel): sel.annotation.set_text( f"x={sel.target[0]:.2f}\ny={sel.target[1]:.2f}" ) plt.show()

关键点在sel.target。它是一个元组,sel.target[0]是 x 值,sel.target[1]是 y 值。sel.target.index是这个点在原始数据数组里的下标,做自定义标签时非常有用。sel.artist指向被选中的 Line2D 对象,你可以通过sel.artist.get_label()拿到图例名。

hover 模式下有个细节:鼠标移动时标注框会频繁触发add事件。如果你在回调里做了重计算,会明显卡顿。解决办法是把重计算提前算好存成数组,回调里只做查表。

3.2 标注框样式定制

默认标注框是白底黑字带箭头,样式比较朴素。sel.annotation是一个Annotation对象,你可以直接改它的属性:

@cursor.connect("add") def on_add(sel): idx = sel.target.index sel.annotation.set_text(f"#{idx} ({sel.target[0]:.1f}, {sel.target[1]:.2f})") sel.annotation.get_bbox_patch().set( facecolor="#1f2937", edgecolor="#60a5fa", alpha=0.92, boxstyle="round,pad=0.4", ) sel.annotation.set_color("white") sel.annotation.set_fontsize(10) sel.annotation.set_fontfamily("monospace")

get_bbox_patch()返回标注框的背景矩形,boxstyle支持round、square、circle等。我实测下来round,pad=0.4在深色背景下观感最好。注意set_color改的是文字颜色,不是框的颜色,这两个容易搞混。

如果你想让不同曲线的标注框颜色跟随曲线,可以这样:

@cursor.connect("add") def on_add(sel): color = sel.artist.get_color() sel.annotation.get_bbox_patch().set(facecolor=color, alpha=0.85) sel.annotation.set_color("white") sel.annotation.set_text(f"{sel.artist.get_label()}: {sel.target[1]:.3f}")

3.3 多子图联动

多子图场景下,你通常希望「在子图 A 悬停时,子图 B 的对应位置也高亮」。mplcursors 本身不直接支持跨子图联动,但通过回调里操作其他 axes 就能实现:

fig, axes = plt.subplots(2, 1, figsize=(9, 7), sharex=True) x = np.linspace(0, 10, 60) y1 = np.sin(x) y2 = np.cos(x) line1, = axes[0].plot(x, y1, color="#ef4444", marker="o", markersize=3) line2, = axes[1].plot(x, y2, color="#3b82f6", marker="o", markersize=3) vline1 = axes[0].axvline(0, color="gray", linestyle="--", alpha=0) vline2 = axes[1].axvline(0, color="gray", linestyle="--", alpha=0) cursor = mplcursors.cursor([line1, line2], hover=True) @cursor.connect("add") def on_add(sel): xval = sel.target[0] vline1.set_xdata([xval, xval]) vline1.set_alpha(0.6) vline2.set_xdata([xval, xval]) vline2.set_alpha(0.6) sel.annotation.set_text(f"x={xval:.2f}\ny={sel.target[1]:.2f}") fig.canvas.draw_idle() @cursor.connect("remove") def on_remove(sel): vline1.set_alpha(0) vline2.set_alpha(0) fig.canvas.draw_idle()

这里有两个要点。第一,cursor()接收的是 artists 列表,把两个子图的 line 都传进去,这样两个子图都能触发。第二,回调里改了图形元素后必须调fig.canvas.draw_idle(),否则界面不会刷新。draw_idle比draw更省资源,它会把重绘合并到下一次事件循环。

如果你用的是 Jupyter Notebook 的%matplotlib widget后端,联动效果是实时的;用%matplotlib inline则不会有交互,因为 inline 后端输出的是静态 PNG。这一点务必确认,否则你会以为代码没生效。

3.4 把模型摘要调用接进绘图脚本

现在把第 2 节的 TaoToken 配置用起来。假设你想在图表生成后,让模型根据数据生成一句摘要,贴在标题下方:

import os from openai import OpenAI def summarize(series_dict): client = OpenAI( api_key=os.environ["TAOTOKEN_API_KEY"], base_url=os.environ["TAOTOKEN_BASE_URL"], ) prompt = "用一句话概括以下序列的趋势,不超过40字:\n" + "\n".join( f"{k}: 首值{v[0]:.2f}, 末值{v[-1]:.2f}, 均值{sum(v)/len(v):.2f}" for k, v in series_dict.items() ) resp = client.chat.completions.create( model="gpt-4o-mini", messages=[{"role": "user", "content": prompt}], temperature=0.3, ) return resp.choices[0].message.content.strip()

调用后把返回值ax.set_title(summary)即可。注意model参数填你实际可用的模型 ID,不同账号可用模型可能不同,可以在模型对话页面先试一条确认。这段代码和 mplcursors 完全解耦——即使模型调用失败,图表和交互光标照常工作,这是刻意的设计。

4. 验证请求:本地运行与光标响应日志

代码写完必须验证,否则你不知道是 mplcursors 没生效还是模型调用挂了。我分两步验证。

第一步,验证 mplcursors 交互。在回调里加一行日志:

import logging logging.basicConfig(level=logging.INFO, format="%(asctime)s %(message)s") @cursor.connect("add") def on_add(sel): logging.info("cursor add: artist=%s index=%s target=(%.3f, %.3f)", sel.artist.get_label(), sel.target.index, sel.target[0], sel.target[1]) sel.annotation.set_text(f"({sel.target[0]:.2f}, {sel.target[1]:.2f})")

运行脚本后,鼠标在数据点上悬停,终端会输出类似:

2025-01-15 14:22:31,108 cursor add: artist=alpha index=17 target=(3.469, 0.912) 2025-01-15 14:22:31,342 cursor add: artist=alpha index=18 target=(3.673, 0.887) 2025-01-15 14:22:31,601 cursor add: artist=beta index=18 target=(3.673, 1.421)

看到index和target随鼠标移动变化,说明 hover 触发正常。如果日志一条都不出,检查三件事:后端是不是 widget/agg 交互后端、hover=True有没有传、artists 列表是不是空。

第二步,验证 TaoToken 通道。单独跑一段最小请求:

import os from openai import OpenAI client = OpenAI( api_key=os.environ["TAOTOKEN_API_KEY"], base_url=os.environ["TAOTOKEN_BASE_URL"], ) resp = client.chat.completions.create( model="gpt-4o-mini", messages=[{"role": "user", "content": "回复两个字:正常"}], ) print(resp.choices[0].message.content)

终端打印「正常」就说明 Key 和 Base URL 都对。如果报错,对照下一节的排查表。

成功运行时,你会看到:图表窗口弹出,鼠标划过数据点出现深色圆角标注框,框内显示坐标;终端同步打印光标日志;标题下方出现模型生成的摘要文字。三者互不阻塞,任何一个环节出问题都能独立定位。

5. 本篇常见错排查:401、local proxy failed、reading choices、OAuth

这一节按真实报错逐条对照。这些错误我基本都遇到过,按顺序排查能省很多时间。

401 Unauthorized / invalid api key。最常见的原因是环境变量没生效。先确认echo $TAOTOKEN_API_KEY有输出,且没有多余空格或换行。如果你在 IDE 里运行,注意 IDE 可能不继承 shell 的环境变量,需要在运行配置里手动加。另一个原因是 Key 被删除或过期,去 API Keys 页面确认状态。还有一种隐蔽情况:base_url写成了https://taotoken.net/api/v1,导致请求路径重复,服务端可能返回 401 而非 404,别被误导。

local proxy failed / connection refused。这个报错通常和本机网络配置有关。先确认base_url是https://taotoken.net/api,没有多余斜杠。如果你所在环境配置了 HTTP 代理,检查HTTP_PROXY/HTTPS_PROXY环境变量是否指向了一个不可用的地址,临时unset掉再试。注意:这里说的是排查本机已有的代理配置,不是让你去搭什么通道,企业内网环境请遵循所在网络的合规要求。

reading 'choices' of undefined。这个报错来自resp.choices[0],说明响应体结构和你预期的不一样。原因通常是请求根本没成功,返回的是一个错误对象而不是正常的 completion。打印完整响应看看:

print(resp.model_dump_json(indent=2))

如果里面是{"error": {...}},按错误信息处理。另一个可能是模型 ID 写错了,服务端返回了非标准结构。确认model参数拼写正确。

OAuth / authentication 相关报错。如果你用的是某些 CLI 工具(比如 Claude Code 类工具),它们可能走 OAuth 流程而非 API Key。这类工具需要单独配置。以 Claude Code 为例,接入时需要三件套齐全:Base URL 填https://taotoken.net/api,Key 填你的 TaoToken Key,Model ID 填实际可用模型。三者缺一不可,只填两个会报认证失败。接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,里面有各工具的完整配置示例。

标注框不显示或位置错乱。这不是网络问题,是 mplcursors 配置问题。检查cursor()的 artists 参数是否传了正确的对象——如果你传的是ax.plot()的返回值列表,注意它是 list of list,需要展开。另外sel.annotation在add事件里才有效,在remove里访问会报错。

多子图联动时界面卡死。多半是在回调里调了fig.canvas.draw()而不是draw_idle(),导致每次鼠标移动都强制全量重绘。改成draw_idle(),并把重计算逻辑移出回调。

排查顺序建议:先确认网络请求通(单独跑最小请求),再确认 mplcursors 触发(看日志),最后确认两者结合时没有互相干扰。分开验证比一起调试快得多。

6. 把交互光标和统一通道固定成你的标准流程

走到这里,你应该已经跑通了一个带 hover 标注、样式定制、多子图联动的 matplotlib 图表,并且模型调用凭证收拢到了 TaoToken 一条通道上。我想强调的不是某个 API 的用法,而是这套组合的工程价值:mplcursors 让图表从「静态交付物」变成「可探索的界面」,TaoToken 让散落的凭证变成「一处配置」。

如果你要长期维护一批报表脚本,建议把第 3 节的回调逻辑抽成一个独立模块,比如cursor_utils.py,里面放attach_hover_cursor(ax, lines, formatter)这样的函数。主脚本只调一行,标注规则集中管理。模型调用同理,抽一个llm_client.py,从环境变量读 Key 和 Base URL。这样你的绘图脚本会非常干净:数据、绘图、交互、摘要各司其职。

下一步可以尝试的方向:把sel.target.index和 pandas DataFrame 的索引对齐,悬停时直接显示原始行数据;或者用cursor.connect("add")触发一个异步任务,把选中点的上下文发给模型做即时解读。这些都不难,前提是你的凭证通道已经稳定。

需要查具体 API 参数时,接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,模型对话验证在 https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,Key 管理在 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。先把最小请求跑通,再回来调光标样式,顺序别反。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询