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 <noreply@anthropic.com>
This commit is contained in:
Paul Trowbridge 2026-04-14 22:51:48 -04:00
parent 1587d06967
commit fb9ff8720a

View File

@ -47,29 +47,35 @@ function normalize(v) {
return String(v).trim() return String(v).trim()
} }
function filterRows(allRows, row, config) { // Apply perspective-click event filters directly to raw rows.
const groupBy = config.group_by || [] // 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)
if (groupBy.length === 0) { // are skipped they can't be matched against the source rows.
// Flat view clicked row IS the record function filterRowsByConfig(allRows, filters) {
return [row] 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))
// __ROW_PATH__ is an array of the group_by values in order, e.g. ["Groceries", "Wal-Mart"] if (applicable.length === 0) return allRows
const rowPath = row['__ROW_PATH__'] return allRows.filter(row =>
const pathVals = Array.isArray(rowPath) applicable.every(([field, op, value]) => {
? rowPath const rawVal = row[field]
: String(rowPath).split(',').map(s => s.trim()) if (rawVal == null) return op === '!=' || op === 'not contains'
const a = normalize(rawVal)
// Zip group_by columns with __ROW_PATH__ values to build filter criteria const b = value != null ? String(value).trim() : ''
const criteria = groupBy const aNum = parseFloat(a), bNum = parseFloat(b)
.map((col, i) => ({ col, val: pathVals[i] != null ? String(pathVals[i]).trim() : null })) const numeric = !isNaN(aNum) && !isNaN(bNum)
.filter(({ val }) => val != null && val !== '') switch (op) {
case '==': return a === b
if (criteria.length === 0) return allRows case '!=': return a !== b
case '>': return numeric ? aNum > bNum : a > b
return allRows.filter(r => case '>=': return numeric ? aNum >= bNum : a >= b
criteria.every(({ col, val }) => normalize(r[col]) === val) 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 detail = e.detail || {}
const { row, column_names } = detail const { row, column_names } = detail
if (!row) return 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() const config = await viewer.save()
console.log('full detail keys:', Object.keys(detail)) setClickDetail({ row, config, column_names, eventFilters })
console.log('detail.config (raw event):', JSON.stringify(detail.config)) const matched = filterRowsByConfig(allRowsRef.current, eventFilters)
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)
setInspectedRows(matched) setInspectedRows(matched)
}) })
@ -173,6 +174,27 @@ export default function Pivot({ source }) {
const cols = inspectedRows?.length ? Object.keys(inspectedRows[0]) : [] 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 ( return (
<div className="relative w-full h-full flex"> <div className="relative w-full h-full flex">
<div className="relative flex-1"> <div className="relative flex-1">
@ -222,45 +244,48 @@ export default function Pivot({ source }) {
</div> </div>
<div className="flex-1 overflow-y-auto"> <div className="flex-1 overflow-y-auto">
{/* Click context — row values from Perspective */} {/* Cell coordinates */}
<div className="px-3 py-2 border-b border-gray-100"> <div className="px-3 py-2 border-b border-gray-100">
<div className="text-xs text-gray-400 uppercase tracking-wide mb-1"> <div className="text-xs text-gray-400 uppercase tracking-wide mb-1">
{[...(clickDetail.config?.group_by || []), ...(clickDetail.config?.split_by || [])].join(' ') || clickDetail.column_names?.join(', ') || 'Cell'} {[...groupBy, ...splitBy].join(' ') || clickDetail.column_names?.join(', ') || 'Cell'}
</div> </div>
{/* Row path */} {cellCoords.length > 0 && (
{clickDetail.row['__ROW_PATH__'] && ( <div className="text-xs text-gray-700 font-mono font-semibold">
<div className="text-xs text-gray-700 font-mono font-semibold mb-1"> {cellCoords.join(' ')}
{Array.isArray(clickDetail.row['__ROW_PATH__'])
? clickDetail.row['__ROW_PATH__'].join(' ')
: String(clickDetail.row['__ROW_PATH__'])}
</div> </div>
)} )}
{/* Non-null metric values only */} {/* Specific cell value if we can identify it, otherwise all non-null metrics */}
{Object.entries(clickDetail.row) {cellKey != null ? (
.filter(([k, v]) => k !== '__ROW_PATH__' && v != null) cellValue != null && (
.map(([k, v]) => ( <div className="flex justify-between py-0.5 gap-2">
<div key={k} className="flex justify-between py-0.5 gap-2"> <span className="text-xs text-gray-400 font-mono shrink-0">{metrics.join(', ')}</span>
<span className="text-xs text-gray-400 font-mono shrink-0">{k}</span> <span className="text-xs text-gray-700 font-mono text-right">{formatVal(cellValue)}</span>
<span className="text-xs text-gray-700 font-mono text-right">{formatVal(v)}</span>
</div> </div>
))} )
) : (
Object.entries(clickDetail.row)
.filter(([k, v]) => k !== '__ROW_PATH__' && v != null)
.map(([k, v]) => (
<div key={k} className="flex justify-between py-0.5 gap-2">
<span className="text-xs text-gray-400 font-mono shrink-0">{k}</span>
<span className="text-xs text-gray-700 font-mono text-right">{formatVal(v)}</span>
</div>
))
)}
</div> </div>
{/* Config context — group_by / split_by / filters if any */} {/* User-set filters (exclude cell-coordinate filters) */}
{(clickDetail.config?.group_by?.length > 0 || clickDetail.config?.split_by?.length > 0 || clickDetail.config?.filter?.length > 0) && ( {(() => {
<div className="px-3 py-2 border-b border-gray-100"> const userFilters = (clickDetail.eventFilters || []).filter(([f]) => !coordFields.has(f))
<div className="text-xs text-gray-400 uppercase tracking-wide mb-1">Pivot context</div> return userFilters.length > 0 ? (
{clickDetail.config.group_by?.length > 0 && ( <div className="px-3 py-2 border-b border-gray-100">
<div className="text-xs text-gray-500 py-0.5">group by: <span className="font-mono text-gray-700">{clickDetail.config.group_by.join(', ')}</span></div> <div className="text-xs text-gray-400 uppercase tracking-wide mb-1">Filters</div>
)} {userFilters.map((f, i) => (
{clickDetail.config.split_by?.length > 0 && ( <div key={i} className="text-xs text-gray-500 py-0.5 font-mono">{f.join(' ')}</div>
<div className="text-xs text-gray-500 py-0.5">split by: <span className="font-mono text-gray-700">{clickDetail.config.split_by.join(', ')}</span></div> ))}
)} </div>
{clickDetail.config.filter?.map((f, i) => ( ) : null
<div key={i} className="text-xs text-gray-500 py-0.5 font-mono">{f.join(' ')}</div> })()}
))}
</div>
)}
{/* Underlying rows */} {/* Underlying rows */}
{inspectedRows.length > 0 && ( {inspectedRows.length > 0 && (