Apache Superset 定制开发自定义可视化插件没那么难一、Superset 够用但不够好用Superset 是我用过的最适合中小团队的 BI 工具——开源免费、SQL 驱动、权限体系成熟。但用了一年多之后你会发现一个尴尬的事实内置的图表类型就那么几十种业务总有奇奇怪怪的展示需求。比如我们市场部想要一个环形漏斗图既能看各阶段的绝对人数又能看相邻阶段的转化率。Superset 自带 Funnel Chart 只有条形不能满足需求。查了一圈社区插件不是太老就是文档残缺。为什么社区插件普遍质量不高Superset 的插件协议superset-ui/core经历过多次 Breaking Change1.0 → 2.0 的升级直接废掉了一大批插件。社区开发者写的时候 Superset 还叫 Caravel现在已经改名三次了命名空间全乱了。所以你看到的文档残缺不是作者懒是 Superset 自己版本迭代太快插件作者追不动了。这也意味着自己做插件反而是最稳定的方案因为你只需要跟自己用的那个 Superset 版本对齐。最后我自己动手写了一个。做完才发现Superset 的插件体系其实设计得相当成熟只是文档散落在各个角落大部分人不知道从哪下手。flowchart TD A[确定可视化需求] -- B[选择基础图表类型] B -- C[创建 Viz 插件项目br/使用 superset-plugin-chart 脚手架] C -- D[编写 transformProps.tsbr/数据处理与映射] D -- E[编写 React 渲染组件br/使用 ECharts/AntV] E -- F[配置 controlPanelbr/定义左侧属性面板] F -- G[本地 npm link 调试] G -- H[打包并安装到 Superset] H -- I[上线生产环境] style D fill:#7B68EE,color:#fff style F fill:#F39C12,color:#000 style G fill:#27AE60,color:#fff二、插件架构Superset 的扩展哲学Superset 的插件体系遵循两个核心原则原则一前端和后端分离。插件本质上是一个 React 组件 数据处理逻辑后端不关心你的图表怎么画只负责传数据。这种设计让你完全可以用任何可视化库ECharts、D3、AntV、Deck.gl来渲染。原则二元数据驱动配置。每个插件通过controlPanel声明自己需要哪些配置项比如X 轴字段颜色映射是否显示图例Superset 自动渲染出左侧的属性面板。你不需要写一行业务表单代码。一个完整的插件文件结构像这样superset-plugin-chart-ring-funnel/ ├── package.json # 插件元信息 ├── src/ │ ├── index.ts # 入口注册插件 │ ├── plugin/ │ │ ├── buildQuery.ts # 构建后端查询 │ │ ├── controlPanel.ts # 左侧配置面板定义 │ │ ├── transformProps.ts # 数据处理核心 │ │ └── index.ts # 导出插件定义 │ ├── RingFunnel.tsx # React 渲染组件 │ └── types.ts # TypeScript 类型定义 └── tsconfig.json关键就三个文件controlPanel.ts定义管理员怎么配transformProps.ts定义数据怎么转RingFunnel.tsx定义画面怎么画。为什么是这三个文件而不是四个五个这是 Superset 团队刻意保持的关注点分离。如果你把数据转换逻辑混进 React 组件里后面想换一个可视化库比如从 ECharts 换到 AntV你就得重写整个组件。控制面板、数据处理、渲染三件事一旦耦合在一起改一个就得动全身。把这个原则吃透了你自己写的插件用两年也不怕维护。三、核心代码手写环形漏斗图插件下面是最核心的几个文件。我们从数据流的角度串起来看3.1 controlPanel.ts — 配置面板定义// controlPanel.ts — 定义 Superset 左侧属性面板的配置项 // 文件位置: src/plugin/controlPanel.ts import { t, validateNonEmpty } from superset-ui/core; import { ControlPanelConfig, sharedControls } from superset-ui/chart-controls; // sharedControls 是 Superset 内置的通用控件 // groupby分组字段、metrics指标列、color_scheme配色等 const config: ControlPanelConfig { controlPanelSections: [ { label: t(数据配置), tabOverride: data, // 挂载到 Superset 的数据标签页下 controlSetRows: [ [groupby], // 阶段名称字段如浏览→加购→下单→支付 [metrics], // 各阶段人数指标 // 为什么 metrics 和 groupby 成对出现 // 漏斗图需要阶段名groupby 阶段人数metrics两者缺一不可 ], }, { label: t(样式配置), tabOverride: customize, controlSetRows: [ // color_scheme 不需要自定义控件直接复用 Superset 内置的 // 这比自己写颜色选择器要省很多代码 [color_scheme], [ { name: show_conversion_rate, config: { type: CheckboxControl, label: t(显示转化率), default: true, // renderTrigger: true 表示勾选后图表需要重新渲染 // 为什么要重新渲染因为转化率会改变图表布局 renderTrigger: true, description: t(在相邻阶段之间显示转化率百分比), }, }, ], [ { name: funnel_gap, config: { type: SliderControl, label: t(阶段间距), default: 20, min: 0, max: 80, step: 5, description: t(控制漏斗各层之间的间距像素), }, }, ], ], }, ], // 查询结果的列映射 // 因为 Superset 返回的数据是通用的 {columns: [...], data: [...]} 格式 formDataOverrides: (formData: any) { // 强制按指标值降序排列保证漏斗从宽到窄 return { ...formData, order_desc: true, }; }, }; export default config;3.2 transformProps.ts — 数据处理层// transformProps.ts — 把 Superset 的原始查询结果转换成图表能用的格式 // 文件位置: src/plugin/transformProps.ts import { ChartProps, TimeseriesDataRecord } from superset-ui/core; interface RingFunnelDataPoint { stage: string; // 阶段名称如浏览商品 value: number; // 阶段人数 conversionRate?: number; // 到下一阶段的转化率 } export default function transformProps(chartProps: ChartProps) { // chartProps 包含四个关键对象 // 1. width/height — 图表容器尺寸 // 2. formData — 用户在控制面板的输入 // 3. queriesData — 后端返回的查询结果数组 // 4. datasource — 数据源信息 const { width, height, formData, queriesData } chartProps; const data: TimeseriesDataRecord[] queriesData[0].data; // 将 Superset 的通用数据格式转换为漏斗图专用格式 // data 的结构类似: [{列名1: 值1, 列名2: 值2}, ...] const funnelData: RingFunnelDataPoint[] data.map((row) ({ // Superset 用列名作为 key我们需要从 formData 中获取用户选的列名 // 为什么不用硬编码因为用户可能在 Superset 里给列起了别名 stage: String(row[formData.groupby[0]] || ), value: Number(row[formData.metrics[0]] || 0), })); // 计算转化率 // 处理方法遍历漏斗数据当前阶段/上一阶段 转化率 for (let i 0; i funnelData.length - 1; i) { const prevValue funnelData[i].value; if (prevValue 0) { funnelData[i].conversionRate Math.round((funnelData[i 1].value / prevValue) * 10000) / 100; } } return { // 原始数据透传给 React 组件 data: funnelData, // 提取用户配置的属性 showConversionRate: formData.show_conversion_rate ?? true, funnelGap: formData.funnel_gap ?? 20, // 容器尺寸响应式必须 width, height, // 配色方案 colorScheme: formData.color_scheme, }; }3.3 RingFunnel.tsx — React 渲染组件// RingFunnel.tsx — 基于 ECharts 的环形漏斗图渲染 // 文件位置: src/RingFunnel.tsx import React, { useMemo } from react; import ReactEChartsCore from echarts-for-react/lib/core; import * as echarts from echarts/core; import { FunnelChart } from echarts/charts; import { TooltipComponent, GridComponent } from echarts/components; import { CanvasRenderer } from echarts/renderers; // 按需加载 ECharts 模块减少打包体积 // 为什么不全量引入Superset 本身已经很大了插件再带全量 echarts 会爆炸 echarts.use([FunnelChart, TooltipComponent, GridComponent, CanvasRenderer]); interface RingFunnelDataPoint { stage: string; value: number; conversionRate?: number; } interface RingFunnelProps { data: RingFunnelDataPoint[]; showConversionRate: boolean; funnelGap: number; width: number; height: number; colorScheme: string; } const RingFunnel: React.FCRingFunnelProps ({ data, showConversionRate, funnelGap, width, height, colorScheme, }) { // useMemo 避免每次渲染都重新计算 ECharts 配置 // 只有 data/funnelGap/showConversionRate 变化才重建 const option useMemo(() { return { tooltip: { trigger: item, formatter: (params: any) { const item data[params.dataIndex]; // 自定义提示框用中文展示指标避免 echarts 默认的 key:value 格式 let tip strong${item.stage}/strongbr/人数${item.value.toLocaleString()}; if (item.conversionRate ! null) { tip br/到下一阶段转化率${item.conversionRate}%; } return tip; }, }, series: [ { type: funnel, left: 10%, top: 30, bottom: 30, width: 80%, // 漏斗图关键配置 minSize: 10%, // 最窄层至少占 10% 宽度防止最后阶段缩成一条线 maxSize: 100%, // 最宽层占满 gap: funnelGap, // 层级间距来自用户配置 label: { show: true, position: inside, formatter: {b}, // 显示阶段名称 }, // 数据结构映射 data: data.map((item, index) ({ name: item.stage, value: item.value, // 如果有转化率且用户选择显示追加到标签 ...(showConversionRate item.conversionRate ! null { label: { formatter: {b}\n转化率 ${item.conversionRate}%, }, }), })), }, ], }; }, [data, funnelGap, showConversionRate]); return ( div style{{ width, height }} ReactEChartsCore echarts{echarts} option{option} style{{ width: 100%, height: 100% }} // 不合并配置每次全量更新 — 防止旧配置残留 notMerge{true} / /div ); }; export default RingFunnel;四、调试和发布从本地到生产插件开发的调试流程比你以为的简单# 1. 用脚手架初始化Superset 官方推荐 # superset-ui/plugin-chart 是官方维护的插件模板 npx superset-ui/generator-superset init superset-plugin-chart-ring-funnel # 2. 进入项目目录装依赖 cd superset-plugin-chart-ring-funnel npm install # 3. 本地开发模式 — 热更新 # --dev 模式启动 webpack-dev-server修改代码自动刷新 npm run dev # 4. 链接到 Superset 开发环境 # 在 Superset 项目目录下执行 npm link /path/to/superset-plugin-chart-ring-funnel # 然后修改 Superset 的 setupPlugins.ts引入你的插件 # 5. 打包生产版本 npm run build # 6. 安装到 Superset生产环境 pip install superset-plugin-chart-ring-funnel开发时有一个小技巧先用 Superset 自带的 Table 图表验证 SQL 正确性再去适配你自己的图表。写 SQL 和画图表是两件事分开调能省不少时间。为什么npm link调试经常会莫名其妙失败核心原因是 Superset 的前端是用 webpack Module Federation 构建的你的插件导入的 React 实例和 Superset 主应用的 React 实例可能不是同一个。如果你在插件里import React from react而 Superset 用的是peerDependencies里的 React两个实例会产生 Hook 错误比如经典的 Invalid hook call。解决办法不是改代码而是在 Superset 的 webpack 配置里把 react、react-dom 设成 shared singleton。 踩坑提醒npm link后 Superset 报 Cannot read properties of undefined99% 是 transformProps 里对queriesData[0]没做判空保护。当 Superset 还在跑查询、数据还没回来的时候queriesData是个空数组你直接.data就崩了。加一行if (!queriesData[0]) return { data: [] }能救你十条命。控制面板的renderTrigger: true不要滥用。你每加一个 renderTrigger用户动一次滑块图表就重新请求一次后端。如果用户的 SQL 跑了 5 秒他拖滑块拖了 10 下就是 50 秒的等待。像min、max、step这些纯视觉参数别开 renderTrigger用户点查询按钮才是最佳实践。order_desc: true强制降序排序是两刃剑。漏斗图确实需要从宽到窄但万一你的数据本身就是排好序的比如按时间线浏览→下单→支付强制降序反而会把支付排到最前面。正确做法是暴露一个排序方向选择器给用户让他们自己决定。五、总结Superset 自定义插件开发没有想象中那么难核心就三件事controlPanel— 告诉 Superset 你的图表需要哪些配置transformProps— 把通用数据格式转成你的图表格式React 组件— 用任何你喜欢的可视化库画出来整个流程最耗时的其实不是写代码而是理解 Superset 的数据流转——query → queriesData → transformProps → React Props → 渲染。一旦这个链路想明白了剩下就是体力活。如果你也想动手写一个 Superset 插件我的建议是从修改一个已有的官方插件开始。比如复制一份 ECharts Timeseries 的代码把它改成柱状图折线图组合图这个改造过程比你从零开始能学到更多。我是朱大喜一个因为画不好漏斗图被迫学会了 React 的数据分析师。你有没有被 Superset 某个内置图表气到想自己写插件的时刻评论区见~