非常牛批的可视化库Plotly
昨天晚上十一点多吧,我在公司楼下抽烟,手机嗡一下,产品群里丢了张截图,说“老板要一个能点来点去的图,别给我那种死的 png 了”,我当时就……哎呀你们懂的,都是临时起意的需求,明早九点要汇报那种。然后我就想起 Plotly 这个东西,真不是吹,平时你用 matplotlib 画得再漂亮,它也就是一张图,Plotly 是那种“你把鼠标放上去它会跟你聊天”的感觉,hover 一下告诉你每个点的细节,拉个框还能缩放,甚至你把图例点一下就能隐藏一条线,领导看了就觉得“哇我们也很数据化”,其实你背后就几行代码,别拆穿我哈。
我印象特别深的是有一次我们做告警平台,日志量大得离谱,后端同学还在那儿纠结 ES 聚合慢不慢,我这边要做的是“让人看得懂”,你懂吧,图要能筛选、能联动、能导出,最后我就直接 Plotly 一把梭。对了,Plotly 在 python 里你一般就两条路:一个是 plotly.express,快,适合你想立刻出图;另一个是 plotly.graph_objects,细,适合你要把每个像素都控制住那种。反正我当时是先用 express 把图跑出来,老板一拍桌子说“能不能加一条均线”,我再切到 go 去做精修,跟修图一样,哈哈。
先给你们来个最常见的场景:做个“接口耗时趋势 + hover 看 p95/p99”,我随便造点数据,你拿真实数据替换就行。注意哈,这段代码我自己写的,不是网上搬的那种花里胡哨 demo,核心就是让你感受一下交互。
import random
import datetime as dt
import pandas as pd
import plotly.express as px
# 假装这是你从 Prometheus / 日志里算出来的聚合结果
random.seed(7)
base = dt.datetime.now() - dt.timedelta(days=30)
rows = []
for i in range(30):
day = (base + dt.timedelta(days=i)).date()
p50 = random.randint(30, 80)
p95 = p50 + random.randint(40, 120)
p99 = p95 + random.randint(10, 60)
rows.append({"day": day, "p50": p50, "p95": p95, "p99": p99})
df = pd.DataFrame(rows)
fig = px.line(
df,
x="day",
y=["p50", "p95", "p99"],
markers=True,
title="接口耗时分位数趋势(ms)"
)
# 让 hover 更像“讲人话”
fig.update_traces(hovertemplate="日期=%{x}<br>耗时=%{y} ms<extra></extra>")
fig.update_layout(
hovermode="x unified",
legend_title_text="分位数",
xaxis_title="日期",
yaxis_title="毫秒",
)
fig.show()
你跑一下就懂了:图上每个点你放上去,它直接告诉你那天的值,而且 hovermode="x unified" 这个小东西很阴险,特别适合给老板演示——你指着某一天,它把 p50/p95/p99 同时弹出来,像你做了个“专业仪表盘”。我以前还傻乎乎地一条线一条线 hover,领导说“看不出来差距”,我一改成 unified,领导立马说“嗯这才对”,你看,很多时候不是数据不行,是展示方式不行。
然后你们肯定会问,光折线图有啥意思,能不能“筛选服务”“点一下看某个服务的耗时”,这时候 Plotly 另一个好处就来了:你不一定要上完整的 Dash(当然 Dash 也很猛),你在一个 figure 里就能塞下拉框,点一下换数据,像变魔术。下面这段我写得尽量短一点,不然你们复制都费劲。
import numpy as np
import pandas as pd
import plotly.graph_objects as go
# 模拟多个服务的耗时分布
np.random.seed(3)
services = ["user-api", "order-api", "pay-api"]
data = []
for s in services:
# 不同服务不同“脾气”
base = {"user-api": 60, "order-api": 90, "pay-api": 130}[s]
samples = np.clip(np.random.normal(loc=base, scale=25, size=500), 1, None)
data.append((s, samples))
fig = go.Figure()
for s, samples in data:
fig.add_trace(go.Histogram(
x=samples,
name=s,
visible=(s == "user-api"),
nbinsx=40,
hovertemplate="耗时=%{x} ms<br>数量=%{y}<extra></extra>",
opacity=0.85
))
# 下拉切换 trace 的可见性
buttons = []
for i, s in enumerate(services):
visible = [False] * len(services)
visible[i] = True
buttons.append(dict(
label=s,
method="update",
args=[{"visible": visible}, {"title": f"{s} 耗时分布直方图"}],
))
fig.update_layout(
title="user-api 耗时分布直方图",
xaxis_title="毫秒",
yaxis_title="数量",
updatemenus=[dict(
type="dropdown",
x=1.02, y=1,
xanchor="left",
buttons=buttons
)]
)
fig.show()
你看这个效果就很“装”,不是,是真好用:一个图里三个服务随便切,领导喜欢点来点去,你就让他点,点累了他就会觉得你这系统很强。顺便说一句,直方图这种东西,用 matplotlib 也能画,但你要做交互、做筛选,一下就开始写一堆前端代码了,Plotly 这边你就一个 dropdown 搞定,省了我好多命。
不过也不是说 Plotly 没坑哈,我跟你们说几个我自己踩过的,不然你们一上来就“卧槽太牛逼了”,然后线上被打脸。第一个坑是数据量特别大时,比如你一次塞十万点进去,浏览器会卡,尤其你还开 markers,那更卡。我的经验是,能聚合就聚合,别把原始明细全丢进去;真要画很多点,就考虑用 WebGL 的 trace(比如 Scattergl),或者先抽样,老板看趋势就够了,别跟他较劲“每个点都要看”。
第二个坑是“导出图片”。有些同学做周报要把图贴 PPT,老板不让你贴 HTML,那你就得导出 png/pdf。Plotly 这块一般配个 kaleido 就行,但你别临时到生产机上装依赖,很多机器没图形环境你会崩溃(别问我怎么知道的,我那次半夜两点在机房里骂人)。我一般在 CI 里提前把导图这件事跑通,或者干脆让周报走 HTML 离线报告,发给领导点开看,体验还更好。
第三个我觉得最爽的点其实是“统一风格”。以前我们报表系统里,A 同学 matplotlib,B 同学 pyecharts,C 同学前端 ECharts,最后整个产品像拼夕夕凑单一样,风格乱七八糟。Plotly 你一套模板(template)下来,字体、网格、背景都能统一,别小看这个,领导审美这东西很玄学,你一统一,他就觉得“成熟”。
还有个小细节啊,Plotly 的图例是可以当开关用的。你做多条线的时候,老板老说“太乱了”,你别急着改数据,你就告诉他:你点图例,把不想看的线关掉。然后他会自己玩半天,你在旁边喝水就行。对了,hover 里的信息也别太“工程化”,别一上来 trace_id、span_id 把人吓死,换成“耗时、错误数、请求量”这种业务能理解的词,真的省沟通。
说到这我突然想起来,上次我们组小李还问我,Plotly 能不能做那种“点一下图上某个点,右边出来详情表格”的联动。我当时没展开讲,因为那就是 Dash 的主场了,你把 Plotly 当成组件塞进 Dash 里,回调一写,点哪儿更新哪儿,妥妥的。只是你要是团队里没人愿意维护这种小前端,那你就别一口气上太复杂,先把交互图搞起来就够赢了。
行了我不说了,我这会儿还在等外卖,估计又是那家店忘记给我筷子……哦对,最后提醒一句,Plotly 图默认是交互的,给同事发的时候别只截个图,你让他直接打开页面自己缩放看看,他会突然理解你想表达啥,真的,少开好多会。好了我去看看外卖到哪了,群里有人@我我得回一下。