UNPKG

kepler.gl

Version:

kepler.gl is a webgl based application to visualize large scale location data in the browser

256 lines (236 loc) 7.3 kB
// Copyright (c) 2021 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 {hexToRgb} from './color-utils'; import uniq from 'lodash.uniq'; import {ALL_FIELD_TYPES} from 'constants/default-settings'; import {validateInputData} from 'processors/data-processor'; import KeplerTable from './table-utils/kepler-table'; // apply a color for each dataset // to use as label colors const datasetColors = [ '#8F2FBF', '#005CFF', '#C06C84', '#F8B195', '#547A82', '#3EACA8', '#A2D4AB' ].map(hexToRgb); /** * Random color generator * @return {Generator<import('reducers/types').RGBColor>} */ function* generateColor() { let index = 0; while (index < datasetColors.length + 1) { if (index === datasetColors.length) { index = 0; } yield datasetColors[index++]; } } export const datasetColorMaker = generateColor(); /** @type {typeof import('./dataset-utils').getNewDatasetColor} */ export function getNewDatasetColor(datasets) { const presetColors = datasetColors.map(String); const usedColors = uniq(Object.values(datasets).map(d => String(d.color))).filter(c => presetColors.includes(c) ); if (usedColors.length === presetColors.length) { // if we already depleted the pool of color return datasetColorMaker.next().value; } let color = datasetColorMaker.next().value; while (usedColors.includes(String(color))) { color = datasetColorMaker.next().value; } return color; } /** * Take datasets payload from addDataToMap, create datasets entry save to visState * @type {typeof import('./dataset-utils').createNewDataEntry} */ export function createNewDataEntry({info, data, ...opts}, datasets = {}) { const validatedData = validateInputData(data); if (!validatedData) { return {}; } info = info || {}; const color = info.color || getNewDatasetColor(datasets); const keplerTable = new KeplerTable({info, data: validatedData, color, ...opts}); return { [keplerTable.id]: keplerTable }; } /** * Field name prefixes and suffixes which should not be considered * as metrics. Fields will still be included if a 'metric word' * is found on the field name, however. */ const EXCLUDED_DEFAULT_FIELDS = [ // Serial numbers and identification numbers '_id', 'id', 'index', 'uuid', 'guid', 'uid', 'gid', 'serial', // Geographic IDs are unlikely to be interesting to color 'zip', 'code', 'post', 'region', 'fips', 'cbgs', 'h3', 's2', // Geographic coords (but not z/elevation/altitude // since that might be a metric) 'lat', 'lon', 'lng', 'latitude', 'longitude', '_x', '_y' ]; /** * Prefixes and suffixes that indicate a field is a metric. * * Note that these are in order of preference, first being * most preferred. */ const METRIC_DEFAULT_FIELDS = [ 'metric', 'value', 'sum', 'count', 'unique', 'mean', 'mode', 'median', 'max', 'min', 'deviation', 'variance', 'p99', 'p95', 'p75', 'p50', 'p25', 'p05', // Abbreviations are less preferred 'cnt', 'val' ]; /** * Choose a field to use as the default color field of a layer. * * The heuristic is: * * First, exclude fields that are on the exclusion list and don't * have names that suggest they contain metrics. Also exclude * field names that are blank. * * Next, look for a field that is of real type and contains one * of the preferred names (in order of the preferred names). * * Next, look for a field that is of integer type and contains * one of the preferred names (in order of the preferred names). * * Next, look for the first field that is of real type (in order * of field index). * * Next, look for the first field that is of integer type (in * order of field index). * * It's possible no field will be chosen (i.e. because all fields * are strings.) * * @param dataset */ export function findDefaultColorField({fields, fieldPairs = []}) { const fieldsWithoutExcluded = fields.filter(field => { if (field.type !== ALL_FIELD_TYPES.real && field.type !== ALL_FIELD_TYPES.integer) { // Only select numeric fields. return false; } if ( fieldPairs.find( pair => pair.pair.lat.value === field.name || pair.pair.lng.value === field.name ) ) { // Do not permit lat, lon fields return false; } const normalizedFieldName = field.name.toLowerCase(); if (normalizedFieldName === '') { // Special case excluded name when the name is blank. return false; } const hasExcluded = EXCLUDED_DEFAULT_FIELDS.find( f => normalizedFieldName.startsWith(f) || normalizedFieldName.endsWith(f) ); const hasInclusion = METRIC_DEFAULT_FIELDS.find( f => normalizedFieldName.startsWith(f) || normalizedFieldName.endsWith(f) ); return !hasExcluded || hasInclusion; }); const sortedFields = fieldsWithoutExcluded.sort((left, right) => { const normalizedLeft = left.name.toLowerCase(); const normalizedRight = right.name.toLowerCase(); const leftHasInclusion = METRIC_DEFAULT_FIELDS.findIndex( f => normalizedLeft.startsWith(f) || normalizedLeft.endsWith(f) ); const rightHasInclusion = METRIC_DEFAULT_FIELDS.findIndex( f => normalizedRight.startsWith(f) || normalizedRight.endsWith(f) ); if (leftHasInclusion !== rightHasInclusion) { if (leftHasInclusion === -1) { // Elements that do not have the inclusion list should go after those that do. return 1; } else if (rightHasInclusion === -1) { // Elements that do have the inclusion list should go before those that don't. return -1; } // Compare based on order in the inclusion list return leftHasInclusion - rightHasInclusion; } // Compare based on type if (left.type !== right.type) { if (left.type === ALL_FIELD_TYPES.real) { return -1; } // left is an integer and right is not // and reals come before integers return 1; } // Finally, order based on the order in the datasets columns return left.index - right.index; }); if (sortedFields.length) { // There was a best match return sortedFields[0]; } // No matches return null; }