` API 实战指南:从点击行构建下钻查询)
Lightdash Data AppdrillDown()API 实战指南从点击行构建下钻查询【免费下载链接】lightdashAgentic BI. Analytics at the speed of code ⚡️项目地址: https://gitcode.com/GitHub_Trending/li/lightdashdrillDown()是 Lightdash Query SDKlightdash/query-sdk提供的一个查询构建辅助函数它接收用户点击的数据行自动生成一个新的QueryBuilder让「点击某个汇总值 → 按更细粒度维度查看明细」这一经典下钻交互可以在一行代码内完成。本文基于 drilldown.md 参考文档并结合 query-sdk 源码 深入讲解其 API 语义、底层实现与在 Data App 中的完整接入方式。读完本文你将掌握drillDown()的完整调用范式、钻取维度的选择策略、结果对话框的组件拆分以及一套可直接复用的「图表/表格 action menu 下钻 Dialog」集成模板。drillDown()是什么语义与适用场景在 Lightdash Data App 的模板环境中每个由useLightdash()驱动的图表和表格都应具备数据交互能力——点击数据点时弹出 action menu至少包含「Filter byvalue」通常还包含「View underlying data」和「Drill into …」。其中「下钻」的含义是保持当前度量metric不变换一个更细的维度dimension重新分组。drillDown()正是为这个动作而生的drillDown()builds a new query from a clicked row.它做的事情可以概括为三句话从被点击的行中取出源查询所有维度的值转成等值过滤器equality filters保留源查询已有的全部过滤器只替换分组维度返回一个可直接交给useLightdash()执行的新的QueryBuilder。它与query、useLightdash一起从 SDK 导出导入方式如下import { query, useLightdash, drillDown } from lightdash/query-sdk;在 packages/query-sdk/src/index.ts 中可以看到它是 SDK 的一等导出成员export { drillDown } from ./drillDown;与query、savedChart、useLightdash、LightdashProvider等并列。API 签名与参数语义drillDown()接受一个DrillDownOptions对象返回一个新的QueryBuilderdrillDown({ sourceQuery, // The QueryBuilder that produced the clicked data metric, // Which metric to drill into (string) dimension, // Which dimension to drill by (string) row, // The clicked row from useLightdash data label, // Optional label for query inspector }) // → QueryBuilder对照 packages/query-sdk/src/drillDown.ts 中的类型定义每个参数的含义如下参数类型必填含义sourceQueryQueryBuilder是产生被点击数据的原始查询构建器。实现中会调用sourceQuery.build()读取其维度、过滤器与 explore 名称metricstring是要下钻的度量字段名短名称如total_revenue将成为新查询中唯一的度量dimensionstring是下钻所依据的新维度如order_date、product_name将成为新查询中唯一的维度rowRow是从useLightdash()返回的data数组中拿到的被点击行其维度值将被转换为等值过滤器labelstring否查询标签不要传默认标签已足够见下文为什么「不要传 label」文档明确指出Do not pass alabel— the default label is automatically prefixed with[Drill down](e.g.,[Drill down] total_revenue by order_date), which makes drill queries easy to identify in the query inspector.即默认标签格式为[Drill down] metric by dimension例如[Drill down] total_revenue by order_date。在实现里可以看到默认值逻辑drillDown.ts.label(label ?? [Drill down] ${metric} by ${dimension})带[Drill down]前缀能让开发者在 query inspector查询检查器中一眼区分下钻查询与普通查询因此不需要自定义。如果你确实有特殊需要例如同时存在多个下钻需要区分来源仍可通过label覆盖。返回的QueryBuilder具备哪些特征drillDown()的返回值是一个全新的、独立的QueryBuilder其结构特征如下下钻维度是唯一维度.dimensions([dimension])被下钻的度量.metrics([metric])来自点击行的每个维度值的等值过滤器源查询中每一个维度都会根据被点击行的取值生成过滤器源查询已有的全部过滤器被保留原有过滤器原样带过来隐含的排序与行数限制按钻取维度升序排序限制 500 行这些行为在实现中是字面可见的drillDown.tsreturn new QueryBuilder(sourceDef.exploreName) .label(label ?? [Drill down] ${metric} by ${dimension}) .dimensions([dimension]) .metrics([metric]) .filters([...sourceFilters, ...rowFilters]) .sorts([{ field: dimension, direction: asc }]) .limit(500);底层实现拆解行值如何变成过滤器下钻的核心是把「点击了哪一行」翻译成查询条件。实现分两步drillDown.ts第一步从点击行生成维度等值过滤器rowFiltersconst rowFilters: Filter[] sourceDef.dimensions.map((dimFieldId) { const value row[dimFieldId]; if (value null || value undefined) { return { field: dimFieldId, operator: isNull as const }; } return { field: dimFieldId, operator: equals as const, value: value as string | number | boolean, }; });要点源查询里选中的所有维度都会被扫描一遍注意是sourceDef.dimensions即源查询的分组维度而不仅仅是钻取维度。如果该维度在被点击行中恰好为null/undefined则生成isNull过滤器而非等值过滤器——这样即使点击的行包含空维度值下钻查询仍然语义正确。第二步携带源查询的既有过滤器sourceFiltersconst sourceFilters: Filter[] sourceDef.filters.map((f) ({ field: f.fieldId, operator: f.operator as Filter[operator], ...(f.values.length 0 ? { value: f.values.length 1 ? f.values[0] : f.values } : {}), ...(f.settings?.unitOfTime ? { unit: f.settings.unitOfTime } : {}), }));源查询的过滤条件sourceDef.filters属于内部表示InternalFilterDefinition被转换为 SDK 的公开Filter形状operator直接透传value在单值时取第一个元素、多值时保留数组时间单位的相对日期过滤器unitOfTime则映射为unit字段。关于Filter的完整类型可以参考 packages/query-sdk/src/types.ts包含field、operator、value、unitdays | weeks | months | quarters | years和completed用于inThePast等相对日期过滤器是否排除当前进行中的周期。执行下钻查询返回的QueryBuilder是一个不可变构建器——每个链式方法都返回新实例见 packages/query-sdk/src/query.ts 的_clone实现。直接把它交给useLightdash()即可执行const { data, columns, format, loading, error } useLightdash(drillQuery);useLightdash的返回值packages/query-sdk/src/useLightdash.ts包括data扁平对象数组、columns字段元数据、format服务端格式化函数、loading、error、lineage、getUnderlyingData、downloadResults等足以支撑结果表格的渲染与后续导出。选择下钻维度何时钻、往哪钻下钻的价值在于「给出对度量有意义的更细粒度详情」。文档给出了三个直观示例按月的收入Revenue by month→ 下钻到天或按产品drill by day or by product按客户分群的总量Total by segment→ 下钻到单个客户drill by individual customer按地区的汇总Summary by region→ 下钻到城市drill by city判断标准很简单钻取维度应该比源查询的分组粒度更细能够解释该度量的构成。反过来如果钻取维度与源查询已有的分组维度相同下钻就没有任何意义——这同时也是文档的 common pitfalls 中明确列出的错误「Drilling by a dimension already in the source query → Pointless — same grouping」应改选一个不同且更细的维度。Agent 构建期选择 vs 用户可选维度在 Lightdash Data App 的生成管线中下钻维度通常由构建 Agent 在生成代码时根据 dbt YAML位于/tmp/dbt-repo/models/下每模型一个文件决定——它知道哪个维度对该度量最有解释力。但如果产品需求是让用户自己选下钻维度文档给出了明确方案用一个Select填充候选维度选项把选择结果传给drillDown()的dimension参数const [drillDim, setDrillDim] useState(order_date); // In the menu item onClick: setDrillQuery(drillDown({ sourceQuery, metric: total_revenue, dimension: drillDim, row }));这里的drillDim可以是useState维护的默认值也可以由Select的选项列表驱动——drillDown()对维度名称本身没有额外限制只要该字段在 dbt YAML 中确实存在且可作为维度使用。展示下钻结果Dialog 独立组件对话框标题必须显示被过滤的值文档强调了一条易被忽视的 UX 原则Always show the filtered value in the dialog title— e.g., Revenue for Enterprise or Orders for 2024-01. This tells the user what they clicked.下钻查询会在后台静默附加等值过滤器如果对话框标题只是笼统的「Drill down」用户会困惑「我到底在看什么」。正确做法是把下钻查询与描述性标题一起放入 state例如{ query, title }结构title 用format()格式化被点击值后拼接如setDrillState({ query: drillDown({ sourceQuery: chartQuery, metric: total_revenue, dimension: order_date, row, }), title: Revenue for ${format(row, customer_segment)}, });用独立组件承载结果让useLightdash只在对话框打开时运行useLightdash在组件挂载时就会发起查询。如果把它直接放在包含图表的主组件里下钻查询会在页面加载时就被执行一次——这是不必要的查询浪费。文档给出的解法是把结果渲染拆成一个独立子组件只有对话框打开子组件挂载时才运行useLightdashfunction DrillResults({ query: q }) { const { data, columns, format, loading, error } useLightdash(q); if (loading) return div classNameflex justify-center p-8Loader2 classNameh-6 w-6 animate-spin text-muted-foreground //div; if (error) return Alert variantdestructiveAlertDescription{error.message}/AlertDescription/Alert; if (data.length 0) return p classNametext-sm text-muted-foregroundNo results/p; return ( ScrollArea classNamemax-h-[400px] Table TableHeader TableRow {columns.map((col) TableHead key{col.name}{col.label}/TableHead)} /TableRow /TableHeader TableBody {data.map((row, i) ( TableRow key{i} {columns.map((col) ( TableCell key{col.name}{formatField(row, col, format, cell)}/TableCell ))} /TableRow ))} /TableBody /Table /ScrollArea ); }这个组件演示了结果表格的完整渲染范式各部分的职责loading显示居中旋转的Loader2图标来自lucide-react容器高度与内容对齐避免布局跳动error使用Alert variantdestructive展示error.message空结果渲染「No results」提示文本数据渲染表头用columns的name/label元数据生成单元格统一使用formatField(row, col, format, cell)来自/lib/format模板预装——它会把日期渲染为Jun 16, 2025这种人类可读形式同时让货币/百分比度量走 SDK 的服务端格式化保持与 dbt YAML 中定义的格式一致滚动用ScrollArea限制max-h-[400px]保证多行结果不撑爆对话框。在父组件中对话框与状态绑定{drillState ( Dialog open onOpenChange{() setDrillState(null)} DialogContent classNamemax-w-3xl DialogHeaderDialogTitle{drillState.title}/DialogTitle/DialogHeader DrillResults query{drillState.query} / /DialogContent /Dialog )}模板环境已预装 shadcn/ui 的Dialog、Table、Alert、ScrollArea、DropdownMenu、Select等组件见 template 的 skill 文档可直接从/components/ui/name导入无需自行安装依赖。完整接入示例Action Menu 中的下钻项drillDown()通常不是独立使用的而是作为图表 action menu 的第三个选项出现。下面的完整示例来自 skill.md 的 action-menu 模式展示了下钻与「Filter by」、getUnderlyingData三者如何共存在一个点击菜单中import { useState, useMemo } from react; import { createPortal } from react-dom; import { query, useLightdash, drillDown } from lightdash/query-sdk; import { Bar, BarChart } from recharts; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from /components/ui/dropdown-menu; import { useGlobalFilters } from /lib/filters; const EXPLORE orders; const baseQuery query(EXPLORE) .label(Revenue by Segment) .dimensions([customer_segment]) .metrics([total_revenue]); function RevenueChart() { const { filtersFor, addFilter } useGlobalFilters(); const chartQuery useMemo( () baseQuery.filters(filtersFor(EXPLORE)), [filtersFor], ); const { data, format, loading, getUnderlyingData, downloadUnderlyingData } useLightdash(chartQuery); const [menuState, setMenuState] useState(null); // { row, x, y } const [drillState, setDrillState] useState(null); // { query, title } const [underlyingState, setUnderlyingState] useState(null); // { title, row, metric, promise } return ( BarChart data{data} {/* ... axes, etc. */} Bar dataKeytotal_revenue onClick{(item, _index, event) { if (!item.payload) return; setMenuState({ row: item.payload, x: event.clientX, y: event.clientY, }); }} / /BarChart {menuState createPortal( DropdownMenu open onOpenChange{() setMenuState(null)} DropdownMenuTrigger asChild div style{{ position: fixed, left: menuState.x, top: menuState.y, width: 1, height: 1 }} / /DropdownMenuTrigger DropdownMenuContent DropdownMenuItem onClick{() { addFilter({ field: customer_segment, operator: equals, value: menuState.row[customer_segment], explore: EXPLORE, }); setMenuState(null); }} Filter by {format(menuState.row, customer_segment)} /DropdownMenuItem DropdownMenuItem onClick{() { const row menuState.row; setUnderlyingState({ title: Orders behind ${format(row, customer_segment)}, row, metric: total_revenue, promise: getUnderlyingData({ row, metric: total_revenue, limit: 500, }), }); setMenuState(null); }} View underlying data /DropdownMenuItem DropdownMenuItem onClick{() { const row menuState.row; setDrillState({ query: drillDown({ sourceQuery: chartQuery, metric: total_revenue, dimension: order_date, row, }), title: Revenue for ${format(row, customer_segment)}, }); setMenuState(null); }} Drill into revenue /DropdownMenuItem /DropdownMenuContent /DropdownMenu, document.body, )} {drillState ( Dialog open onOpenChange{() setDrillState(null)} DialogContent classNamemax-w-3xl DialogHeaderDialogTitle{drillState.title}/DialogTitle/DialogHeader DrillResults query{drillState.query} / /DialogContent /Dialog )} {/* underlyingState 的 Dialog 渲染包含带 Download 按钮的 UnderlyingRows 组件 */} / ); }这个模式中值得注意的工程细节drillState存{ query, title }查询对象与人类可读标题一起入 stateDialogTitle直接渲染drillState.title确保用户始终知道当前下钻的上下文在 onClick 中构建下钻查询而不是在 render 中drillDown()只在点击时调用一次并存入 state避免每次渲染都生成新的QueryBuilder导致useLightdash反复触发查询这是文档 common pitfalls 明确列出的「Building drill query inside render → Infinite re-fetching」错误菜单用createPortal挂到document.body避免被带transform动画的祖先元素劫持为position: fixed的包含块导致菜单偏离点击位置菜单坐标来自 Recharts 3 item-level handler 的第三个参数event.clientX/event.clientY而不是已移除的图表级activePayload或e.chartX。常见陷阱与规避结合 drilldown.md 与 skill.md 中的 common pitfalls使用drillDown()时最容易踩的坑如下陷阱后果正确做法在 render 中构建下钻查询QueryBuilder每次渲染都是新实例useLightdash无限重取在 onClick 处理器中构建并存入 state钻取维度与源查询分组维度相同分组不变下钻无意义选择更细粒度、不同的维度忘记在对话框标题显示被过滤值用户不知道自己在看什么数据把{ query, title }一起入 state标题用format()展示被点击值把useLightdash放在主组件里跑下钻查询页面加载时就执行了多余的查询拆成DrillResults独立组件随 Dialog 挂载才执行下钻结果表格不用formatField日期显示为2025-03-17这类制表符形式货币/百分比丢失服务端格式单元格统一用formatField(row, col, format, cell)使用格式化后的展示值如$1,234做过滤/下钻无法匹配原始行数据一律使用原始值row[field]format()只用于菜单标签与标题忽略加载状态对话框内出现空白表格loading 时显示Loader2旋转图标容器高度与内容对齐与 Query SDK 其他能力的配合drillDown()是 query-sdk 中与query、savedChart、useLightdash并列的核心导出之一其返回的QueryBuilder天然兼容 SDK 的其他能力与全局过滤器协同下钻查询在构造时已携带源查询的过滤器包括通过filtersFor(EXPLORE)注入的全局过滤器因此下钻结果与图表本身处于一致的过滤上下文结果可继续导出如果希望下钻结果也能导出 CSV/XLSX可直接对DrillResults中的查询使用downloadResults({ fileType, values, limit, filename })后端导出管线不经过 iframe 序列化结果可继续下钻DrillResults中的数据行同样可以再次作为row传入drillDown()实现多级下钻——只要每次选择更细的维度。小结drillDown()把「点击行 → 生成新查询」这一交互背后的模板代码压缩成一个语义清晰的函数调用行值变等值过滤器、源过滤器原样保留、维度替换、默认标签与排序均由实现兜底。在 Data App 中正确使用它的要点可以总结为四句话在 onClick 中构建查询、把{ query, title }一起入 state、标题显示被过滤的值、用独立组件随 Dialog 挂载执行useLightdash。掌握这些模式后你可以在任何由useLightdash()驱动的图表或表格上以极低的成本添加专业水准的下钻能力。【免费下载链接】lightdashAgentic BI. Analytics at the speed of code ⚡️项目地址: https://gitcode.com/GitHub_Trending/li/lightdash创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考