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:
parent
1587d06967
commit
fb9ff8720a
@ -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))
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
})
|
||||||
// __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)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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 ? (
|
||||||
|
cellValue != null && (
|
||||||
|
<div 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-700 font-mono text-right">{formatVal(cellValue)}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
Object.entries(clickDetail.row)
|
||||||
.filter(([k, v]) => k !== '__ROW_PATH__' && v != null)
|
.filter(([k, v]) => k !== '__ROW_PATH__' && v != null)
|
||||||
.map(([k, v]) => (
|
.map(([k, v]) => (
|
||||||
<div key={k} 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">{k}</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(v)}</span>
|
<span className="text-xs text-gray-700 font-mono text-right">{formatVal(v)}</span>
|
||||||
</div>
|
</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) && (
|
{(() => {
|
||||||
|
const userFilters = (clickDetail.eventFilters || []).filter(([f]) => !coordFields.has(f))
|
||||||
|
return userFilters.length > 0 ? (
|
||||||
<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">Pivot context</div>
|
<div className="text-xs text-gray-400 uppercase tracking-wide mb-1">Filters</div>
|
||||||
{clickDetail.config.group_by?.length > 0 && (
|
{userFilters.map((f, i) => (
|
||||||
<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>
|
|
||||||
)}
|
|
||||||
{clickDetail.config.split_by?.length > 0 && (
|
|
||||||
<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>
|
|
||||||
)}
|
|
||||||
{clickDetail.config.filter?.map((f, i) => (
|
|
||||||
<div key={i} className="text-xs text-gray-500 py-0.5 font-mono">{f.join(' ')}</div>
|
<div key={i} className="text-xs text-gray-500 py-0.5 font-mono">{f.join(' ')}</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
) : null
|
||||||
|
})()}
|
||||||
|
|
||||||
{/* Underlying rows */}
|
{/* Underlying rows */}
|
||||||
{inspectedRows.length > 0 && (
|
{inspectedRows.length > 0 && (
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user