那天晚上快十二点,我还在公司楼下便利店门口吹风,手机一边刷着群消息,一边远程连着服务器调一个报表的图。产品在群里说了一句:“这个折线图能不能鼠标移上去看具体数值啊?最好能筛选、能放大,还能导出给老板点一点就能看。” 我当时心里一咯噔:完了,这要是纯 matplotlib,基本就是导个 PNG 甩过去,交互这事儿直接原地去世。
后来我就想起之前瞄过两眼的 Plotly,回去一试,哎,还真就一条线救命了。
你先别管 Plotly 有多强,先看它跑起来到底麻不麻烦。我那天是在 Jupyter 里随手敲了这么一段:
import pandas as pd
import plotly.express as px
# 模拟一点数据:每天的 PV 和 UV
df = pd.DataFrame({
"date": pd.date_range("2024-01-01", periods=7),
"pv": [100, 230, 180, 260, 300, 280, 350],
"uv": [80, 160, 130, 210, 240, 220, 260],
})
fig = px.line(
df,
x="date",
y=["pv", "uv"],
markers=True,
title="一周流量趋势"
)
fig.show()
这段跑完你能得到什么效果呢:一个网页里的交互图——支持拖拽缩放、鼠标悬浮看具体数值、右上角自带导出 PNG 按钮、还能在图例上点一下某条线把它隐藏。
对比一下以前 matplotlib 的玩法:同样的效果你要自己写一大坨配置,还只能导出静态图,发给老板以后老板一句“我想再看某一天的细节”,你只能重新画一张图再发一次;Plotly 这边直接让他自己框选放大看完事。
那一刻我真有点后悔用得太晚了。
Plotly 的两套“人格”:傻瓜相机 vs 单反
用久了你会发现,Plotly 大概有两种写法:
一种是上面这种 plotly.express,很像“傻瓜相机”:给它一张 DataFrame,说好 x 和 y 是哪几列,它就帮你把图堆出来,配色、图例、交互都给你安排好。它极大地简化了在 Python 中创建常见图表类型的流程。
还有一种是 plotly.graph_objects,这个就偏“单反相机”了,所有东西都能自定义,适合那种领导“乱点将军”的复杂需求。
我给你对比一下,同样是一张散点图。先是 express 版本,很轻松的那种:
import plotly.express as px
iris = px.data.iris() # 自带的示例数据集
fig = px.scatter(
iris,
x="sepal_width",
y="sepal_length",
color="species",
size="petal_length",
title="鸢尾花数据散点图"
)
fig.show()
你只要看得懂“宽度”“长度”“按物种分颜色”,基本就知道图画的啥。
如果换成 graph_objects,大概这样:
import plotly.graph_objects as go
import plotly.express as px
iris = px.data.iris()
fig = go.Figure()
for species, sub_df in iris.groupby("species"):
fig.add_trace(
go.Scatter(
x=sub_df["sepal_width"],
y=sub_df["sepal_length"],
mode="markers",
name=species,
marker=dict(
size=sub_df["petal_length"] * 3, # 大小手动算一算
opacity=0.8
)
)
)
fig.update_layout(
title="鸢尾花数据散点图(自定义版)",
xaxis_title="sepal_width",
yaxis_title="sepal_length"
)
fig.show()
你看逻辑就清晰很多了:先 new 一个 Figure,然后一个物种一条 trace,最后再统一 update 布局,轴标题啊透明度啊都自己掌控。缺点就是字多点,好处是你能把图从里到外拆开改。
我自己的习惯是:简单报表、临时分析用 px.*,一旦要做“准正式的看板”、嵌入到系统里就切到 graph_objects。
一个真实业务场景:活动数据监控看板
之前给运营做一个活动数据监控,需求很典型:要看每天每个渠道的转化漏斗,而且希望“点某个渠道,下面所有图跟着联动变”。这本质上是一个简单的 大数据 监控场景。
我第一版用的是最粗暴的方式:一张大表,写 SQL 把数据拉下来,再用 Plotly 做几个图拼一块:
- 上面一行:总体 PV/UV/支付数折线图
- 中间一行:不同渠道的条形图
- 下面:转化漏斗
代码大概这样起手:
import pandas as pd
import plotly.express as px
from plotly.subplots import make_subplots
import plotly.graph_objects as go
# 假设这是从数据库查出来的结果
df = pd.DataFrame({
"date": pd.date_range("2024-01-01", periods=5).repeat(3),
"channel": ["A", "B", "C"] * 5,
"pv": [100, 80, 60, 120, 90, 70, 150, 110, 80, 160, 130, 90, 180, 140, 100],
"uv": [80, 60, 40, 90, 70, 50, 110, 85, 60, 120, 95, 65, 140, 100, 70],
"order": [10, 8, 5, 15, 9, 6, 18, 12, 7, 20, 15, 8, 22, 16, 9],
})
# 汇总成整体趋势
daily = df.groupby("date").sum(numeric_only=True).reset_index()
fig = make_subplots(
rows=2, cols=2,
specs=[[{"colspan": 2}, None], [{"type": "bar"}, {"type": "domain"}]],
subplot_titles=("整体趋势", "渠道订单数", "整体转化漏斗")
)
# 上面:整体 PV/UV 折线
fig.add_trace(
go.Scatter(x=daily["date"], y=daily["pv"], name="PV", mode="lines+markers"),
row=1, col=1
)
fig.add_trace(
go.Scatter(x=daily["date"], y=daily["uv"], name="UV", mode="lines+markers"),
row=1, col=1
)
# 左下:各渠道订单柱状图(最近一天)
latest = df[df["date"] == df["date"].max()]
fig.add_trace(
go.Bar(x=latest["channel"], y=latest["order"], name="各渠道订单"),
row=2, col=1
)
# 右下:转化漏斗(简单点就用三个数)
total_pv = daily["pv"].sum()
total_uv = daily["uv"].sum()
total_order = daily["order"].sum()
fig.add_trace(
go.Funnel(
y=["PV", "UV", "订单"],
x=[total_pv, total_uv, total_order],
textinfo="value+percent initial"
),
row=2, col=2
)
fig.update_layout(
title="活动数据监控总览(Demo)",
height=600
)
fig.show()
这个东西丢给运营之后,基本能撑一阵子:他们要看趋势,可以在上面那块缩放;要看当天哪个渠道拉垮,一眼看到柱状图就行;漏斗那块至少可以大致感知有没有明显的掉血。
后来他们说想把“按渠道过滤”做成下拉框,其实用 Plotly 的 updatemenus、dropdown 搞一搞就行,或者懒一点直接上 Dash 做个小页面也能完成。
动画效果:让数据“动”起来
有一次数据组的小伙伴在写一个“城市热力”演示,说想做一个“时间推移”的效果,我第一反应是:这不就是动画嘛。Plotly 其实这块写得还挺顺手。
比如做一个随时间变化的折线动画,很简单这样:
import plotly.express as px
import pandas as pd
df = pd.DataFrame({
"day": list(range(1, 8)) * 2,
"value": [10, 12, 15, 18, 17, 20, 23, 8, 9, 11, 14, 13, 15, 16],
"group": ["A"] * 7 + ["B"] * 7
})
fig = px.line(
df,
x="day",
y="value",
color="group",
animation_frame="day", # 按天推进动画
range_y=[0, 25],
title="两组数据随时间变化的动画"
)
fig.show()
你会看到底下有个时间条,可以拖着看每一天的状态。虽然这个例子很简陋,但你把“day”换成“日期”,“value”换成“交易额”,动画效果一上,秒变“业务增长故事片”,在汇报时非常抓人眼球。这种动态演示也是当前 智能 & 数据 & 云 应用中的一个小亮点。
如何整合到 Web 项目与报告文档
我后来就有个非常爽的用法:每周要给领导发周报,以前是 markdown + 几张静态图。用 Plotly 之后,我直接输出一个 HTML,把几个图拼成一个页面发过去。
导出其实就一句话:
fig.write_html("weekly_report_demo.html", include_plotlyjs="cdn")
或者你多张图的时候,可以写一个很简陋的“报告生成脚本”:
import plotly.express as px
import pandas as pd
df = px.data.gapminder().query("year == 2007")
fig1 = px.scatter(df, x="gdpPercap", y="lifeExp",
size="pop", color="continent",
hover_name="country", log_x=True,
title="人均GDP vs 寿命")
fig2 = px.bar(df.sort_values("gdpPercap", ascending=False).head(10),
x="country", y="gdpPercap",
title="人均GDP前10国家")
fig1.write_html("report.html", include_plotlyjs="cdn")
# 追加第二个图到同一个 html
with open("report.html", "a", encoding="utf-8") as f:
f.write(fig2.to_html(full_html=False, include_plotlyjs=False))
你把这个 HTML 丢到飞书、企微、邮件,领导点开就能自己玩图。而且这东西挂到任何 Web 容器里就是一个前端页面,部署上去就能用。
要是你们团队再折腾一点,上 Dash(也是 Plotly 家的)做一个简易 BI,基本就是 Python 写后端顺手把前端交互做完那种感觉,不用强行学一遍前端框架也能搞出还不错的结果页面。
实践中的一些“小坑”
这个库再“省心”,也有几个地方容易翻车,我自己踩过几次:
一个是中文字体。你用默认配置画中文,偶尔会遇到某些环境字体发虚或者直接方块字,这时候就要自己指定:
fig.update_layout(
font=dict(family="Microsoft YaHei", size=14)
)
服务器是 Linux 的话还得保证系统真有这个字体,不然你以为“我配好了”,实际导出来还是奇奇怪怪。
还有一个是数据量的问题。Plotly 本质上是前端渲染,几万点没啥事,几十万、上百万点直接喂进去,浏览器就开始拉胯了,鼠标一挪卡半天。这种情况我一般是:
- 要么后端先按时间或分位数抽样一遍,只给前端 1/10 的点
- 要么就丢给专门的大数据可视化方案,不强行让 Plotly 背锅
最后一个小点是:你在 Notebook 里看图很爽,一旦放到脚本里跑,有时候会忘了写 fig.show(),然后只剩一脸懵逼的“咦怎么没有图”。还有就是远程服务器跑脚本的时候,别去期待像本地一样弹个窗口,老老实实导 HTML 或者静态图:
fig.write_image("chart.png") # 需要先安装 kaleido
# pip install -U kaleido
反正我现在但凡是跟“图”有点关系,又不想写前端的时候,第一反应就是先拉个 Plotly 跑一跑,能解决 80% 的交互需求,还顺带把“汇报体验”升级了一档。如果你也在寻找高效制作交互式图表的方法,不妨到 云栈社区 与其他开发者交流更多实战技巧。