Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 12 additions & 5 deletions src/layers/src/base-layer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1299,11 +1299,12 @@
this.meta = {...this.meta, ...meta};
}

getDataUpdateTriggers({filteredIndex, id, dataContainer}: KeplerTable): any {
getDataUpdateTriggers({filteredIndex, filteredIndexByLayer, id, dataContainer}: KeplerTable): any {
const {columns} = this.config;
const layerFilteredIndex = filteredIndexByLayer?.[this.id] ?? filteredIndex;

return {
getData: {datasetId: id, dataContainer, columns, filteredIndex},
getData: {datasetId: id, dataContainer, columns, filteredIndex: layerFilteredIndex},
getMeta: {datasetId: id, dataContainer, columns},
...(this.config.textLabel || []).reduce(
(accu, tl, i) => ({
Expand All @@ -1322,12 +1323,18 @@
const layerDataset = datasets[this.config.dataId];
const {dataContainer} = layerDataset;

const getPosition = this.getPositionAccessor(dataContainer, layerDataset);
// Use per-layer polygon-filtered index if available
const effectiveDataset =
layerDataset.filteredIndexByLayer?.[this.id] != null
? {...layerDataset, filteredIndex: layerDataset.filteredIndexByLayer[this.id]}
: layerDataset;

const getPosition = this.getPositionAccessor(dataContainer, effectiveDataset);

Check failure on line 1332 in src/layers/src/base-layer.ts

View workflow job for this annotation

GitHub Actions / build

Argument of type 'KeplerTable<Field> | { filteredIndex: number[]; id: string; type?: string | undefined; label: string; color: RGBColor; fields: Field[]; dataContainer: DataContainerInterface; ... 15 more ...; getFileProcessor?: ((data: any, inputFormat?: string | undefined) => { ...; }) | undefined; }' is not assignable to parameter of type 'KeplerTable<Field> | undefined'.
const dataUpdateTriggers = this.getDataUpdateTriggers(layerDataset);
const triggerChanged = this.getChangedTriggers(dataUpdateTriggers);

if (triggerChanged && (triggerChanged.getMeta || triggerChanged.getData)) {
this.updateLayerMeta(layerDataset, getPosition);
this.updateLayerMeta(effectiveDataset, getPosition);

Check failure on line 1337 in src/layers/src/base-layer.ts

View workflow job for this annotation

GitHub Actions / build

Argument of type 'KeplerTable<Field> | { filteredIndex: number[]; id: string; type?: string | undefined; label: string; color: RGBColor; fields: Field[]; dataContainer: DataContainerInterface; ... 15 more ...; getFileProcessor?: ((data: any, inputFormat?: string | undefined) => { ...; }) | undefined; }' is not assignable to parameter of type 'KeplerTable<Field>'.

// reset filteredItemCount
this.filteredItemCount = {};
Expand All @@ -1339,7 +1346,7 @@
// same data
data = oldLayerData.data;
} else {
data = this.calculateDataAttribute(layerDataset, getPosition);
data = this.calculateDataAttribute(effectiveDataset, getPosition);

Check failure on line 1349 in src/layers/src/base-layer.ts

View workflow job for this annotation

GitHub Actions / build

Argument of type 'KeplerTable<Field> | { filteredIndex: number[]; id: string; type?: string | undefined; label: string; color: RGBColor; fields: Field[]; dataContainer: DataContainerInterface; ... 15 more ...; getFileProcessor?: ((data: any, inputFormat?: string | undefined) => { ...; }) | undefined; }' is not assignable to parameter of type 'KeplerTable<Field>'.
}

return {data, triggerChanged};
Expand Down
5 changes: 3 additions & 2 deletions src/layers/src/mapboxgl-layer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,9 @@ class MapboxLayerGL extends Layer {
return Array.isArray(filter) && filter.length;
}

getDataUpdateTriggers({filteredIndex, gpuFilter, id}: KeplerTable): any {
getDataUpdateTriggers({filteredIndex, filteredIndexByLayer, gpuFilter, id}: KeplerTable): any {
const {columns} = this.config;
const layerFilteredIndex = filteredIndexByLayer?.[this.id] ?? filteredIndex;

const visualChannelFields = Object.values(this.visualChannels).reduce(
(accu, v) => ({
Expand All @@ -97,7 +98,7 @@ class MapboxLayerGL extends Layer {
getData: {
datasetId: id,
columns,
filteredIndex,
filteredIndex: layerFilteredIndex,
...visualChannelFields,
...gpuFilter.filterValueUpdateTriggers
},
Expand Down
77 changes: 76 additions & 1 deletion src/table/src/kepler-table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import {
SORT_ORDER,
ALL_FIELD_TYPES,
ALTITUDE_FIELDS,
SCALE_TYPES
SCALE_TYPES,
FILTER_TYPES
} from '@kepler.gl/constants';
import {
RGBColor,
Expand Down Expand Up @@ -39,6 +40,8 @@ import {
getFilterFunction,
getFilterProps,
getFilterRecord,
getPolygonFilterFunctor,
isValidFilterValue,
getNumericFieldDomain,
getTimestampFieldDomain,
getLinearDomain,
Expand Down Expand Up @@ -118,6 +121,67 @@ export function maybeToDate(
return Array.isArray(d) ? d[fieldIdx] : dc.valueAt(d.index, fieldIdx);
}

/**
* Compute per-layer filtered indices for polygon filters.
* Polygon filters are layer-specific: they should only affect the layers listed in filter.layerId.
* For each layer on this dataset, compute a filtered index that applies only the polygon filters
* targeting that specific layer (using OR logic when multiple layers on the same dataset are selected).
*/
function computePolygonFilteredIndexByLayer(
filters: Filter[],
layers: Layer[],
dataId: string,
dataContainer: DataContainerInterface,
baseFilteredIndex: number[]
): Record<string, number[]> {
const polygonFilters = filters.filter(
f =>
f.type === FILTER_TYPES.polygon &&
f.dataId.includes(dataId) &&
f.enabled !== false &&
isValidFilterValue(f.type, f.value)
);

if (!polygonFilters.length) {
return {};
}

const layersOnDataset = layers.filter(l => l.config?.dataId === dataId);
const result: Record<string, number[]> = {};

for (const layer of layersOnDataset) {
// For each polygon filter, check if this layer is targeted
const applicableFilters = polygonFilters.filter(
f => f.layerId && f.layerId.includes(layer.id)
);

if (!applicableFilters.length) {
// This layer is not targeted by any polygon filter - use base index
continue;
}

// Build polygon filter functors for this layer
const filterFunctors = applicableFilters.map(filter =>
getPolygonFilterFunctor(layer, filter, dataContainer)
);

// Filter the base filtered index: a row passes if it passes ALL polygon filters
// (each polygon filter already uses this layer's position accessor)
const layerFilteredIndex: number[] = [];
const filterContext = {index: -1};
for (let i = 0; i < baseFilteredIndex.length; i++) {
filterContext.index = baseFilteredIndex[i];
if (filterFunctors.every(fn => fn(filterContext))) {
layerFilteredIndex.push(baseFilteredIndex[i]);
}
}

result[layer.id] = layerFilteredIndex;
}

return result;
}

class KeplerTable<F extends Field = Field> {
readonly id: string;

Expand All @@ -134,6 +198,7 @@ class KeplerTable<F extends Field = Field> {
filteredIndex: number[] = [];
filteredIdxCPU?: number[];
filteredIndexForDomain: number[] = [];
filteredIndexByLayer: Record<string, number[]> = {};
fieldPairs: FieldPair[] = [];
gpuFilter: GpuFilter;
filterRecord?: FilterRecord;
Expand Down Expand Up @@ -359,6 +424,7 @@ class KeplerTable<F extends Field = Field> {
if (!filters.length) {
this.filteredIndex = this.allIndexes;
this.filteredIndexForDomain = this.allIndexes;
this.filteredIndexByLayer = {};
return this;
}

Expand Down Expand Up @@ -393,6 +459,15 @@ class KeplerTable<F extends Field = Field> {
this.filteredIndexForDomain =
filterResult.filteredIndexForDomain || this.filteredIndexForDomain;

// Compute per-layer filtered indices for polygon filters
this.filteredIndexByLayer = computePolygonFilteredIndexByLayer(
filters,
layers,
dataId,
dataContainer,
this.filteredIndex
);

return this;
}

Expand Down
6 changes: 6 additions & 0 deletions src/utils/src/filter-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -586,6 +586,12 @@ export function getFilterRecord(

filters.forEach(f => {
if (isValidFilterValue(f.type, f.value) && toArray(f.dataId).includes(dataId)) {
// Polygon filters are layer-specific and handled per-layer, not at the dataset level
if (f.type === FILTER_TYPES.polygon) {
filterRecord.fixedDomain.push(f);
return;
}

(f.fixedDomain || opt.ignoreDomain
? filterRecord.fixedDomain
: filterRecord.dynamicDomain
Expand Down
24 changes: 23 additions & 1 deletion test/helpers/comparison-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,20 @@ export function cmpColumns(t, expectedColumns, actualColumns, layerName) {

export function cmpDataset(t, expectedDataset, actualDataset, opt = {}) {
assertDatasetIsTable(t, actualDataset);
cmpObjectKeys(t, expectedDataset, actualDataset, `dataset:${expectedDataset.id}`);
// filteredIndexByLayer is an internal optimization property, skip in key comparison if not in expected
const actualKeysForComparison = Object.keys(actualDataset).filter(
key => key !== 'filteredIndexByLayer' || key in expectedDataset
);
const expectedKeysForComparison = Object.keys(expectedDataset);
t.deepEqual(
actualKeysForComparison
.filter(key => actualDataset[key] !== undefined)
.sort(),
expectedKeysForComparison
.filter(key => expectedDataset[key] !== undefined)
.sort(),
`dataset:${expectedDataset.id} should have same keys`
);

// test everything except auto generated color
Object.keys(actualDataset)
Expand Down Expand Up @@ -365,6 +378,15 @@ export function cmpDataset(t, expectedDataset, actualDataset, opt = {}) {
);
});
break;
case 'filteredIndexByLayer':
if (key in expectedDataset) {
t.deepEqual(
actualDataset[key],
expectedDataset[key],
`dataset.${expectedDataset.id}.${key} should be correct`
);
}
break;
default:
if (key !== 'color' || opt.color) {
t.deepEqual(
Expand Down
26 changes: 15 additions & 11 deletions test/node/reducers/vis-state-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5026,7 +5026,7 @@ test('#visStateReducer -> POLYGON: Create polygon filter', t => {

t.equal(newReducer.layerData[0].data.length, 2, 'Layer Point 1 should only show 2 points');

t.equal(newReducer.layerData[1].data.length, 2, 'Layer Point 2 should only show 2 points');
t.equal(newReducer.layerData[1].data.length, 4, 'Layer Point 2 should show all 4 points (not targeted by filter)');

const filterFeature = newReducer.filters[0].value;

Expand All @@ -5040,9 +5040,9 @@ test('#visStateReducer -> POLYGON: Create polygon filter', t => {

t.equal(newReducer.filters[0].layerId.length, 2, 'Should have two values in filter.layerId');

t.equal(newReducer.layerData[0].data.length, 0, 'Layer Point 1 should show 0 points');
t.equal(newReducer.layerData[0].data.length, 2, 'Layer Point 1 should show 2 points (filtered by its own position)');

t.equal(newReducer.layerData[1].data.length, 0, 'Layer Point 2 show show 0 points');
t.equal(newReducer.layerData[1].data.length, 0, 'Layer Point 2 should show 0 points (end positions are outside polygon)');

// Adding a new dataset - creates extra 4 layers
newReducer = applyActions(reducer, newReducer, [
Expand Down Expand Up @@ -5079,15 +5079,15 @@ test('#visStateReducer -> POLYGON: Create polygon filter', t => {
t.equal(
newReducer.layerData[0].data.length,
2,
'Layer Point 1 show 2 points because we removed layer 2'
'Layer Point 1 show 2 points because it is still filtered'
);

t.equal(newReducer.layerData[4].data.length, 2, 'Layer Point 5 should 2 points because filtered');

t.equal(
newReducer.layerData[2].data.length,
2,
'Layer Point 2 should still show 2 filters because layer 1 is still filtered'
newReducer.layerData[1].data.length,
4,
'Layer Point 2 should show full data because it was removed from filter'
);

t.end();
Expand Down Expand Up @@ -5250,8 +5250,12 @@ test('#visStateReducer -> POLYGON: Toggle filter feature', t => {
);
t.deepEqual(
newReducer.datasets.puppy.filteredIndex,
[0, 2],
'The polygon filter should be applied'
[0, 1, 2, 3],
'The dataset filteredIndex should not be affected by polygon filters'
);
t.ok(
newReducer.datasets.puppy.filteredIndexByLayer[newReducer.layers[0].id],
'Should have per-layer polygon filtered index'
);

newReducer = reducer(newReducer, VisStateActions.toggleFilterFeature(0));
Expand Down Expand Up @@ -5412,9 +5416,9 @@ test('#visStateReducer -> POLYGON: setPolygonFilterLayer: H3', t => {

const expectedFilteredIndex = [1, 3, 5, 8];
t.deepEqual(
newState.datasets['190vdll3di'].filteredIndex,
newState.datasets['190vdll3di'].filteredIndexByLayer[newState.layers[0].id],
expectedFilteredIndex,
'should filter data based on h3 layer'
'should have per-layer polygon filtered index for h3 layer'
);
t.deepEqual(
newState.layerData[0].data.map(d => d.index),
Expand Down
Loading