Metabase 嵌入式分析 SDK 的 MetabaseDataPointObject:点击数据点对象类型完整指南
Metabase 嵌入式分析 SDK 的 MetabaseDataPointObject:点击数据点对象类型完整指南
MetabaseDataPointObject 是 Metabase 嵌入式分析 SDK(Embedded Analytics SDK)在用户点击可视化图表时,传递给点击行为插件(click action plugin)的结构化数据对象。它承载了被点击单元格的值、所属列、整行数据、触发事件以及当前 Question 元信息,是使用 mapQuestionClickActions 插件实现自定义下钻、联动弹窗、条件跳转等交互的核心入参。读完本文,你将完整掌握该类型的全部字段语义、它在 SDK 内部是如何从底层 ClickObject 转换而来的,以及如何在实际的 InteractiveDashboard / InteractiveQuestion 场景中消费它。
一、什么是 MetabaseDataPointObject
在 Metabase 嵌入式分析 SDK 中,当终端用户点击图表上的某个数据点(如柱状图的一根柱子、饼图的一个扇区、表格中的某个单元格)时,SDK 会把这个点击行为抽象为一个"数据点对象",作为第二个参数传入点击行为插件配置 mapQuestionClickActions。
该类型定义在 SDK 插件类型文件 中,同时通过 SDK 打包入口类型文件 对外导出,供通过 embedding-sdk-bundle 包名导入的宿主应用使用。其完整类型声明如下:
type MetabaseDataPointObject = {
column?: {
display_name?: string;
name?: string;
};
data?: Record<string, string | number | null | boolean | object>;
event?: MouseEvent;
question?: MetabaseQuestion;
raw?: {
column?: Record<string, any>;
data?: {
col: Record<string, any> | null;
value: string | number | null | boolean;
}[];
event?: MouseEvent;
value?: string | number | null | boolean;
};
value?: string | number | null | boolean | object;
};
从设计意图上看,SDK 团队在源码注释中明确说明(见 transform-clicked.ts):
将内部
question与clicked对象转换成一个更简单、更不易令人不知所措的结构;同时在raw中保留原始对象,供需要访问映射值(mapped values)等高级场景使用。
因此该类型本质上是内部点击对象的一个"面向插件开发者"的简化投影:常用字段被整理成易读、稳定的形态,完整原始数据则原样保留在 raw 中。
二、属性详解
原文档以 TypeDoc 生成的 API 表格形式列出了全部属性,下表完整继承并补充了每个字段的语义说明:
| 属性 | 类型 | 语义 |
|---|---|---|
column? |
{ display_name?: string; name?: string; } |
被点击单元格所属列的元信息,包含内部字段名 name 与界面展示名 display_name |
column.display_name? |
string |
列在界面上的展示名称(如 "Total"、"Created At") |
column.name? |
string |
列的内部名称(数据库字段名,如 total、created_at) |
data? |
Record<string, string | number | null | boolean | object> |
被点击行的完整数据,以"列名 → 值"的映射形式提供,便于按列名直接取值 |
event? |
MouseEvent |
触发此次点击的浏览器原生鼠标事件,可用于获取坐标、判断修饰键(shift/ctrl/alt)等 |
question? |
MetabaseQuestion |
被点击的 Question 对象(见下文"MetabaseQuestion 字段"一节) |
raw? |
见下方子结构 | 底层内部点击对象的原样保留,包含最完整、未经简化的字段 |
raw.column? |
Record<string, any> |
完整的列对象(DatasetColumn),包含 base_type、semantic_type 等全部列元数据 |
raw.data? |
{ col: Record<string, any> | null; value: string | number | null | boolean; }[] |
原始的行数据数组,每项为"列对象 + 单元格值"的结构 |
raw.event? |
MouseEvent |
原始事件对象 |
raw.value? |
string | number | null | boolean |
原始单元格值(注意:raw.value 不包含 object 类型,而顶层 value 包含) |
value? |
string | number | null | boolean | object |
被点击单元格的值,可能是标量(字符串/数字/布尔/null),也可能是对象 |
2.1 顶层 value 与 raw.value 的差异
顶层 value 的类型比 raw.value 多了一个 object 联合成员(见 类型定义)。这意味着顶层 value 在转换时是直接透传内部 clicked.value 的原始值,而 raw.value 则是内部 ClickObject.value 字段(RowValue)在内部类型层面的表达。实际使用中二者通常指向同一份数据,但类型上顶层更宽松,插件代码按顶层取值即可。
2.2 data 字段的构造方式
data 是一个以列名为键的 Record,它并非 SDK 内部直接产生,而是在转换函数中由 raw.data(数组结构)折叠(reduce)而成——见下文第三节的源码分析。这个字段让你无需遍历数组,即可通过 data["订单金额"] 或 data.orders_count 这类方式直接取出被点击行的任意列值,是编写联动逻辑时最常用的入口。
三、源码级解析:对象是如何被构造的
MetabaseDataPointObject 并非凭空而来,而是由 SDK 内部的转换函数 transformClickedDataPoint 从底层点击对象 ClickObject 转换得到。完整实现位于 frontend/src/metabase/embedding-sdk/lib/transform-clicked.ts:
export function transformClickedDataPoint(
clicked: ClickObject,
question: Question,
): MetabaseDataPointObject {
return {
// Unjustified type cast. FIXME
raw: clicked as Record<string, unknown>,
column: clicked.column
? {
name: clicked.column.name,
display_name: clicked.column.display_name,
}
: undefined,
value: clicked.value,
question: transformSdkQuestion(question),
data: clicked.data?.reduce<
Record<string, string | number | null | boolean | object>
>((acc, curr) => {
if (curr.col) {
acc[curr.col.name] = curr.value;
}
return acc;
}, {}),
};
}
逐字段对应关系可以总结为:
MetabaseDataPointObject 字段 |
来源 |
|---|---|
raw |
内部 ClickObject 原样透传(强转为 Record<string, unknown>) |
column |
取 clicked.column 的 name 与 display_name 两个字段,若无列则为 undefined |
value |
透传 clicked.value |
question |
经 transformSdkQuestion 转换后的 Question 摘要 |
data |
将 clicked.data({ col, value }[] 数组)按 col.name 折叠成 Record |
3.1 底层 ClickObject 的结构
raw 中保留的内部对象类型为 ClickObject,其完整定义位于 frontend/src/metabase-lib/query/types.ts。除上文已见字段外,它还包含插件场景可能用到的扩展信息:
export interface ClickObject {
value?: RowValue;
column?: DatasetColumn;
dimensions?: ClickObjectDimension[];
event?: MouseEvent;
element?: Element;
seriesIndex?: number;
cardId?: CardId;
settings?: Record<string, unknown>;
columnShortcuts?: boolean;
origin?: {
rowIndex?: number;
row: RowValue[];
cols: DatasetColumn[];
};
extraData?: Record<string, unknown>;
data?: ClickObjectDataRow[];
brushRange?: BrushRange;
}
其中值得注意的几个高级字段:
dimensions:被点击数据点所对应的维度信息(如分组的字段与其值),是判断"用户点了哪个分组"的关键;origin.row/origin.cols:整行原始数据与列定义,适合需要完整行上下文的高级联动;extraData:额外的任意元数据;brushRange:仅在刷选类图表(如缩放选择)中存在,此时对象同时满足BrushClickObject约束(见 isBrushClickObject 判定)。
由于 raw 字段的类型是 Record<string, any>,插件代码可以安全地访问这些内部字段而不受类型约束,但需要注意内部结构可能随 SDK 版本演进而变化,优先使用顶层简化字段,仅在确有高级需求时再读取 raw。
3.2 question 字段与 MetabaseQuestion
question 字段的类型为 MetabaseQuestion,其定义在 question.ts:
export interface MetabaseQuestion {
id: number;
name: string;
description: string | null;
entityId: string;
isSavedQuestion: boolean;
}
该对象由 transformSdkQuestion(见 transform-question.ts)从内部 Question 对象提取:
export function transformSdkQuestion(question: Question): MetabaseQuestion {
const card = question.card();
return {
id: question.id(),
name: question.displayName() ?? "",
description: question.description(),
entityId: card.entity_id,
isSavedQuestion: question.isSaved(),
};
}
id:Question 的数字 ID(临时问题可能为 0 或空值,可用isSavedQuestion区分);name:展示名称;description:描述(可为null);entityId:序列化/迁移场景使用的实体 ID(字符串);isSavedQuestion:是否为已保存的 Question——注意:当用户点击的是仪表盘上临时创建的图表或 ad-hoc 分析结果时,该值为false,编写依赖 Question ID 的条件逻辑前应先判断此字段。
四、在插件中消费 MetabaseDataPointObject
MetabaseDataPointObject 的典型使用场景是 MetabasePluginsConfig.mapQuestionClickActions 插件回调。插件类型定义同样位于 plugins.ts:
export type MetabaseClickActionPluginsConfig = (
clickActions: ClickAction[],
clickedDataPoint: MetabaseDataPointObject,
) => ClickAction[] | { onClick: () => void };
回调接收两个参数:SDK 内置的默认点击行为数组 clickActions,以及本文的主角 clickedDataPoint。返回值可以是:
ClickAction[]:追加或替换后的行为数组;{ onClick: () => void }:直接执行单个行为(相当于把唯一行为设为默认动作)。
4.1 插件调用的底层流程
mapQuestionClickActions 的调用并非直接发生在组件中,而是经过 SDK 的 Mode 包装层。在 getEmbeddingMode.ts 中,applyClickActionsPlugin 负责将插件与内部点击管线衔接:
const actionsOrActionObject = plugins.mapQuestionClickActions(
actions,
transformClickedDataPoint(clicked, question),
);
if (Array.isArray(actionsOrActionObject)) {
return actionsOrActionObject;
}
if ("onClick" in actionsOrActionObject) {
return [
{
default: true,
section: "auto",
type: "custom",
buttonType: "horizontal",
name: "default",
...(actionsOrActionObject as Partial<ClickAction>),
},
];
}
console.warn(
"mapQuestionClickActions should return an array of actions, or a single object with a `onClick` property",
);
return actions;
这段实现揭示了三个关键事实:
- 每次点击都会先调用
transformClickedDataPoint,把内部ClickObject转换成MetabaseDataPointObject后再交给你的插件函数; - 返回
{ onClick }对象时,SDK 会把它包装成一个default: true的ClickAction,此时点击将直接执行该动作而不再弹出行为菜单(源码注释也说明了这一点:"当一次点击恰好产生一个行为且其default: true时,performDefaultAction会直接运行它而不是弹出 popover"); - 若返回值既不是数组也不含
onClick属性,SDK 会输出console.warn提示并回退为默认行为数组。
4.2 实战示例:读取点击数据点
仓库中的 Story 演示了三种典型用法(见 mapQuestionClickActions.stories.tsx),以下是经提炼的可运行模式:
示例一:追加一个自定义行为,读取被点击 Question 的名称
<InteractiveDashboard
dashboardId={1}
withDownloads
plugins={{
mapQuestionClickActions: (clickActions, clicked) => {
return [
...clickActions,
{
buttonType: "horizontal",
name: "custom",
title: "Open modal",
onClick: () => {
// clicked 即 MetabaseDataPointObject
setModalData(clicked);
},
},
];
},
}}
/>
示例二:根据点击数据点条件分发——命中特定 Question 直接执行动作
mapQuestionClickActions: (clickActions, clicked) => {
// 命中 dashboardId=1 仪表盘上的第一个图表(Question id === 12)时直接执行
if (clicked.question?.id === 12) {
return {
onClick: () => setModalData(clicked),
};
}
// 其余图表仍展示默认行为菜单,并追加自定义项
return [
...clickActions,
{
name: "custom",
title: "Open modal",
buttonType: "horizontal",
onClick: () => setModalData(clicked),
},
];
},
示例三:读取单元格值与整行数据
mapQuestionClickActions: (clickActions, clicked) => {
// 单元格值(标量或对象)
console.log("clicked value:", clicked.value);
// 列信息
console.log("column:", clicked.column?.name, clicked.column?.display_name);
// 整行数据(列名 → 值)
console.log("row data:", clicked.data);
// 原始完整对象(高级场景)
console.log("raw:", clicked.raw);
return clickActions;
},
五、最佳实践与注意事项
综合类型定义、转换源码与插件调用链,可以总结出以下实践建议:
-
优先使用顶层字段,谨慎依赖
raw。raw是对内部ClickObject的透传,字段结构与 SDK 内部实现强耦合,升级 SDK 时可能变化;而column、data、value、question是稳定的公共 API。 -
判空处理不可省略。类型中除
question外的顶层字段全部是可选的:表格类可视化的某些区域点击可能没有column;部分图表(如折线图空白处)可能没有value。参考仓库中 transformClickedDataPoint 对clicked.column的undefined处理,插件内应使用可选链(clicked.column?.name)安全访问。 -
question可能不是已保存的 Question。isSavedQuestion为false时id的可用性有限(ad-hoc 问题没有持久化 ID),条件逻辑(如示例二中按id === 12判断)应同时考虑该标志。 -
用
data而不是遍历raw.data。转换函数已把行数据折叠为Record<列名, 值>,直接按键取值即可,这也是该字段存在的意义;只有需要同时拿到"列对象 + 值"(例如要读取列的semantic_type、base_type)时才去遍历raw.data。 -
{ onClick }返回值会跳过行为菜单。如 getEmbeddingMode.ts 所示,返回单动作对象会被包装为default: true并直接执行,适合"点击即触发"的交互;如需保留菜单则始终返回数组。
六、关联资源
- 类型定义:frontend/src/metabase/embedding-sdk/types/plugins.ts
- SDK 打包导出:frontend/src/embedding-sdk-bundle/types/plugins.ts
- 转换实现:frontend/src/metabase/embedding-sdk/lib/transform-clicked.ts
- Question 转换:frontend/src/metabase/embedding-sdk/lib/transform-question.ts
MetabaseQuestion类型:frontend/src/metabase/embedding-sdk/types/question.ts- 底层
ClickObject:frontend/src/metabase-lib/query/types.ts - 插件接线层:frontend/src/embedding-sdk-bundle/lib/modes/getEmbeddingMode.ts
- 可运行示例:frontend/src/embedding-sdk-bundle/stories/mapQuestionClickActions.stories.tsx
- 关联类型文档:MetabaseClickActionPluginsConfig、MetabaseQuestion、MetabaseClickAction