Skip to content

Commit

Permalink
[chore] Add several vis state mergers combineConfigs and improve TS (#…
Browse files Browse the repository at this point in the history
…2634)

- Adds `combineConfigs` to some vis state saved configs that are non-array complex objects:
  1. `interactionConfig`
  2. `layerBlending`
  3. `overlayBlending`
  4. `editor`
  
  Each of these has inline comments (and unit tests) explaining the decisions made on how to combine values in different situations. For example, `interactionConfig.brush.size` gets aggregated into keeping the single largest value among all brush sizes.

- Improves and fixes some TS defs.

- Adds jest unit tests for newly added `combineConfigs`.

Signed-off-by: Ihor Dykhta <dikhta.igor@gmail.com>
Co-authored-by: Ilya Boyandin <iboyandin@foursquare.com>
  • Loading branch information
igorDykhta and ilyabo authored Dec 29, 2024
1 parent 9f3f089 commit 1412365
Show file tree
Hide file tree
Showing 5 changed files with 363 additions and 19 deletions.
10 changes: 5 additions & 5 deletions src/reducers/src/merger-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ function callFunctionGetTask(fn: () => any): [any, any] {
export function mergeStateFromMergers<State extends VisState>(
state: State,
initialState: State,
mergers: Merger<any>[],
mergers: Merger<State>[],
postMergerPayload: PostMergerPayload
): {
mergedState: State;
Expand Down Expand Up @@ -80,7 +80,7 @@ export function mergeStateFromMergers<State extends VisState>(

export function hasPropsToMerge<State extends object>(
state: State,
mergerProps: string | string[]
mergerProps?: string | string[]
): boolean {
return Array.isArray(mergerProps)
? Boolean(mergerProps.some(p => Object.prototype.hasOwnProperty.call(state, p)))
Expand All @@ -89,15 +89,15 @@ export function hasPropsToMerge<State extends object>(

export function getPropValueToMerger<State extends object>(
state: State,
mergerProps: string | string[],
mergerProps?: string | string[],
toMergeProps?: string | string[]
): Partial<State> | ValueOf<State> {
return Array.isArray(mergerProps)
return Array.isArray(mergerProps) && Array.isArray(toMergeProps)
? mergerProps.reduce((accu, p, i) => {
if (!toMergeProps) return accu;
return {...accu, [toMergeProps[i]]: state[p]};
}, {})
: state[mergerProps];
: state[mergerProps as string];
}

export function resetStateToMergeProps<State extends VisState>(
Expand Down
153 changes: 144 additions & 9 deletions src/reducers/src/vis-state-merger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@ import {
getInitialMapLayersForSplitMap,
applyFiltersToDatasets,
validateFiltersUpdateDatasets,
findById
findById,
aggregate
} from '@kepler.gl/utils';

import {Layer, VisualChannel} from '@kepler.gl/layers';
import {createEffect} from '@kepler.gl/effects';
import {LAYER_BLENDINGS, OVERLAY_BLENDINGS} from '@kepler.gl/constants';
import {notNullorUndefined} from '@kepler.gl/common-utils';
import {AGGREGATION_TYPES, LAYER_BLENDINGS, OVERLAY_BLENDINGS} from '@kepler.gl/constants';
import {CURRENT_VERSION, VisState, VisStateMergers, KeplerGLSchemaClass} from '@kepler.gl/schemas';

import {
Expand All @@ -30,7 +32,9 @@ import {
ParsedEffect,
LayerColumns,
LayerColumn,
ParsedFilter
ParsedFilter,
NestedPartial,
SavedAnimationConfig
} from '@kepler.gl/types';
import {KeplerTable, Datasets, assignGpuChannels, resetFilterGpuMode} from '@kepler.gl/table';

Expand Down Expand Up @@ -334,7 +338,7 @@ export function mergeInteractions<S extends VisState>(
state: S,
interactionToBeMerged: Partial<SavedInteractionConfig> | undefined
): S {
const merged: Partial<SavedInteractionConfig> = {};
const merged: NestedPartial<SavedInteractionConfig> = {};
const unmerged: Partial<SavedInteractionConfig> = {};

if (interactionToBeMerged) {
Expand Down Expand Up @@ -399,6 +403,64 @@ export function mergeInteractions<S extends VisState>(
return nextState;
}

function combineInteractionConfigs(configs: SavedInteractionConfig[]): SavedInteractionConfig {
const combined = {...configs[0]};
// handle each property key of an `InteractionConfig`, e.g. tooltip, geocoder, brush, coordinate
// by combining values for each among all passed in configs

for (const key in combined) {
const toBeCombinedProps = configs.map(c => c[key]);

// all of these have an enabled boolean
combined[key] = {
// are any of the configs' enabled values true?
enabled: toBeCombinedProps.some(p => p?.enabled)
};

if (key === 'tooltip') {
// are any of the configs' compareMode values true?
combined[key].compareMode = toBeCombinedProps.some(p => p?.compareMode);

// return the compare type mode, it will be either absolute or relative
combined[key].compareType = getValueWithHighestOccurrence(
toBeCombinedProps.map(p => p.compareType)
);

// combine fieldsToShow among all dataset ids
combined[key].fieldsToShow = toBeCombinedProps
.map(p => p.fieldsToShow)
.reduce((acc, nextFieldsToShow) => {
for (const nextDataIdKey in nextFieldsToShow) {
const nextTooltipFields = nextFieldsToShow[nextDataIdKey];
if (!acc[nextDataIdKey]) {
// if the dataset id is not present in the accumulator
// then add it with its tooltip fields
acc[nextDataIdKey] = nextTooltipFields;
} else {
// otherwise the dataset id is already present in the accumulator
// so only add the next tooltip fields for this dataset's array if they are not already present,
// using the tooltipField.name property for uniqueness
nextTooltipFields.forEach(nextTF => {
if (!acc[nextDataIdKey].find(({name}) => nextTF.name === name)) {
acc[nextDataIdKey].push(nextTF);
}
});
}
}
return acc;
}, {});
}

if (key === 'brush') {
// keep the biggest brush size
combined[key].size =
aggregate(toBeCombinedProps, AGGREGATION_TYPES.maximum, p => p.size) ?? null;
}
}

return combined;
}

function savedUnmergedInteraction<S extends VisState>(
state: S,
unmerged: Partial<SavedInteractionConfig>
Expand Down Expand Up @@ -434,6 +496,7 @@ function replaceInteractionDatasetIds(interactionConfig, dataId: string, dataIdT
}
return null;
}

/**
* Merge splitMaps config with current visStete.
* 1. if current map is split, but splitMap DOESNOT contain maps
Expand Down Expand Up @@ -571,6 +634,15 @@ export function mergeLayerBlending<S extends VisState>(
return state;
}

/**
* Combines multiple layer blending configs into a single string
* by returning the one with the highest occurrence
*/
function combineLayerBlendingConfigs(configs: string[]): string | null {
// return the mode of the layer blending type
return getValueWithHighestOccurrence(configs);
}

/**
* Merge overlayBlending with saved
*/
Expand All @@ -588,6 +660,15 @@ export function mergeOverlayBlending<S extends VisState>(
return state;
}

/**
* Combines multiple overlay blending configs into a single string
* by returning the one with the highest occurrence
**/
function combineOverlayBlendingConfigs(configs: string[]): string | null {
// return the mode of the overlay blending type
return getValueWithHighestOccurrence(configs);
}

/**
* Merge animation config
*/
Expand All @@ -609,6 +690,14 @@ export function mergeAnimationConfig<S extends VisState>(
return state;
}

function combineAnimationConfigs(configs: SavedAnimationConfig[]): SavedAnimationConfig {
// get the smallest values of currentTime and speed among all configs
return {
currentTime: aggregate(configs, AGGREGATION_TYPES.minimum, c => c.currentTime) ?? null,
speed: aggregate(configs, AGGREGATION_TYPES.minimum, c => c.speed) ?? null
};
}

/**
* Validate saved layer columns with new data,
* update fieldIdx based on new fields
Expand Down Expand Up @@ -944,6 +1033,24 @@ export function mergeEditor<S extends VisState>(state: S, savedEditor: SavedEdit
};
}

function combineEditorConfigs(configs: SavedEditor[]): SavedEditor {
return configs.reduce(
(acc, nextConfig) => {
return {
...acc,
features: [...acc.features, ...(nextConfig.features || [])]
};
},
{
// start with:
// - empty array for features accumulation
// - and are any of the configs' visible values true?
features: [],
visible: configs.some(c => c?.visible)
}
);
}

/**
* Validate saved layer config with new data,
* update fieldIdx based on new fields
Expand Down Expand Up @@ -971,6 +1078,29 @@ export function mergeDatasetsByOrder(state: VisState, newDataEntries: Datasets):
return merged;
}

/**
* Simliar purpose to aggregation utils `getMode` function,
* but returns the mode in the same value type without coercing to a string.
* It ignores `undefined` or `null` values, but returns `null` if no mode could be calculated.
*/
function getValueWithHighestOccurrence<T>(arr: T[]): T | null {
const tallys = new Map();
arr.forEach(value => {
if (notNullorUndefined(value)) {
if (!tallys.has(value)) {
tallys.set(value, 1);
} else {
tallys.set(value, tallys.get(value) + 1);
}
}
});
// return the value with the highest total occurrence count
if (tallys.size === 0) {
return null;
}
return [...tallys.entries()]?.reduce((acc, next) => (next[1] > acc[1] ? next : acc))[0];
}

export const VIS_STATE_MERGERS: VisStateMergers<any> = [
{
merge: mergeLayers,
Expand All @@ -994,11 +1124,16 @@ export const VIS_STATE_MERGERS: VisStateMergers<any> = [
prop: 'interactionConfig',
toMergeProp: 'interactionToBeMerged',
replaceParentDatasetIds: replaceInteractionDatasetIds,
saveUnmerged: savedUnmergedInteraction
saveUnmerged: savedUnmergedInteraction,
combineConfigs: combineInteractionConfigs
},
{merge: mergeLayerBlending, prop: 'layerBlending', combineConfigs: combineLayerBlendingConfigs},
{
merge: mergeOverlayBlending,
prop: 'overlayBlending',
combineConfigs: combineOverlayBlendingConfigs
},
{merge: mergeLayerBlending, prop: 'layerBlending'},
{merge: mergeOverlayBlending, prop: 'overlayBlending'},
{merge: mergeSplitMaps, prop: 'splitMaps', toMergeProp: 'splitMapsToBeMerged'},
{merge: mergeAnimationConfig, prop: 'animationConfig'},
{merge: mergeEditor, prop: 'editor'}
{merge: mergeAnimationConfig, prop: 'animationConfig', combineConfigs: combineAnimationConfigs},
{merge: mergeEditor, prop: 'editor', combineConfigs: combineEditorConfigs}
];
1 change: 1 addition & 0 deletions src/schemas/src/vis-state-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ export type Merger<S extends object> = {
waitForLayerData?: boolean;
replaceParentDatasetIds?: ReplaceParentDatasetIdsFunc<ValueOf<S>>;
saveUnmerged?: (state: S, unmerged: any) => S;
combineConfigs?: (configs: S[]) => S;
getChildDatasetIds?: any;
};
export type VisStateMergers<S extends object> = Merger<S>[];
Expand Down
10 changes: 5 additions & 5 deletions src/types/schemas.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import {RGBColor, Merge, RequireFrom} from './types';

import {Filter, TooltipInfo, AnimationConfig, SplitMap, Feature} from './reducers';
import {Filter, InteractionConfig, AnimationConfig, SplitMap, Feature} from './reducers';

import {LayerTextLabel} from './layers';

Expand All @@ -30,16 +30,16 @@ export type MinSavedFilter = RequireFrom<SavedFilter, 'dataId' | 'id' | 'name' |
export type ParsedFilter = SavedFilter | MinSavedFilter;

export type SavedInteractionConfig = {
tooltip: TooltipInfo['config'] & {
tooltip: InteractionConfig['tooltip']['config'] & {
enabled: boolean;
};
geocoder: TooltipInfo['geocoder'] & {
geocoder: {
enabled: boolean;
};
brush: TooltipInfo['brush'] & {
brush: InteractionConfig['brush']['config'] & {
enabled: boolean;
};
coordinate: TooltipInfo['coordinate'] & {
coordinate: {
enabled: boolean;
};
};
Expand Down
Loading

0 comments on commit 1412365

Please sign in to comment.