Skip to content

Commit

Permalink
[fix] Fixed synced filter domain and interval calculation (#2725)
Browse files Browse the repository at this point in the history
- Fixed synced filter domain and interval calculation

Signed-off-by: Ihor Dykhta <[email protected]>
  • Loading branch information
igorDykhta authored Nov 1, 2024
1 parent 695861b commit 8ea1cab
Show file tree
Hide file tree
Showing 7 changed files with 508 additions and 69 deletions.
2 changes: 1 addition & 1 deletion src/components/src/common/range-slider-timeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const TIMELINE_MARKER_STYLE: CSSProperties = {
const containerStyle = {
display: 'flex',
width: '100%',
height: '16px'
height: '24px'
};

const iconWrapperStyle = {
Expand Down
199 changes: 144 additions & 55 deletions src/reducers/src/vis-state-updaters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,13 @@ import {mergeDatasetsByOrder} from './vis-state-merger';
import {
fixEffectOrder,
getAnimatableVisibleLayers,
getAnimatableVisibleLayersByType,
mergeTimeDomains
getIntervalBasedAnimationLayers,
mergeTimeDomains,
adjustValueToAnimationWindow,
updateTimeFilterPlotType,
getDefaultTimeFormat,
LayerToFilterTimeInterval,
TIME_INTERVALS_ORDERED
} from '@kepler.gl/utils';
import {createEffect} from '@kepler.gl/effects';

Expand Down Expand Up @@ -968,10 +973,10 @@ export function setFilterAnimationTimeUpdater(
* @memberof visStateUpdaters
* @public
*/
export function setFilterAnimationWindowUpdater(
state: VisState,
export function setFilterAnimationWindowUpdater<S extends VisState>(
state: S,
{id, animationWindow}: VisStateActions.SetFilterAnimationWindowUpdaterAction
): VisState {
): S {
const filter = state.filters.find(f => f.id === id);

if (!filter) {
Expand All @@ -998,10 +1003,10 @@ export function setFilterAnimationWindowUpdater(
* @memberof visStateUpdaters
* @public
*/
export function setFilterUpdater(
state: VisState,
export function setFilterUpdater<S extends VisState>(
state: S,
action: VisStateActions.SetFilterUpdaterAction
): VisState {
): S {
const {idx, valueIndex = 0} = action;
const oldFilter = state.filters[idx];
if (!oldFilter) {
Expand Down Expand Up @@ -1061,10 +1066,6 @@ export function setFilterUpdater(
// pass only the dataset we need to update
newState = updateAllLayerDomainData(newState, datasetIdsToFilter, newFilter);

// if (newFilter.syncedWithLayerTimeline) {
// newState = syncTimeFilterWithLayerTimelineUpdater(state, {idx, enable: newFilter.syncedWithLayerTimeline});
// }

return newState;
}

Expand Down Expand Up @@ -2697,11 +2698,11 @@ export function updateFileLoadingProgressUpdater(state, {fileName, progress}) {
/**
* Helper function to update layer domains for an array of datasets
*/
export function updateAllLayerDomainData(
state: VisState,
export function updateAllLayerDomainData<S extends VisState>(
state: S,
dataId: string | string[],
updatedFilter?: Filter
): VisState {
): S {
const dataIds = typeof dataId === 'string' ? [dataId] : dataId;
const newLayers: Layer[] = [];
const newLayerData: any[] = [];
Expand Down Expand Up @@ -3185,62 +3186,126 @@ export function layerFilteredItemsChangeUpdater<S extends VisState>(
};
}

// eslint-disable-next-line max-statements
export function syncTimeFilterWithLayerTimelineUpdater<S extends VisState>(
state: S,
action: VisStateActions.SyncTimeFilterWithLayerTimelineAction
) {
const {idx: filterIdx, enable = false} = action;

const filter = state.filters[filterIdx] as TimeRangeFilter;
const hasHexTileLayer =
enable && Boolean(getAnimatableVisibleLayersByType(state.layers, 'hexTile').length);

const newFilterDomain =
hasHexTileLayer && state.animationConfig.domain
? mergeTimeDomains([filter.domain, state.animationConfig.domain as [number, number]])
: filter.domain;
let newState = state;
let newFilter = filter;

// if we enable sync we are going to merge filter and animationConfig domains and store into filter.domain
if (enable) {
const animatableLayers = getAnimatableVisibleLayers(newState.layers);
// if no animatableLayers are present we simply return
if (!animatableLayers.length) {
return newState;
}

const syncTimelineMode = getSyncAnimationMode(filter);
const intervalBasedAnimationLayers = getIntervalBasedAnimationLayers(animatableLayers);
const hasIntervalBasedAnimationLayer = Boolean(intervalBasedAnimationLayers.length);

let newState = setFilterAnimationWindowUpdater(state, {
id: filter.id,
animationWindow: hasHexTileLayer ? ANIMATION_WINDOW.interval : filter.animationWindow
});
const newFilterDomain = mergeTimeDomains([filter.domain, newState.animationConfig.domain]);

let newFilter = newState.filters[filterIdx];
// we only update animationWindow if we have interval based animation layers with defined intervals and the current filter animation window is not interval
if (hasIntervalBasedAnimationLayer) {
if (filter.animationWindow !== ANIMATION_WINDOW.interval) {
newState = setFilterAnimationWindowUpdater(newState, {
id: filter.id,
animationWindow: ANIMATION_WINDOW.interval
});
}

const newFilterValue = adjustValueToFilterDomain(
hasHexTileLayer ? [newFilterDomain[0], newFilterDomain[0]] : newFilter.value,
{
domain: newFilterDomain,
// check whether this is valid type here - required to pass ts checks.
type: newFilter.type
newFilter = newState.filters[filterIdx] as TimeRangeFilter;

// adjust time filter interval
newFilter = adjustTimeFilterInterval(newState, newFilter);

// replace filter in state with newFilter
newState = {
...newState,
filters: swap_<Filter>(newFilter)(newState.filters)
};
}
);

newFilter = newState.filters[filterIdx] as TimeRangeFilter;

// adjust value based on new domain
const newFilterValue = adjustValueToFilterDomain(
newFilter.animationWindow === ANIMATION_WINDOW.interval
? [newFilterDomain[0], newFilterDomain[0]]
: newFilterDomain,
{...newFilter, domain: newFilterDomain}
);

newState = setFilterUpdater(newState, {
idx: filterIdx,
prop: 'value',
value: newFilterValue
});

newFilter = {
...(newState.filters[filterIdx] as TimeRangeFilter),
syncedWithLayerTimeline: true
};

// replace filter in state with newFilter
newState = {
...newState,
filters: swap_<Filter>(newFilter)(newState.filters)
};

newState = setTimeFilterTimelineModeUpdater(newState, {
id: newFilter.id,
mode: getSyncAnimationMode(newFilter)
});

// set the animation config value to match filter value
return setLayerAnimationTimeUpdater(newState, {value: newState.filters[filterIdx].value[0]});
}

// set domain and step
newFilter = {
...filter,
syncedWithLayerTimeline: false
};

// replace filter in state with newFilter
newState = {
...newState,
filters: swap_<Filter>(newFilter)(newState.filters)
};

// reset sync timeline mode
newState = setTimeFilterTimelineModeUpdater(newState, {
id: newFilter.id,
mode: SYNC_TIMELINE_MODES.end
});

newFilter = newState.filters[filterIdx] as TimeRangeFilter;

// reset filter value
const newFilterValue = adjustValueToFilterDomain(newFilter.domain, newFilter);

newState = setFilterUpdater(newState, {
idx: filterIdx,
prop: 'value',
value: newFilterValue
});

newFilter = {
...newState.filters[filterIdx],
syncedWithLayerTimeline: enable,
syncTimelineMode
} as TimeRangeFilter;

const animationConfigCurrentTime = enable
? newFilterDomain[0]
: newState.animationConfig.domain?.[0] ?? 0;
newState = setTimeFilterTimelineModeUpdater(newState, {
id: newFilter.id,
mode: getSyncAnimationMode(newFilter)
});

return setLayerAnimationTimeUpdater(
{
...newState,
filters: swap_<Filter>(newFilter)(newState.filters)
},
{value: animationConfigCurrentTime ?? null}
);
// reset animation config current time to
return setLayerAnimationTimeUpdater(newState, {
value: newState.animationConfig.domain?.[0] ?? null
});
}

export function setTimeFilterTimelineModeUpdater<S extends VisState>(
Expand All @@ -3251,11 +3316,7 @@ export function setTimeFilterTimelineModeUpdater<S extends VisState>(

const filter = state.filters.find(f => f.id === filterId) as TimeRangeFilter | undefined;

if (!filter?.syncedWithLayerTimeline) {
return state;
}

if (!validateSyncAnimationMode(filter, syncTimelineMode)) {
if (!filter || !validateSyncAnimationMode(filter, syncTimelineMode)) {
return state;
}

Expand Down Expand Up @@ -3288,6 +3349,34 @@ function validateSyncAnimationMode(filter: TimeRangeFilter, newMode: number) {
);
}

function adjustTimeFilterInterval(state, filter) {
const intervalBasedAnimationLayers = getIntervalBasedAnimationLayers(state.layers);

let interval: string | null = null;
if (intervalBasedAnimationLayers.length > 0) {
// @ts-ignore
const intervalIndex = intervalBasedAnimationLayers.reduce((currentIndex, l) => {
if (l.meta.targetTimeInterval) {
const newIndex = TIME_INTERVALS_ORDERED.findIndex(i => i === l.meta.targetTimeInterval);
return newIndex > -1 && newIndex < currentIndex ? newIndex : currentIndex;
}
}, TIME_INTERVALS_ORDERED.length - 1);
// @ts-ignore
const hexTileInterval = TIME_INTERVALS_ORDERED[intervalIndex];
interval = LayerToFilterTimeInterval[hexTileInterval];
}

if (!interval) {
return filter;
}

// adjust filter
const timeFormat = getDefaultTimeFormat(interval);
const updatedPlotType = {...filter.plotType, interval, timeFormat};
const newFilter = updateTimeFilterPlotType(filter, updatedPlotType, state.datasets);
return adjustValueToAnimationWindow(state, newFilter);
}

// Find dataId from a saved visState property:
// layers, filters, interactions, layerBlending, overlayBlending, splitMaps, animationConfig, editor
// replace it with another dataId
Expand Down
9 changes: 9 additions & 0 deletions src/utils/src/filter-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1252,6 +1252,15 @@ export function getAnimatableVisibleLayersByType(layers: any[], type: string): a
return getAnimatableVisibleLayers(layers).filter(l => l.type === type);
}

/**
* @param {Layer[]} layers
* @returns {Layer[]}
*/
export function getIntervalBasedAnimationLayers(layers: any[]): any[] {
// @ts-ignore
return getAnimatableVisibleLayers(layers).filter(l => l.config.animation?.timeSteps);
}

export function mergeFilterWithTimeline(
filter: TimeRangeFilter,
animationConfig: AnimationConfig
Expand Down
16 changes: 13 additions & 3 deletions src/utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,14 @@ export {createNotification, exportImageError, successNotification} from './notif
export {setStyleSheetBaseHref} from './dom-utils';
export {default as domtoimage} from './dom-to-image';
export {getFrequency, getMode, aggregate} from './aggregation';
export {getBinThresholds, histogramFromThreshold, histogramFromDomain, runGpuFilterForPlot} from './plot';
export {
getBinThresholds,
histogramFromThreshold,
histogramFromDomain,
runGpuFilterForPlot,
adjustValueToAnimationWindow,
updateTimeFilterPlotType
} from './plot';
// eslint-disable-next-line prettier/prettier
export type {FieldFormatter} from './data-utils';
export * from './data-utils';
Expand All @@ -30,7 +37,10 @@ export {
getTimelineFromAnimationConfig,
getTimelineFromFilter,
SAMPLE_TIMELINE,
TIMELINE_MODES
TIMELINE_MODES,
LayerToFilterTimeInterval,
TIME_INTERVALS_ORDERED,
TileTimeInterval
} from './time';

export {
Expand All @@ -45,7 +55,7 @@ export {
getFormatLabels,
getFieldFormatLabels
} from './dataset-utils';
export {getFormatValue} from './format';
export {getFormatValue, getDefaultTimeFormat} from './format';
export {exportMapToHTML} from './export-map-html';
export {
isMSEdge,
Expand Down
33 changes: 29 additions & 4 deletions src/utils/src/time.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,46 @@
// SPDX-License-Identifier: MIT
// Copyright contributors to the kepler.gl project

import {AnimationConfig, Timeline, TimeRangeFilter} from '@kepler.gl/types';
import {ascending, bisector, tickStep} from 'd3-array';
import moment from 'moment';

import {toArray} from './utils';
import {
TICK_INTERVALS,
BINS_LARGE,
DURATIONS,
TIME_INTERVALS,
durationYear,
INTERVAL,
TickInterval
} from '@kepler.gl/constants';
import moment from 'moment';
import {AnimationConfig, Timeline, TimeRangeFilter} from '@kepler.gl/types';

import {toArray} from './utils';
import {getFrequency} from './aggregation';

import {ascending, bisector, tickStep} from 'd3-array';
export const TileTimeInterval = {
YEAR: 'Y',
MONTH: 'M',
DAY: 'D',
HOUR: 'H',
MINUTE: 'T'
};

export const TIME_INTERVALS_ORDERED = [
TileTimeInterval.MINUTE,
TileTimeInterval.HOUR,
TileTimeInterval.DAY,
TileTimeInterval.MONTH,
TileTimeInterval.YEAR
];

export const LayerToFilterTimeInterval = {
[TileTimeInterval.MINUTE]: INTERVAL['1-minute'],
[TileTimeInterval.HOUR]: INTERVAL['1-hour'],
[TileTimeInterval.DAY]: INTERVAL['1-day'],
[TileTimeInterval.MONTH]: INTERVAL['1-month'],
[TileTimeInterval.YEAR]: INTERVAL['1-year']
};

export const TIMELINE_MODES = {
inner: 'inner',
Expand Down
Loading

0 comments on commit 8ea1cab

Please sign in to comment.