From d151c5f80dd4795db3e508495b3cbe7e68e29f43 Mon Sep 17 00:00:00 2001 From: Dhiraj Kumar Date: Sun, 11 Jan 2026 20:13:12 +0530 Subject: [PATCH 01/53] init pivot click filter --- .../pivot/CanvasPivotDisplay.svelte | 1 + .../pivot/CanvasPivotRenderer.svelte | 77 +++++++++++++++++++ .../dashboards/pivot/PivotTable.svelte | 15 +++- .../pivot/pivot-filter-extraction.ts | 44 +++++++++++ 4 files changed, 135 insertions(+), 2 deletions(-) create mode 100644 web-common/src/features/dashboards/pivot/pivot-filter-extraction.ts diff --git a/web-common/src/features/canvas/components/pivot/CanvasPivotDisplay.svelte b/web-common/src/features/canvas/components/pivot/CanvasPivotDisplay.svelte index 4bc857e4472..dfa14c78cb3 100644 --- a/web-common/src/features/canvas/components/pivot/CanvasPivotDisplay.svelte +++ b/web-common/src/features/canvas/components/pivot/CanvasPivotDisplay.svelte @@ -78,4 +78,5 @@ {pivotDataStore} pivotConfig={config} {pivotState} + {component} /> diff --git a/web-common/src/features/canvas/components/pivot/CanvasPivotRenderer.svelte b/web-common/src/features/canvas/components/pivot/CanvasPivotRenderer.svelte index 1e183177beb..cd416df6417 100644 --- a/web-common/src/features/canvas/components/pivot/CanvasPivotRenderer.svelte +++ b/web-common/src/features/canvas/components/pivot/CanvasPivotRenderer.svelte @@ -1,5 +1,6 @@
{/if} {/if} diff --git a/web-common/src/features/dashboards/pivot/PivotTable.svelte b/web-common/src/features/dashboards/pivot/PivotTable.svelte index 32c9351b9e7..7f0a994fc71 100644 --- a/web-common/src/features/dashboards/pivot/PivotTable.svelte +++ b/web-common/src/features/dashboards/pivot/PivotTable.svelte @@ -59,6 +59,9 @@ export let setPivotRowLimitForExpanded: | ((expandIndex: string, limit: number) => void) | undefined = undefined; + export let onCellClickToFilter: + | ((rowId: string, columnId: string) => Promise) + | undefined = undefined; const options: Readable> = derived( [pivotDataStore, pivotState], @@ -222,8 +225,16 @@ if (rowHeader) { if (row.getCanExpand()) row.getToggleExpandedHandler()(); - } else if (setPivotActiveCell && canShowDataViewer) { - setPivotActiveCell(rowId, columnId); + } else { + // Set active cell for dashboard (if enabled) + if (setPivotActiveCell && canShowDataViewer) { + setPivotActiveCell(rowId, columnId); + } + + // Apply filters for canvas (if enabled) + if (onCellClickToFilter) { + onCellClickToFilter(rowId, columnId); + } } } diff --git a/web-common/src/features/dashboards/pivot/pivot-filter-extraction.ts b/web-common/src/features/dashboards/pivot/pivot-filter-extraction.ts new file mode 100644 index 00000000000..609c1b63106 --- /dev/null +++ b/web-common/src/features/dashboards/pivot/pivot-filter-extraction.ts @@ -0,0 +1,44 @@ +import { + V1Operation, + type V1Expression, +} from "@rilldata/web-common/runtime-client"; + +export interface ExtractedFilter { + dimensionName: string; + values: string[]; +} + +/** + * Extracts dimension filters from a V1Expression structure. + * The expression is expected to be an AND operation containing IN operations. + * + * @param filters - The V1Expression containing dimension filters + * @returns Array of extracted dimension filters with their values + */ +export function extractDimensionFiltersFromExpression( + filters: V1Expression | undefined, +): ExtractedFilter[] { + if (!filters?.cond?.exprs) return []; + + const result: ExtractedFilter[] = []; + + // Walk the AND expression tree + for (const expr of filters.cond.exprs) { + if (expr.cond?.op === V1Operation.OPERATION_IN) { + const ident = expr.cond.exprs?.[0]?.ident; + if (!ident) continue; + + // Extract values (skip first expr which is the identifier) + const values = expr?.cond?.exprs + ?.slice(1) + .map((e) => e.val) + .filter((val): val is string => val !== undefined && val !== null); + + if (values && values?.length > 0) { + result.push({ dimensionName: ident, values }); + } + } + } + + return result; +} From ee819cc152e1a398aa7e4a3637e531828d910680 Mon Sep 17 00:00:00 2001 From: Dhiraj Kumar Date: Mon, 2 Mar 2026 20:56:59 +0530 Subject: [PATCH 02/53] init pivot click to filter --- .../pivot/CanvasPivotRenderer.svelte | 201 ++++++++++++------ .../features/canvas/components/pivot/index.ts | 5 + .../features/canvas/components/pivot/util.ts | 58 ++++- .../dashboards/pivot/FlatTable.svelte | 39 +++- .../dashboards/pivot/NestedTable.svelte | 45 +++- .../pivot/PivotExpandableCell.svelte | 19 +- .../dashboards/pivot/PivotTable.svelte | 30 ++- .../dashboards/pivot/pivot-data-store.ts | 1 + .../dashboards/pivot/pivot-row-selection.ts | 124 +++++++++++ .../features/dashboards/pivot/pivot-utils.ts | 2 +- .../src/features/dashboards/pivot/types.ts | 1 + 11 files changed, 440 insertions(+), 85 deletions(-) create mode 100644 web-common/src/features/dashboards/pivot/pivot-row-selection.ts diff --git a/web-common/src/features/canvas/components/pivot/CanvasPivotRenderer.svelte b/web-common/src/features/canvas/components/pivot/CanvasPivotRenderer.svelte index cd416df6417..4f83c8729e7 100644 --- a/web-common/src/features/canvas/components/pivot/CanvasPivotRenderer.svelte +++ b/web-common/src/features/canvas/components/pivot/CanvasPivotRenderer.svelte @@ -1,7 +1,14 @@
{ pivotState.update((state) => ({ ...state, diff --git a/web-common/src/features/canvas/components/pivot/index.ts b/web-common/src/features/canvas/components/pivot/index.ts index 6ffa4559407..7ef56564d23 100644 --- a/web-common/src/features/canvas/components/pivot/index.ts +++ b/web-common/src/features/canvas/components/pivot/index.ts @@ -54,6 +54,9 @@ export class PivotCanvasComponent extends BaseCanvasComponent< config: Readable; pivotDataStore: ReturnType; pivotState: Writable; + /** Dimensions the pivot itself has filtered via click-to-filter. + * These are excluded from the pivot's own data query so all rows remain visible. */ + selfFilteredDimensions: Writable>; constructor(resource: V1Resource, parent: CanvasEntity, path: ComponentPath) { const type = resource.component?.state?.validSpec @@ -87,12 +90,14 @@ export class PivotCanvasComponent extends BaseCanvasComponent< this.type = type; this.pivotState = writable(this.getInitPivotState(type)); + this.selfFilteredDimensions = writable(new Set()); this.config = createPivotConfig( this.parent, this.specStore, this.pivotState, this.timeAndFilterStore, + this.selfFilteredDimensions, ); this.pivotDataStore = usePivotForCanvas( diff --git a/web-common/src/features/canvas/components/pivot/util.ts b/web-common/src/features/canvas/components/pivot/util.ts index ce81a4969f6..34a6d779c58 100644 --- a/web-common/src/features/canvas/components/pivot/util.ts +++ b/web-common/src/features/canvas/components/pivot/util.ts @@ -23,10 +23,35 @@ import type { V1MetricsViewSpec, V1TimeRange, } from "@rilldata/web-common/runtime-client"; -import { type Readable, type Writable, derived, writable } from "svelte/store"; +import { + type Readable, + type Writable, + derived, + readable, + writable, +} from "svelte/store"; import type { CanvasEntity } from "../../stores/canvas-entity"; import type { PivotSpec, TableSpec } from "./"; +/** + * Strips filters for the pivot's own dimensions from the where filter. + * This lets the pivot query return all rows; selection highlighting + * (via rowSelectionState) shows which rows match the active filters. + * Same pattern as leaderboard's getFiltersForOtherDimensions but for + * multiple dimensions at once. + */ +function excludeOwnDimensionFilters( + where: V1Expression | undefined, + dimensionNames: string[], +): V1Expression | undefined { + if (!where?.cond?.exprs || dimensionNames.length === 0) return where; + const dimSet = new Set(dimensionNames); + const filtered = where.cond.exprs.filter( + (e) => !dimSet.has(e.cond?.exprs?.[0]?.ident ?? ""), + ); + return createAndExpression(filtered); +} + type CacheEntry = { store: ReturnType; unsubscribe: () => void; @@ -62,21 +87,44 @@ export function createPivotConfig( tableSpecStore: Readable, pivotState: Writable, timeAndFilterStore: Readable, + selfFilteredDimensions?: Readable>, ): Readable { + const selfFilteredStore = selfFilteredDimensions ?? readable(null); + return derived( - [canvas.specStore, tableSpecStore, pivotState, timeAndFilterStore], - ([$canvasData, $tableSpec, $pivotState, $timeAndFilterStore]) => { + [ + canvas.specStore, + tableSpecStore, + pivotState, + timeAndFilterStore, + selfFilteredStore, + ], + ([ + $canvasData, + $tableSpec, + $pivotState, + $timeAndFilterStore, + $selfFiltered, + ]) => { const { timeRange, comparisonTimeRange, where } = $timeAndFilterStore; const metricsViewName = $tableSpec.metrics_view; const metricsView = $canvasData?.data?.metricsViews[metricsViewName]?.state?.validSpec ?? {}; + let queryWhere: V1Expression | undefined; + if (!$selfFiltered || $selfFiltered.size === 0) { + queryWhere = where; + } else { + // Only exclude dimensions the pivot itself applied via click-to-filter + queryWhere = excludeOwnDimensionFilters(where, [...$selfFiltered]); + } + return "columns" in $tableSpec ? processFlat( $tableSpec, $pivotState, - where, + queryWhere, metricsView, $timeAndFilterStore, comparisonTimeRange, @@ -86,7 +134,7 @@ export function createPivotConfig( : processPivot( $tableSpec, $pivotState, - where, + queryWhere, metricsView, $timeAndFilterStore, comparisonTimeRange, diff --git a/web-common/src/features/dashboards/pivot/FlatTable.svelte b/web-common/src/features/dashboards/pivot/FlatTable.svelte index 608eafa53ee..e3c7e172bc8 100644 --- a/web-common/src/features/dashboards/pivot/FlatTable.svelte +++ b/web-common/src/features/dashboards/pivot/FlatTable.svelte @@ -16,6 +16,7 @@ import { cellInspectorStore } from "../stores/cell-inspector-store"; import type { Cell, Column, HeaderGroup, Row } from "@tanstack/svelte-table"; import { flexRender } from "@tanstack/svelte-table"; + import type { PivotRowSelectionState } from "./pivot-row-selection"; import type { PivotDataRow } from "./types"; // State props @@ -24,6 +25,9 @@ export let dataRows: PivotDataRow[]; export let hasMeasureContextColumns: boolean; export let canShowDataViewer = false; + export let enableClickToFilter = false; + export let rowSelectionState: PivotRowSelectionState | undefined = undefined; + export let clickedCell: { rowId: string; columnId: string } | null = null; export let activeCell: { rowId: string; columnId: string } | null | undefined; // Table props @@ -88,6 +92,13 @@ ); } + function isCellClicked(cell: Cell) { + return ( + cell.row.id === clickedCell?.rowId && + cell.column.id === clickedCell?.columnId + ); + } + function hasBorderRight(columnId: string): boolean { if (!hasMeasureContextColumns) return true; const measureIndex = measures.findIndex((m) => m.name === columnId); @@ -184,17 +195,26 @@ {#each virtualRows as row (row.index)} {@const cells = rows[row.index].getVisibleCells()} - + {@const rowId = rows[row.index].id} + {@const isSelected = rowSelectionState?.isRowSelected(rowId) ?? false} + {@const hasSelection = rowSelectionState?.hasActiveSelection ?? false} + {#each cells as cell (cell.id)} {@const result = typeof cell.column.columnDef.cell === "function" ? cell.column.columnDef.cell(cell.getContext()) : cell.column.columnDef.cell} {@const isActive = isCellActive(cell)} + {@const isClicked = isCellClicked(cell)} diff --git a/web-common/src/features/dashboards/pivot/NestedTable.svelte b/web-common/src/features/dashboards/pivot/NestedTable.svelte index d1ac76697ff..da202114fdf 100644 --- a/web-common/src/features/dashboards/pivot/NestedTable.svelte +++ b/web-common/src/features/dashboards/pivot/NestedTable.svelte @@ -20,6 +20,7 @@ calculateRowDimensionWidth, COLUMN_WIDTH_CONSTANTS as WIDTHS, } from "./pivot-column-width-utils"; + import type { PivotRowSelectionState } from "./pivot-row-selection"; import { isShowMoreRow } from "./pivot-utils"; import type { PivotDataRow } from "./types"; @@ -32,6 +33,9 @@ export let measures: MeasureColumnProps; export let totalsRow: PivotDataRow | undefined; export let canShowDataViewer = false; + export let enableClickToFilter = false; + export let rowSelectionState: PivotRowSelectionState | undefined = undefined; + export let clickedCell: { rowId: string; columnId: string } | null = null; export let activeCell: { rowId: string; columnId: string } | null | undefined; // Table props @@ -160,6 +164,13 @@ ); } + function isCellClicked(cell: Cell) { + return ( + cell.row.id === clickedCell?.rowId && + cell.column.id === clickedCell?.columnId + ); + } + function shouldShowHeaderRightBorder(header: any, index: number): boolean { const isMeasure = isMeasureColumn(header, index); if (!isMeasure) return true; @@ -329,17 +340,26 @@ {#each virtualRows as row (row.index)} {@const cells = rows[row.index].getVisibleCells()} - + {@const rowId = rows[row.index].id} + {@const isSelected = rowSelectionState?.isRowSelected(rowId) ?? false} + {@const hasSelection = rowSelectionState?.hasActiveSelection ?? false} + {#each cells as cell, i (cell.id)} {@const result = typeof cell.column.columnDef.cell === "function" ? cell.column.columnDef.cell(cell.getContext()) : cell.column.columnDef.cell} {@const isActive = isCellActive(cell)} + {@const isClicked = isCellClicked(cell)} td:first-of-type { + @apply bg-primary-50; + } + .selected-row:hover > td:first-of-type { + @apply bg-primary-100; + } + + .dimmed-row .cell { + @apply opacity-50; + } + /* Show more row styling */ .show-more-row, .show-more-row .cell { diff --git a/web-common/src/features/dashboards/pivot/PivotExpandableCell.svelte b/web-common/src/features/dashboards/pivot/PivotExpandableCell.svelte index 019c247ea11..d477d3b12c3 100644 --- a/web-common/src/features/dashboards/pivot/PivotExpandableCell.svelte +++ b/web-common/src/features/dashboards/pivot/PivotExpandableCell.svelte @@ -15,18 +15,31 @@ $: assembledAndCanExpand = assembled && canExpand; $: needsSpacer = row.depth >= 1 || (hasNestedDimensions && !canExpand); + + function handleExpandClick(e: MouseEvent) { + e.stopPropagation(); + if (assembledAndCanExpand) { + row.getToggleExpandedHandler()(); + } + }