kepler.gl
Version:
kepler.gl is a webgl based application to visualize large scale location data in the browser
1,080 lines (930 loc) • 28 kB
JavaScript
// Copyright (c) 2018 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import {console as Console} from 'global/window';
import Task, {disableStackCapturing, withTask} from 'react-palm/tasks';
// Tasks
import {LOAD_FILE_TASK} from 'tasks/tasks';
// Actions
import {loadFilesErr} from 'actions/vis-state-actions';
import {addDataToMap} from 'actions';
// Utils
import {generateHashId} from 'utils/utils';
import {
getDefaultInteraction,
findFieldsToShow
} from 'utils/interaction-utils';
import {
getDefaultFilter,
getFilterProps,
getFilterPlot,
getDefaultFilterPlotType,
filterData
} from 'utils/filter-utils';
import {createNewDataEntry} from 'utils/dataset-utils';
import {
findDefaultLayer,
calculateLayerData
} from 'utils/layer-utils/layer-utils';
import {getFileHandler} from 'processors/file-handler';
import {
mergeFilters,
mergeLayers,
mergeInteractions,
mergeLayerBlending
} from './vis-state-merger';
// LayerClasses contain ES6 Class, do not instatiate in iso rendering
// const {LayerClasses} = isBrowser || isTesting ?
// require('layers') : {
// LayerClasses: {}
// };
import {Layer, LayerClasses} from 'layers';
// react-palm
// disable capture exception for react-palm call to withTask
disableStackCapturing();
export const INITIAL_VIS_STATE = {
// layers
layers: [],
layerData: [],
layerToBeMerged: [],
layerOrder: [],
// filters
filters: [],
filterToBeMerged: [],
// a collection of multiple dataset
datasets: {},
editingDataset: undefined,
interactionConfig: getDefaultInteraction(),
interactionToBeMerged: undefined,
layerBlending: 'normal',
hoverInfo: undefined,
clicked: undefined,
fileLoading: false,
fileLoadingErr: null,
// this is used when user split maps
splitMaps: [
// this will contain a list of objects to
// describe the state of layer availability and visibility for each map
// [
// {
// layers: {
// layer_id: {
// isAvailable: true|false # this is driven by the left hand panel
// isVisible: true|false
// }
// }
// }
// ]
],
// defaults layer classes
layerClasses: LayerClasses
};
function updateStateWithLayerAndData(state, {layerData, layer, idx}) {
return {
...state,
layers: state.layers.map((lyr, i) => (i === idx ? layer : lyr)),
layerData: layerData
? state.layerData.map((d, i) => (i === idx ? layerData : d))
: state.layerData
};
}
/**
* Called to update layer base config: dataId, label, column, isVisible
*
*/
export function layerConfigChangeUpdater(state, action) {
const {oldLayer} = action;
const idx = state.layers.findIndex(l => l.id === oldLayer.id);
const props = Object.keys(action.newConfig);
const newLayer = oldLayer.updateLayerConfig(action.newConfig);
if (newLayer.shouldCalculateLayerData(props)) {
const oldLayerData = state.layerData[idx];
const {layerData, layer} = calculateLayerData(
newLayer,
state,
oldLayerData,
{sameData: true}
);
return updateStateWithLayerAndData(state, {layerData, layer, idx});
}
const newState = {
...state,
splitMaps:
'isVisible' in action.newConfig
? toggleLayerFromSplitMaps(state, newLayer)
: state.splitMaps
};
return updateStateWithLayerAndData(newState, {layer: newLayer, idx});
}
export function layerTypeChangeUpdater(state, action) {
const {oldLayer, newType} = action;
const oldId = oldLayer.id;
const idx = state.layers.findIndex(l => l.id === oldId);
if (!state.layerClasses[newType]) {
Console.error(`${newType} is not a valid layer type`);
return state;
}
// get a mint layer, with new id and type
// because deck.gl uses id to match between new and old layer.
// If type has changed but id is the same, it will break
const newLayer = new state.layerClasses[newType]();
newLayer.assignConfigToLayer(oldLayer.config, oldLayer.visConfigSettings);
if (newLayer.config.dataId) {
const dataset = state.datasets[newLayer.config.dataId];
newLayer.updateLayerDomain(dataset);
}
const {layerData, layer} = calculateLayerData(newLayer, state);
let newState = state;
// update splitMap layer id
if (state.splitMaps) {
newState = {
...state,
splitMaps: state.splitMaps.map(settings => {
const {[oldId]: oldLayerMap, ...otherLayers} = settings.layers;
return {
...settings,
layers: {
...otherLayers,
[layer.id]: oldLayerMap
}
};
})
};
}
return updateStateWithLayerAndData(newState, {layerData, layer, idx});
}
export function layerVisualChannelChangeUpdater(state, action) {
const {oldLayer, newConfig, channel} = action;
const dataset = state.datasets[oldLayer.config.dataId];
const idx = state.layers.findIndex(l => l.id === oldLayer.id);
const newLayer = oldLayer.updateLayerConfig(newConfig);
newLayer.updateLayerVisualChannel(dataset, channel);
const oldLayerData = state.layerData[idx];
const {layerData, layer} = calculateLayerData(newLayer, state, oldLayerData, {
sameData: true
});
return updateStateWithLayerAndData(state, {layerData, layer, idx});
}
export function layerVisConfigChangeUpdater(state, action) {
const {oldLayer} = action;
const idx = state.layers.findIndex(l => l.id === oldLayer.id);
const props = Object.keys(action.newVisConfig);
const newVisConfig = {
...oldLayer.config.visConfig,
...action.newVisConfig
};
const newLayer = oldLayer.updateLayerConfig({visConfig: newVisConfig});
if (newLayer.shouldCalculateLayerData(props)) {
const oldLayerData = state.layerData[idx];
const {layerData, layer} = calculateLayerData(
newLayer,
state,
oldLayerData,
{sameData: true}
);
return updateStateWithLayerAndData(state, {layerData, layer, idx});
}
return updateStateWithLayerAndData(state, {layer: newLayer, idx});
}
/* eslint-enable max-statements */
export function interactionConfigChangeUpdater(state, action) {
const {config} = action;
const interactionConfig = {
...state.interactionConfig,
...{[config.id]: config}
};
if (config.enabled && !state.interactionConfig[config.id].enabled) {
// only enable one interaction at a time
Object.keys(interactionConfig).forEach(k => {
if (k !== config.id) {
interactionConfig[k] = {...interactionConfig[k], enabled: false};
}
});
}
return {
...state,
interactionConfig
};
}
export function setFilterUpdater(state, action) {
const {idx, prop, value} = action;
let newState = state;
let newFilter = {
...state.filters[idx],
[prop]: value
};
const {dataId} = newFilter;
if (!dataId) {
return state;
}
const {fields, allData} = state.datasets[dataId];
switch (prop) {
case 'dataId':
// if trying to update filter dataId. create an empty new filter
newFilter = getDefaultFilter(dataId);
break;
case 'name':
// find the field
const fieldIdx = fields.findIndex(f => f.name === value);
let field = fields[fieldIdx];
if (!field.filterProp) {
// get filter domain from field
// save filterProps: {domain, steps, value} to field, avoid recalculate
field = {
...field,
filterProp: getFilterProps(allData, field)
};
}
newFilter = {
...newFilter,
...field.filterProp,
name: field.name,
// can't edit dataId once name is selected
freeze: true,
fieldIdx
};
const enlargedFilterIdx = state.filters.findIndex(f => f.enlarged);
if (enlargedFilterIdx > -1 && enlargedFilterIdx !== idx) {
// there should be only one enlarged filter
newFilter.enlarged = false;
}
newState = {
...state,
datasets: {
...state.datasets,
[dataId]: {
...state.datasets[dataId],
fields: fields.map((d, i) => (i === fieldIdx ? field : d))
}
}
};
break;
case 'value':
default:
break;
}
// save new filters to newState
newState = {
...newState,
filters: state.filters.map((f, i) => (i === idx ? newFilter : f))
};
// filter data
newState = {
...newState,
datasets: {
...newState.datasets,
[dataId]: {
...newState.datasets[dataId],
...filterData(allData, dataId, newState.filters)
}
}
};
newState = updateAllLayerDomainData(newState, dataId, newFilter);
return newState;
}
export const setFilterPlotUpdater = (state, {idx, newProp}) => {
let newFilter = {...state.filters[idx], ...newProp};
const prop = Object.keys(newProp)[0];
if (prop === 'yAxis') {
const plotType = getDefaultFilterPlotType(newFilter);
if (plotType) {
newFilter = {
...newFilter,
...getFilterPlot(
{...newFilter, plotType},
state.datasets[newFilter.dataId].allData
),
plotType
};
}
}
return {
...state,
filters: state.filters.map((f, i) => (i === idx ? newFilter : f))
};
};
export const addFilterUpdater = (state, action) =>
!action.dataId
? state
: {
...state,
filters: [...state.filters, getDefaultFilter(action.dataId)]
};
export const toggleFilterAnimationUpdater = (state, action) => ({
...state,
filters: state.filters.map(
(f, i) => (i === action.idx ? {...f, isAnimating: !f.isAnimating} : f)
)
});
export const updateAnimationSpeedUpdater = (state, action) => ({
...state,
filters: state.filters.map(
(f, i) => (i === action.idx ? {...f, speed: action.speed} : f)
)
});
export const enlargeFilterUpdater = (state, action) => {
const isEnlarged = state.filters[action.idx].enlarged;
return {
...state,
filters: state.filters.map((f, i) => {
f.enlarged = !isEnlarged && i === action.idx;
return f;
})
};
};
export const removeFilterUpdater = (state, action) => {
const {idx} = action;
const {dataId} = state.filters[idx];
const newFilters = [
...state.filters.slice(0, idx),
...state.filters.slice(idx + 1, state.filters.length)
];
const newState = {
...state,
datasets: {
...state.datasets,
[dataId]: {
...state.datasets[dataId],
...filterData(state.datasets[dataId].allData, dataId, newFilters)
}
},
filters: newFilters
};
return updateAllLayerDomainData(newState, dataId);
};
export const addLayerUpdater = (state, action) => {
const defaultDataset = Object.keys(state.datasets)[0];
const newLayer = new Layer({
isVisible: true,
isConfigActive: true,
dataId: defaultDataset,
...action.props
});
return {
...state,
layers: [...state.layers, newLayer],
layerData: [...state.layerData, {}],
layerOrder: [...state.layerOrder, state.layerOrder.length],
splitMaps: addNewLayersToSplitMap(state.splitMaps, newLayer)
};
};
export const removeLayerUpdater = (state, {idx}) => {
const {layers, layerData, clicked, hoverInfo} = state;
const layerToRemove = state.layers[idx];
const newMaps = removeLayerFromSplitMaps(state, layerToRemove);
return {
...state,
layers: [...layers.slice(0, idx), ...layers.slice(idx + 1, layers.length)],
layerData: [
...layerData.slice(0, idx),
...layerData.slice(idx + 1, layerData.length)
],
layerOrder: state.layerOrder
.filter(i => i !== idx)
.map(pid => (pid > idx ? pid - 1 : pid)),
clicked: layerToRemove.isLayerHovered(clicked) ? undefined : clicked,
hoverInfo: layerToRemove.isLayerHovered(hoverInfo) ? undefined : hoverInfo,
splitMaps: newMaps
};
};
export const reorderLayerUpdater = (state, {order}) => ({
...state,
layerOrder: order
});
export const removeDatasetUpdater = (state, action) => {
// extract dataset key
const {key: datasetKey} = action;
const {datasets} = state;
// check if dataset is present
if (!datasets[datasetKey]) {
return state;
}
/* eslint-disable no-unused-vars */
const {
layers,
datasets: {[datasetKey]: dataset, ...newDatasets}
} = state;
/* eslint-enable no-unused-vars */
const indexes = layers.reduce((listOfIndexes, layer, index) => {
if (layer.config.dataId === datasetKey) {
listOfIndexes.push(index);
}
return listOfIndexes;
}, []);
// remove layers and datasets
const {newState} = indexes.reduce(
({newState: currentState, indexCounter}, idx) => {
const currentIndex = idx - indexCounter;
currentState = removeLayerUpdater(currentState, {idx: currentIndex});
indexCounter++;
return {newState: currentState, indexCounter};
},
{newState: {...state, datasets: newDatasets}, indexCounter: 0}
);
// remove filters
const filters = state.filters.filter(filter => filter.dataId !== datasetKey);
// update interactionConfig
let {interactionConfig} = state;
const {tooltip} = interactionConfig;
if (tooltip) {
const {config} = tooltip;
/* eslint-disable no-unused-vars */
const {[datasetKey]: fields, ...fieldsToShow} = config.fieldsToShow;
/* eslint-enable no-unused-vars */
interactionConfig = {
...interactionConfig,
tooltip: {...tooltip, config: {...config, fieldsToShow}}
};
}
return {...newState, filters, interactionConfig};
};
export const updateLayerBlendingUpdater = (state, action) => ({
...state,
layerBlending: action.mode
});
export const showDatasetTableUpdater = (state, action) => {
return {
...state,
editingDataset: action.dataId
};
};
export const resetMapConfigVisStateUpdater = (state, action) => ({
...INITIAL_VIS_STATE,
...state.initialState,
initialState: state.initialState
});
/**
* Loads custom configuration into state
* @param state
* @param action
* @returns {*}
*/
export const receiveMapConfigUpdater = (state, action) => {
if (!action.payload.visState) {
return state;
}
const {
filters,
layers,
interactionConfig,
layerBlending,
splitMaps
} = action.payload.visState;
// always reset config when receive a new config
const resetState = resetMapConfigVisStateUpdater(state);
let mergedState = {
...resetState,
splitMaps: splitMaps || [] // maps doesn't require any logic
};
mergedState = mergeFilters(mergedState, filters);
mergedState = mergeLayers(mergedState, layers);
mergedState = mergeInteractions(mergedState, interactionConfig);
mergedState = mergeLayerBlending(mergedState, layerBlending);
return mergedState;
};
export const layerHoverUpdater = (state, action) => ({
...state,
hoverInfo: action.info
});
export const layerClickUpdater = (state, action) => ({
...state,
clicked: action.info && action.info.picked ? action.info : null
});
export const mapClickUpdater = (state, action) => ({
...state,
clicked: null
});
export const toggleSplitMapUpdater = (state, action) =>
state.splitMaps && state.splitMaps.length === 0
? {
...state,
// maybe we should use an array to store state for a single map as well
// if current maps length is equal to 0 it means that we are about to split the view
splitMaps: computeSplitMapLayers(state.layers)
}
: closeSpecificMapAtIndex(state, action);
/**
* This is triggered when view is split into multiple maps.
* It will only update layers that belong to the map layer dropdown
* the user is interacting wit
* @param state
* @param action
*/
export const setVisibleLayersForMapUpdater = (state, action) => {
const {mapIndex, layerIds} = action;
if (!layerIds) {
return state;
}
const {splitMaps = []} = state;
if (splitMaps.length === 0) {
// we should never get into this state
// because this action should only be triggered
// when map view is split
// but something may have happened
return state;
}
// need to check if maps is populated otherwise will create
const {[mapIndex]: map = {}} = splitMaps;
const layers = map.layers || [];
// we set visibility to true for all layers included in our input list
const newLayers = (Object.keys(layers) || []).reduce((currentLayers, idx) => {
return {
...currentLayers,
[idx]: {
...layers[idx],
isVisible: layerIds.includes(idx)
}
};
}, {});
const newMaps = [...splitMaps];
newMaps[mapIndex] = {
...splitMaps[mapIndex],
layers: newLayers
};
return {
...state,
splitMaps: newMaps
};
};
export const toggleLayerForMapUpdater = (state, action) => {
if (!state.splitMaps[action.mapIndex]) {
return state;
}
const mapSettings = state.splitMaps[action.mapIndex];
const {layers} = mapSettings;
if (!layers || !layers[action.layerId]) {
return state;
}
const layer = layers[action.layerId];
const newLayer = {
...layer,
isVisible: !layer.isVisible
};
const newLayers = {
...layers,
[action.layerId]: newLayer
};
// const splitMaps = state.splitMaps;
const newSplitMaps = [...state.splitMaps];
newSplitMaps[action.mapIndex] = {
...mapSettings,
layers: newLayers
};
return {
...state,
splitMaps: newSplitMaps
};
};
/* eslint-disable max-statements */
export const updateVisDataUpdater = (state, action) => {
// datasets can be a single data entries or an array of multiple data entries
const datasets = Array.isArray(action.datasets)
? action.datasets
: [action.datasets];
if (action.config) {
// apply config if passed from action
state = receiveMapConfigUpdater(state, {
payload: {visState: action.config}
});
}
const newDateEntries = datasets.reduce(
(accu, {info = {}, data}) => ({
...accu,
...(createNewDataEntry({info, data}, state.datasets) || {})
}),
{}
);
if (!Object.keys(newDateEntries).length) {
return state;
}
const stateWithNewData = {
...state,
datasets: {
...state.datasets,
...newDateEntries
}
};
// previously saved config before data loaded
const {
filterToBeMerged = [],
layerToBeMerged = [],
interactionToBeMerged = {}
} = stateWithNewData;
// merge state with saved filters
let mergedState = mergeFilters(stateWithNewData, filterToBeMerged);
// merge state with saved layers
mergedState = mergeLayers(mergedState, layerToBeMerged);
if (mergedState.layers.length === state.layers.length) {
// no layer merged, find defaults
mergedState = addDefaultLayers(mergedState, newDateEntries);
}
if (mergedState.splitMaps.length) {
const newLayers = mergedState.layers.filter(
l => l.config.dataId in newDateEntries
);
// if map is splited, add new layers to splitMaps
mergedState = {
...mergedState,
splitMaps: addNewLayersToSplitMap(mergedState.splitMaps, newLayers)
};
}
// merge state with saved interactions
mergedState = mergeInteractions(mergedState, interactionToBeMerged);
// if no tooltips merged add default tooltips
Object.keys(newDateEntries).forEach(dataId => {
const tooltipFields =
mergedState.interactionConfig.tooltip.config.fieldsToShow[dataId];
if (!Array.isArray(tooltipFields) || !tooltipFields.length) {
mergedState = addDefaultTooltips(mergedState, newDateEntries[dataId]);
}
});
return updateAllLayerDomainData(mergedState, Object.keys(newDateEntries));
};
/* eslint-enable max-statements */
function generateLayerMetaForSplitViews(layer) {
return {
isAvailable: layer.config.isVisible,
isVisible: layer.config.isVisible
};
}
/**
* This emthod will compute the default maps custom list
* based on the current layers status
* @param layers
* @returns {[*,*]}
*/
function computeSplitMapLayers(layers) {
const mapLayers = layers.reduce(
(newLayers, currentLayer) => ({
...newLayers,
[currentLayer.id]: generateLayerMetaForSplitViews(currentLayer)
}),
{}
);
return [
{
layers: mapLayers
},
{
layers: mapLayers
}
];
}
/**
* Remove an existing layers from custom map layer objects
* @param state
* @param layer
* @returns {[*,*]} Maps of custom layer objects
*/
function removeLayerFromSplitMaps(state, layer) {
return state.splitMaps.map(settings => {
const {layers} = settings;
/* eslint-disable no-unused-vars */
const {[layer.id]: _, ...newLayers} = layers;
/* eslint-enable no-unused-vars */
return {
...settings,
layers: newLayers
};
});
}
/**
* Add new layers to both existing maps
* @param splitMaps
* @param layers
* @returns {[*,*]} new splitMaps
*/
function addNewLayersToSplitMap(splitMaps, layers) {
const newLayers = Array.isArray(layers) ? layers : [layers];
if (!splitMaps || !splitMaps.length || !newLayers.length) {
return splitMaps;
}
// add new layer to both maps,
// don't override, if layer.id is already in splitMaps.settings.layers
return splitMaps.map(settings => ({
...settings,
layers: {
...settings.layers,
...newLayers.reduce(
(accu, newLayer) =>
newLayer.config.isVisible
? {
...accu,
[newLayer.id]: settings.layers[newLayer.id]
? settings.layers[newLayer.id]
: generateLayerMetaForSplitViews(newLayer)
}
: accu,
{}
)
}
}));
}
/**
* Hide an existing layers from custom map layer objects
* @param state
* @param layer
* @returns {[*,*]} Maps of custom layer objects
*/
function toggleLayerFromSplitMaps(state, layer) {
return state.splitMaps.map(settings => {
const {layers} = settings;
const newLayers = {
...layers,
[layer.id]: generateLayerMetaForSplitViews(layer)
};
return {
...settings,
layers: newLayers
};
});
}
/**
* When a user clicks on the specific map closing icon
* the application will close the selected map
* and will merge the remaining one with the global state
* TODO: i think in the future this action should be called merge map layers with global settings
* @param state
* @param action
* @returns {*}
*/
function closeSpecificMapAtIndex(state, action) {
// retrieve layers meta data from the remaining map that we need to keep
const indexToRetrieve = 1 - action.payload;
const metaSettings = state.splitMaps[indexToRetrieve];
if (!metaSettings || !metaSettings.layers) {
// if we can't find the meta settings we simply clean up splitMaps and
// keep global state as it is
// but why does this ever happen?
return {
...state,
splitMaps: []
};
}
const {layers} = state;
// update layer visibility
const newLayers = layers.map(layer =>
layer.updateLayerConfig({
isVisible: metaSettings.layers[layer.id]
? metaSettings.layers[layer.id].isVisible
: layer.config.isVisible
})
);
// delete map
return {
...state,
layers: newLayers,
splitMaps: []
};
}
// TODO: redo write handler to not use tasks
export const loadFilesUpdater = (state, action) => {
const {files} = action;
const filesToLoad = files.map(fileBlob => ({
fileBlob,
info: {
id: generateHashId(4),
label: fileBlob.name,
size: fileBlob.size
},
handler: getFileHandler(fileBlob)
}));
// reader -> parser -> augment -> receiveVisData
const loadFileTasks = [
Task.all(filesToLoad.map(LOAD_FILE_TASK)).bimap(
results => {
const data = results.reduce((f, c) => ({
// using concat here because the current datasets could be an array or a single item
datasets: f.datasets.concat(c.datasets),
// we need to deep merge this thing unless we find a better solution
// this case will only happen if we allow to load multiple keplergl json files
config: {
...f.config,
...(c.config || {})
}
}), {datasets: [], config: {}, options: {centerMap: true}});
return addDataToMap(data);
},
error => loadFilesErr(error)
)
];
return withTask(
{
...state,
fileLoading: true
},
loadFileTasks
);
};
export const loadFilesErrUpdater = (state, {error}) => ({
...state,
fileLoading: false,
fileLoadingErr: error
});
/**
* helper function to update All layer domain and layer data of state
*
* @param {object} state
* @param {string} datasets
* @returns {object} state
*/
export function addDefaultLayers(state, datasets) {
const defaultLayers = Object.values(datasets).reduce(
(accu, dataset) => [
...accu,
...(findDefaultLayer(dataset, state.layerClasses) || [])
],
[]
);
return {
...state,
layers: [...state.layers, ...defaultLayers],
layerOrder: [
// put new layers on top of old ones
...defaultLayers.map((_, i) => state.layers.length + i),
...state.layerOrder
]
};
}
/**
* helper function to find default tooltips
*
* @param {object} state
* @param {object} dataset
* @returns {object} state
*/
export function addDefaultTooltips(state, dataset) {
const tooltipFields = findFieldsToShow(dataset);
return {
...state,
interactionConfig: {
...state.interactionConfig,
tooltip: {
...state.interactionConfig.tooltip,
config: {
// find default fields to show in tooltip
fieldsToShow: {
...state.interactionConfig.tooltip.config.fieldsToShow,
...tooltipFields
}
}
}
}
};
}
/**
* helper function to update layer domains for an array of datsets
*
* @param {object} state
* @param {array | string} dataId
* @param {object} newFilter - if is called by setFilter, the filter that has changed
* @returns {object} state
*/
export function updateAllLayerDomainData(state, dataId, newFilter) {
const dataIds = typeof dataId === 'string' ? [dataId] : dataId;
const newLayers = [];
const newLayerDatas = [];
state.layers.forEach((oldLayer, i) => {
if (oldLayer.config.dataId && dataIds.includes(oldLayer.config.dataId)) {
// No need to recalculate layer domain if filter has fixed domain
const newLayer =
newFilter && newFilter.fixedDomain
? oldLayer
: oldLayer.updateLayerDomain(
state.datasets[oldLayer.config.dataId],
newFilter
);
const {layerData, layer} = calculateLayerData(
newLayer,
state,
state.layerData[i]
);
newLayers.push(layer);
newLayerDatas.push(layerData);
} else {
newLayers.push(oldLayer);
newLayerDatas.push(state.layerData[i]);
}
});
return {
...state,
layers: newLayers,
layerData: newLayerDatas
};
}