Metabase 嵌入式分析 SDK 的 MetabaseDataPointObject:点击数据点对象类型完整指南

原创2026-09-09 21:15:431,823 阅读
文章标签:数据分析数据可视化后端前端

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;

这段实现揭示了三个关键事实:

  1. 每次点击都会先调用 transformClickedDataPoint,把内部 ClickObject 转换成 MetabaseDataPointObject 后再交给你的插件函数;
  2. 返回 { onClick } 对象时,SDK 会把它包装成一个 default: true 的 ClickAction,此时点击将直接执行该动作而不再弹出行为菜单(源码注释也说明了这一点:"当一次点击恰好产生一个行为且其 default: true 时,performDefaultAction 会直接运行它而不是弹出 popover");
  3. 若返回值既不是数组也不含 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;
},

五、最佳实践与注意事项

综合类型定义、转换源码与插件调用链,可以总结出以下实践建议:

  1. 优先使用顶层字段,谨慎依赖 raw。raw 是对内部 ClickObject 的透传,字段结构与 SDK 内部实现强耦合,升级 SDK 时可能变化;而 column、data、value、question 是稳定的公共 API。

  2. 判空处理不可省略。类型中除 question 外的顶层字段全部是可选的:表格类可视化的某些区域点击可能没有 column;部分图表(如折线图空白处)可能没有 value。参考仓库中 transformClickedDataPoint 对 clicked.column 的 undefined 处理,插件内应使用可选链(clicked.column?.name)安全访问。

  3. question 可能不是已保存的 Question。isSavedQuestion 为 false 时 id 的可用性有限(ad-hoc 问题没有持久化 ID),条件逻辑(如示例二中按 id === 12 判断)应同时考虑该标志。

  4. 用 data 而不是遍历 raw.data。转换函数已把行数据折叠为 Record<列名, 值>,直接按键取值即可,这也是该字段存在的意义;只有需要同时拿到"列对象 + 值"(例如要读取列的 semantic_type、base_type)时才去遍历 raw.data。

  5. { onClick } 返回值会跳过行为菜单。如 getEmbeddingMode.ts 所示,返回单动作对象会被包装为 default: true 并直接执行,适合"点击即触发"的交互;如需保留菜单则始终返回数组。

六、关联资源

登录后查看全文
metabase