diff --git a/ui/src/pages/Pivot.jsx b/ui/src/pages/Pivot.jsx index ffece6d..c69eb6c 100644 --- a/ui/src/pages/Pivot.jsx +++ b/ui/src/pages/Pivot.jsx @@ -47,29 +47,35 @@ function normalize(v) { return String(v).trim() } -function filterRows(allRows, row, config) { - const groupBy = config.group_by || [] - - if (groupBy.length === 0) { - // Flat view — clicked row IS the record - return [row] - } - - // __ROW_PATH__ is an array of the group_by values in order, e.g. ["Groceries", "Wal-Mart"] - const rowPath = row['__ROW_PATH__'] - const pathVals = Array.isArray(rowPath) - ? rowPath - : String(rowPath).split(',').map(s => s.trim()) - - // Zip group_by columns with __ROW_PATH__ values to build filter criteria - const criteria = groupBy - .map((col, i) => ({ col, val: pathVals[i] != null ? String(pathVals[i]).trim() : null })) - .filter(({ val }) => val != null && val !== '') - - if (criteria.length === 0) return allRows - - return allRows.filter(r => - criteria.every(({ col, val }) => normalize(r[col]) === val) +// Apply perspective-click event filters directly to raw rows. +// Each filter is [field, operator, value] — same format Perspective uses internally. +// Filters for fields that don't exist in the raw data (e.g. Perspective computed columns) +// are skipped — they can't be matched against the source rows. +function filterRowsByConfig(allRows, filters) { + if (!filters || filters.length === 0) return allRows + const knownFields = allRows.length > 0 ? new Set(Object.keys(allRows[0])) : new Set() + const applicable = filters.filter(([field]) => knownFields.has(field)) + if (applicable.length === 0) return allRows + return allRows.filter(row => + applicable.every(([field, op, value]) => { + const rawVal = row[field] + if (rawVal == null) return op === '!=' || op === 'not contains' + const a = normalize(rawVal) + const b = value != null ? String(value).trim() : '' + const aNum = parseFloat(a), bNum = parseFloat(b) + const numeric = !isNaN(aNum) && !isNaN(bNum) + switch (op) { + case '==': return a === b + case '!=': return a !== b + case '>': return numeric ? aNum > bNum : a > b + case '>=': return numeric ? aNum >= bNum : a >= b + case '<': return numeric ? aNum < bNum : a < b + case '<=': return numeric ? aNum <= bNum : a <= b + case 'contains': return a.toLowerCase().includes(b.toLowerCase()) + case 'not contains': return !a.toLowerCase().includes(b.toLowerCase()) + default: return true + } + }) ) } @@ -122,17 +128,12 @@ export default function Pivot({ source }) { const detail = e.detail || {} const { row, column_names } = detail if (!row) return - // perspective-click's config only has filter — save() gives us the full config incl. group_by + // detail.config has the cell-specific filters (group_by + split_by values + user filters) + // viewer.save() gives us the full config including group_by/split_by field names + const eventFilters = (detail.config || {}).filter || [] const config = await viewer.save() - console.log('full detail keys:', Object.keys(detail)) - console.log('detail.config (raw event):', JSON.stringify(detail.config)) - console.log('column_names:', JSON.stringify(column_names)) - console.log('config.group_by:', JSON.stringify(config.group_by)) - console.log('config.split_by:', JSON.stringify(config.split_by)) - console.log('config.plugin_config:', JSON.stringify(config.plugin_config)) - console.log('row:', JSON.stringify(row)) - setClickDetail({ row, config, column_names }) - const matched = filterRows(allRowsRef.current, row, config) + setClickDetail({ row, config, column_names, eventFilters }) + const matched = filterRowsByConfig(allRowsRef.current, eventFilters) setInspectedRows(matched) }) @@ -173,6 +174,27 @@ export default function Pivot({ source }) { const cols = inspectedRows?.length ? Object.keys(inspectedRows[0]) : [] + // Extract cell coordinates from event filters for display. + // group_by and split_by values appear as "==" filters in the event config. + const groupBy = clickDetail?.config?.group_by || [] + const splitBy = clickDetail?.config?.split_by || [] + const coordFields = new Set([...groupBy, ...splitBy]) + const coordMap = Object.fromEntries( + (clickDetail?.eventFilters || []) + .filter(([f, op]) => coordFields.has(f) && op === '==') + .map(([f, , v]) => [f, v]) + ) + const cellCoords = [...groupBy, ...splitBy].map(f => coordMap[f]).filter(Boolean) + + // Build the specific clicked cell's column key (e.g. "2025-01-01|08 August|Amount") + // so we can show just that value rather than all non-null metric columns. + const splitVals = splitBy.map(f => coordMap[f]).filter(Boolean) + const metrics = clickDetail?.column_names || [] + const cellKey = splitVals.length > 0 && metrics.length > 0 + ? [...splitVals, ...metrics].join('|') + : null + const cellValue = cellKey != null ? clickDetail?.row?.[cellKey] : null + return (