From fb9ff8720af0caac7531b7a204152b34e253fb44 Mon Sep 17 00:00:00 2001 From: Paul Trowbridge Date: Tue, 14 Apr 2026 22:51:48 -0400 Subject: [PATCH] Pivot: use event filters for row matching, skip computed columns Replace __ROW_PATH__ zip approach with direct application of perspective-click event filters against raw rows. Fields not present in the raw data (Perspective computed columns like Month, YearDate) are skipped. Also removes debug console.log calls. Co-Authored-By: Claude Sonnet 4.6 --- ui/src/pages/Pivot.jsx | 153 ++++++++++++++++++++++++----------------- 1 file changed, 89 insertions(+), 64 deletions(-) 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 (
@@ -222,45 +244,48 @@ export default function Pivot({ source }) {
- {/* Click context — row values from Perspective */} + {/* Cell coordinates */}
- {[...(clickDetail.config?.group_by || []), ...(clickDetail.config?.split_by || [])].join(' › ') || clickDetail.column_names?.join(', ') || 'Cell'} + {[...groupBy, ...splitBy].join(' › ') || clickDetail.column_names?.join(', ') || 'Cell'}
- {/* Row path */} - {clickDetail.row['__ROW_PATH__'] && ( -
- {Array.isArray(clickDetail.row['__ROW_PATH__']) - ? clickDetail.row['__ROW_PATH__'].join(' › ') - : String(clickDetail.row['__ROW_PATH__'])} + {cellCoords.length > 0 && ( +
+ {cellCoords.join(' › ')}
)} - {/* Non-null metric values only */} - {Object.entries(clickDetail.row) - .filter(([k, v]) => k !== '__ROW_PATH__' && v != null) - .map(([k, v]) => ( -
- {k} - {formatVal(v)} + {/* Specific cell value if we can identify it, otherwise all non-null metrics */} + {cellKey != null ? ( + cellValue != null && ( +
+ {metrics.join(', ')} + {formatVal(cellValue)}
- ))} + ) + ) : ( + Object.entries(clickDetail.row) + .filter(([k, v]) => k !== '__ROW_PATH__' && v != null) + .map(([k, v]) => ( +
+ {k} + {formatVal(v)} +
+ )) + )}
- {/* Config context — group_by / split_by / filters if any */} - {(clickDetail.config?.group_by?.length > 0 || clickDetail.config?.split_by?.length > 0 || clickDetail.config?.filter?.length > 0) && ( -
-
Pivot context
- {clickDetail.config.group_by?.length > 0 && ( -
group by: {clickDetail.config.group_by.join(', ')}
- )} - {clickDetail.config.split_by?.length > 0 && ( -
split by: {clickDetail.config.split_by.join(', ')}
- )} - {clickDetail.config.filter?.map((f, i) => ( -
{f.join(' ')}
- ))} -
- )} + {/* User-set filters (exclude cell-coordinate filters) */} + {(() => { + const userFilters = (clickDetail.eventFilters || []).filter(([f]) => !coordFields.has(f)) + return userFilters.length > 0 ? ( +
+
Filters
+ {userFilters.map((f, i) => ( +
{f.join(' ')}
+ ))} +
+ ) : null + })()} {/* Underlying rows */} {inspectedRows.length > 0 && (