diff --git a/src/actions/src/vis-state-actions.ts b/src/actions/src/vis-state-actions.ts index 8822da1c80..44fda1a67e 100644 --- a/src/actions/src/vis-state-actions.ts +++ b/src/actions/src/vis-state-actions.ts @@ -622,22 +622,23 @@ export function copyTableColumn( export type SetColumnDisplayFormatUpdaterAction = { dataId: string; - column: string; - displayFormat: string; + formats: { + [key: string]: string; + }; }; /** * Set column display format * @param dataId - * @param column - * @param displayFormat + * @param formats * @returns action * @public */ export function setColumnDisplayFormat( dataId: string, - column: string, - displayFormat: string + formats: { + [key: string]: string; + } ): Merge< SetColumnDisplayFormatUpdaterAction, {type: typeof ActionTypes.SET_COLUMN_DISPLAY_FORMAT} @@ -645,8 +646,7 @@ export function setColumnDisplayFormat( return { type: ActionTypes.SET_COLUMN_DISPLAY_FORMAT, dataId, - column, - displayFormat + formats }; } diff --git a/src/components/src/common/data-table/display-format.tsx b/src/components/src/common/data-table/display-format.tsx new file mode 100644 index 0000000000..a8832eef55 --- /dev/null +++ b/src/components/src/common/data-table/display-format.tsx @@ -0,0 +1,177 @@ +// Copyright (c) 2023 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 React, {useCallback, useState} from 'react'; +import styled from 'styled-components'; + +import {getFieldFormatLabels} from '@kepler.gl/utils'; +import {ALL_FIELD_TYPES, TooltipFormat} from '@kepler.gl/constants'; +import {ColMeta, ColMetaProps} from '@kepler.gl/types'; + +import {InputLight} from '../../common/styled-components'; +import {FormatterDropdown} from './option-dropdown'; + +const StyledConfigPanel = styled.div` + background-color: ${props => props.theme.headerCellBackground}; + box-shadow: 0 10px 18px 0 rgb(0 0 0 / 36%); + flex-grow: 1; +`; +const StyledConfigPanelContent = styled.div` + padding: 20px; + min-width: 230px; + max-height: 400px; + overflow: overlay; +`; + +const StyledTableConfigGroup = styled.div` + margin-bottom: 10px; + display: flex; + align-items: center; + + input { + cursor: pointer !important; + width: 184px; + height: 22px; + } +`; + +export type DataTableConfigProps = { + title: string; + id: string; + defaultFormat: string; + options: TooltipFormat[]; + columns: {name: string}[]; + colMeta: ColMeta; + setColumnDisplayFormat: (formats: {[key: string]: string}) => void; + onClose: () => void; +}; + +export const NumberFormatConfig: React.FC = ({ + title, + id, + defaultFormat, + options, + columns, + setColumnDisplayFormat, + onClose +}: DataTableConfigProps) => { + const [showFormatter, setShowFormatter] = useState(false); + const [format, setFormat] = useState(defaultFormat); + + const onSetDisplayFormat = useCallback( + (option: TooltipFormat) => { + setFormat(option.label); + const formats: {[key: string]: string} = columns.reduce((prev, col) => { + prev[col.name] = option.format; + return prev; + }, {}); + setColumnDisplayFormat(formats); + onClose(); + }, + [columns, setColumnDisplayFormat, onClose] + ); + + return ( + + setShowFormatter(true)} + /> + setShowFormatter(false)} + formatLabels={options} + /> + + ); +}; + +function DataTableConfigFactory() { + const getColumnsByFieldType = (columns: string[], colMeta: ColMeta, fieldType: string) => { + const result: ColMetaProps[] = []; + columns.forEach(colName => { + if (colMeta[colName]?.type === fieldType) { + result.push(colMeta[colName]); + } + }); + return result; + }; + + const DataTableConfig = ({columns, colMeta, setColumnDisplayFormat, onClose}) => { + const formatConfigs = [ + { + title: '# Set Integer Number Format', + id: 'input-iteger-format', + displayType: ALL_FIELD_TYPES.integer + }, + { + title: '# Set Float Number Format', + id: 'input-float-format', + displayType: ALL_FIELD_TYPES.real + }, + { + title: '# Set Timestamp Format', + id: 'input-datetime-format', + displayType: ALL_FIELD_TYPES.timestamp + }, + { + title: '# Set Date Format', + id: 'input-date-format', + displayType: ALL_FIELD_TYPES.date + }, + { + title: '# Set Boolean Format', + id: 'input-bool-format', + displayType: ALL_FIELD_TYPES.boolean + } + ]; + + return ( + + + {formatConfigs.map((config, index) => ( + + ))} + + + ); + }; + return DataTableConfig; +} + +export default DataTableConfigFactory; diff --git a/src/components/src/common/data-table/header-cell.tsx b/src/components/src/common/data-table/header-cell.tsx index 1326afae78..2cd5b52548 100644 --- a/src/components/src/common/data-table/header-cell.tsx +++ b/src/components/src/common/data-table/header-cell.tsx @@ -106,7 +106,7 @@ const HeaderCellFactory = (FieldToken: React.FC) => { sortTableColumn, pinTableColumn, copyTableColumn, - setDisplayFormat + setColumnDisplayFormat } = props; const [showFormatter, setShowFormatter] = useState(false); const column = columns[columnIndex]; @@ -125,9 +125,9 @@ const HeaderCellFactory = (FieldToken: React.FC) => { const onCopy = useCallback(() => copyTableColumn(column), [copyTableColumn, column]); const onSetDisplayFormat = useCallback( displayFormat => { - setDisplayFormat(column, displayFormat.format); + setColumnDisplayFormat({[column]: displayFormat.format}); }, - [column, setDisplayFormat] + [column, setColumnDisplayFormat] ); const onToggleDisplayFormat = useCallback(() => { @@ -172,7 +172,7 @@ const HeaderCellFactory = (FieldToken: React.FC) => { left={0} top={0} isOpened={isFormatted && showFormatter} - column={colMeta[column]} + displayFormat={colMeta[column].displayFormat} setDisplayFormat={onSetDisplayFormat} onClose={() => setShowFormatter(false)} formatLabels={formatLabels} diff --git a/src/components/src/common/data-table/index.tsx b/src/components/src/common/data-table/index.tsx index 19dbafe592..e7e244ac13 100644 --- a/src/components/src/common/data-table/index.tsx +++ b/src/components/src/common/data-table/index.tsx @@ -390,7 +390,7 @@ export interface DataTableProps { sortColumn: SortColumn; sortTableColumn: (column: string, mode?: string) => void; pinTableColumn: (column: string) => void; - setDisplayFormat: (column: string, displayFormat: string) => void; + setColumnDisplayFormat: (formats: {[key: string]: string}) => void; copyTableColumn: (column: string) => void; sortOrder?: number[] | null; showStats?: boolean; diff --git a/src/components/src/common/data-table/option-dropdown.tsx b/src/components/src/common/data-table/option-dropdown.tsx index f76b8d8c0e..7c20da97a7 100644 --- a/src/components/src/common/data-table/option-dropdown.tsx +++ b/src/components/src/common/data-table/option-dropdown.tsx @@ -24,7 +24,7 @@ import Portaled from '../portaled'; import DropdownList from '../item-selector/dropdown-list'; import {SORT_ORDER, TABLE_OPTION, TABLE_OPTION_LIST, TooltipFormat} from '@kepler.gl/constants'; import {getFieldFormatLabels} from '@kepler.gl/utils'; -import {ColMeta, ColMetaProps} from '@kepler.gl/types'; +import {ColMeta} from '@kepler.gl/types'; import {ArrowDown, ArrowUp, Clipboard, Pin, Cancel, Hash} from '../icons'; const ListItem = ({value}) => ( @@ -80,8 +80,8 @@ export type FormatterDropdownProps = { left: number; top: number; isOpened: boolean; - column: ColMetaProps; - setDisplayFormat: (displayFormat: string) => void; + displayFormat?: string; + setDisplayFormat: (displayFormat: TooltipFormat) => void; onClose: () => void; formatLabels: TooltipFormat[]; }; @@ -89,8 +89,16 @@ export type FormatterDropdownProps = { export const FormatterDropdown: React.FC = ( props: FormatterDropdownProps ) => { - const {left, top, isOpened, column, setDisplayFormat, onClose, formatLabels} = props; - const selectionIndex = formatLabels.findIndex(label => label.format === column.displayFormat); + const { + left, + top, + isOpened, + displayFormat = 'None', + setDisplayFormat, + onClose, + formatLabels + } = props; + const selectionIndex = formatLabels.findIndex(label => label.format === displayFormat); const onSelectDisplayFormat = useCallback( (result, e) => { @@ -123,7 +131,7 @@ interface OptionDropdownProps { sortTableColumn: (sort: string) => void; pinTableColumn: () => void; copyTableColumn: () => void; - setDisplayFormat: (displayFormat: string) => void; + setDisplayFormat: (displayFormat: any) => void; sortMode?: string; isSorted?: string; isPinned?: boolean; @@ -214,7 +222,7 @@ const OptionDropdown = (props: OptionDropdownProps) => { top={-10} isOpened={Boolean(isOpened && showFormatter)} formatLabels={formatLabels} - column={colMeta[column]} + displayFormat={colMeta[column]?.displayFormat} setDisplayFormat={setDisplayFormat} onClose={onClose} /> diff --git a/src/components/src/common/field-selector.tsx b/src/components/src/common/field-selector.tsx index a435925d93..b42cebb1b6 100644 --- a/src/components/src/common/field-selector.tsx +++ b/src/components/src/common/field-selector.tsx @@ -107,7 +107,7 @@ interface FieldSelectorFactoryProps { closeOnSelect?: boolean; showToken?: boolean; suggested?: ReadonlyArray | null; - CustomChickletComponent?: ComponentType; + CustomChickletComponent?: ComponentType; size?: string; } diff --git a/src/components/src/index.ts b/src/components/src/index.ts index f7dc6b915c..e49dd1da63 100644 --- a/src/components/src/index.ts +++ b/src/components/src/index.ts @@ -222,6 +222,7 @@ export {default as SliderHandle} from './common/slider/slider-handle'; export {default as SliderBarHandle} from './common/slider/slider-bar-handle'; export {default as ActionPanel, ActionPanelItem} from './common/action-panel'; export {default as HeaderCellFactory} from './common/data-table/header-cell'; +export {default as DataTableConfigFactory, NumberFormatConfig} from './common/data-table/display-format'; export {default as DataTableFactory} from './common/data-table'; export {default as CanvasHack} from './common/data-table/canvas'; export {default as OptionDropdown, FormatterDropdown} from './common/data-table/option-dropdown'; diff --git a/src/components/src/map/layer-hover-info.tsx b/src/components/src/map/layer-hover-info.tsx index 475fcff5e2..fcabd0333f 100644 --- a/src/components/src/map/layer-hover-info.tsx +++ b/src/components/src/map/layer-hover-info.tsx @@ -133,14 +133,15 @@ const EntryInfoRow = ({item, fields, data, primaryData, compareType}) => { const value = data.valueAt(fieldIdx); const displayValue = getTooltipDisplayValue({item, field, value}); - const displayDeltaValue = getTooltipDisplayDeltaValue({ - item, - field, - data, - fieldIdx, - primaryData, - compareType - }); + const displayDeltaValue = primaryData + ? getTooltipDisplayDeltaValue({ + field, + data, + fieldIdx, + primaryData, + compareType + }) + : null; return ( ` } `; +const StyledConfigureButton = styled.div` + display: flex; + justify-content: flex-end; + position: absolute; + top: 24px; + right: 48px; + svg { + stroke: black; + } +`; + interface DatasetTabsUnmemoizedProps { activeDataset: KeplerTable; datasets: Datasets; @@ -97,7 +111,7 @@ export const DatasetTabs = React.memo(DatasetTabsUnmemoized); DatasetTabs.displayName = 'DatasetTabs'; -DataTableModalFactory.deps = [DataTableFactory]; +DataTableModalFactory.deps = [DataTableFactory, DataTableConfigFactory]; const TableContainer = styled.div` display: flex; @@ -116,13 +130,20 @@ interface DataTableModalProps { datasets: Datasets; showDatasetTable: (id: string) => void; showTab?: boolean; - setColumnDisplayFormat: (dataId: string, column: string, displayFormat: string) => void; + setColumnDisplayFormat: ( + dataId: string, + formats: { + column: string; + displayFormat: string; + } + ) => void; uiStateActions: typeof UIStateActions; uiState: UiState; } function DataTableModalFactory( - DataTable: ReturnType + DataTable: ReturnType, + DataTableConfig: ReturnType ): React.ComponentType> { class DataTableModal extends React.Component { state = { @@ -211,9 +232,9 @@ function DataTableModalFactory( sortTableColumn(dataId, column, mode); }; - setDisplayFormat = (column, displayFormat) => { + setColumnDisplayFormat = formats => { const {dataId, setColumnDisplayFormat} = this.props; - if (dataId) setColumnDisplayFormat(dataId, column, displayFormat); + if (dataId) setColumnDisplayFormat(dataId, formats); }; onOpenConfig = () => { @@ -245,6 +266,22 @@ function DataTableModalFactory( showDatasetTable={showDatasetTable} /> ) : null} + + + + + + {datasets[dataId] ? ( ) : null} diff --git a/src/components/src/side-panel/interaction-manager.tsx b/src/components/src/side-panel/interaction-manager.tsx index eb78b1f2fe..280e04d96e 100644 --- a/src/components/src/side-panel/interaction-manager.tsx +++ b/src/components/src/side-panel/interaction-manager.tsx @@ -49,7 +49,7 @@ function InteractionManagerFactory( visStateActions, panelMetadata }) => { - const {interactionConfigChange: onConfigChange} = visStateActions; + const {interactionConfigChange: onConfigChange, setColumnDisplayFormat} = visStateActions; const intl = useIntl(); return (
@@ -63,6 +63,7 @@ function InteractionManagerFactory( config={interactionConfig[key]} key={key} onConfigChange={onConfigChange} + setColumnDisplayFormat={setColumnDisplayFormat} /> ))}
diff --git a/src/components/src/side-panel/interaction-panel/interaction-panel.tsx b/src/components/src/side-panel/interaction-panel/interaction-panel.tsx index 6193f6aa16..cb685c2179 100644 --- a/src/components/src/side-panel/interaction-panel/interaction-panel.tsx +++ b/src/components/src/side-panel/interaction-panel/interaction-panel.tsx @@ -26,6 +26,10 @@ import BrushConfigFactory from './brush-config'; import TooltipConfigFactory from './tooltip-config'; import {Datasets} from '@kepler.gl/table'; import {InteractionConfig, ValueOf} from '@kepler.gl/types'; +import { + setColumnDisplayFormat as setColumnDisplayFormatAction, + ActionHandler +} from '@kepler.gl/actions'; import { StyledPanelHeader, @@ -44,6 +48,7 @@ interface InteractionPanelProps { interactionConfigIcons?: { [key: string]: React.ElementType; }; + setColumnDisplayFormat: ActionHandler; } const StyledInteractionPanel = styled.div` @@ -68,6 +73,7 @@ function InteractionPanelFactory( config, onConfigChange, datasets, + setColumnDisplayFormat, interactionConfigIcons = INTERACTION_CONFIG_ICONS }) => { const [isConfigActive, setIsConfigAction] = useState(false); @@ -82,6 +88,13 @@ function InteractionPanelFactory( [onConfigChange, config] ); + const onDisplayFormatChange = useCallback( + (dataId, column, displayFormat) => { + setColumnDisplayFormat(dataId, {[column]: displayFormat}); + }, + [setColumnDisplayFormat] + ); + const togglePanelActive = useCallback(() => { setIsConfigAction(!isConfigActive); }, [setIsConfigAction, isConfigActive]); @@ -92,13 +105,21 @@ function InteractionPanelFactory( }, [_updateConfig, enabled]); const onChange = useCallback(newConfig => _updateConfig({config: newConfig}), [_updateConfig]); + const IconComponent = interactionConfigIcons[config.id]; let template: ReactElement | null = null; switch (config.id) { case 'tooltip': - template = ; + template = ( + + ); break; case 'brush': template = ; diff --git a/src/components/src/side-panel/interaction-panel/tooltip-config.tsx b/src/components/src/side-panel/interaction-panel/tooltip-config.tsx index 34f4d72cf1..5d10e971c9 100644 --- a/src/components/src/side-panel/interaction-panel/tooltip-config.tsx +++ b/src/components/src/side-panel/interaction-panel/tooltip-config.tsx @@ -85,6 +85,7 @@ type TooltipConfigProps = { }) => void; datasets: Datasets; intl: IntlShape; + onDisplayFormatChange: (dataId, column, displayFormat) => void; }; type DatasetTooltipConfigProps = { @@ -103,6 +104,7 @@ type DatasetTooltipConfigProps = { compareType: string | null; }) => void; dataset: KeplerTable; + onDisplayFormatChange: (dataId, column, displayFormat) => void; }; TooltipConfigFactory.deps = [DatasetTagFactory, FieldSelectorFactory]; @@ -110,7 +112,12 @@ function TooltipConfigFactory( DatasetTag: ReturnType, FieldSelector: ReturnType ) { - const DatasetTooltipConfig = ({config, onChange, dataset}: DatasetTooltipConfigProps) => { + const DatasetTooltipConfig = ({ + config, + onChange, + dataset, + onDisplayFormatChange + }: DatasetTooltipConfigProps) => { const dataId = dataset.id; return ( @@ -164,14 +171,25 @@ function TooltipConfigFactory( closeOnSelect={false} multiSelect inputTheme="secondary" - // @ts-expect-error - CustomChickletComponent={TooltipChickletFactory(dataId, config, onChange, dataset.fields)} + CustomChickletComponent={TooltipChickletFactory( + dataId, + config, + onChange, + dataset.fields, + onDisplayFormatChange + )} /> ); }; - const TooltipConfig = ({config, datasets, onChange, intl}: TooltipConfigProps) => { + const TooltipConfig = ({ + config, + datasets, + onChange, + onDisplayFormatChange, + intl + }: TooltipConfigProps) => { return ( {Object.keys(config.fieldsToShow).map(dataId => @@ -181,6 +199,7 @@ function TooltipConfigFactory( config={config} onChange={onChange} dataset={datasets[dataId]} + onDisplayFormatChange={onDisplayFormatChange} /> ) )} diff --git a/src/components/src/side-panel/interaction-panel/tooltip-config/tooltip-chicklet.tsx b/src/components/src/side-panel/interaction-panel/tooltip-config/tooltip-chicklet.tsx index 8ba0b38b94..5972127a3c 100644 --- a/src/components/src/side-panel/interaction-panel/tooltip-config/tooltip-chicklet.tsx +++ b/src/components/src/side-panel/interaction-panel/tooltip-config/tooltip-chicklet.tsx @@ -41,7 +41,7 @@ type TooltipConfig = { [key: string]: {name: string; format: string | null}[]; }; compareMode: boolean; - compareType: string[]; + compareType: string | null; }; type IconDivProps = { @@ -95,7 +95,8 @@ function TooltipChickletFactory( dataId: string, config: TooltipConfig, onChange: (cfg: TooltipConfig) => void, - fields: TooltipFields[] + fields: TooltipFields[], + onDisplayFormatChange ): ComponentType { class TooltipChicklet extends Component { state = { @@ -126,10 +127,14 @@ function TooltipChickletFactory( if (!tooltipField) { return null; } + const field = fields.find(f => f.name === tooltipField.name); + if (!field) { + return null; + } const formatLabels = getFormatLabels(fields, tooltipField.name); - const hasFormat = Boolean(tooltipField.format); + const hasFormat = Boolean(field.displayFormat); const selectionIndex = formatLabels.findIndex( - fl => getFormatValue(fl) === tooltipField.format + fl => getFormatValue(fl) === field.displayFormat ); const hashStyle = show ? hashStyles.SHOW : hasFormat ? hashStyles.ACTIVE : null; @@ -143,7 +148,7 @@ function TooltipChickletFactory( render={() => ( {hasFormat ? ( - getFormatTooltip(formatLabels, tooltipField.format) + getFormatTooltip(formatLabels, field.displayName) ) : ( )} @@ -174,12 +179,13 @@ function TooltipChickletFactory( show: false }); + const displayFormat = getFormatValue(result); const oldFieldsToShow = config.fieldsToShow[dataId]; const fieldsToShow = oldFieldsToShow.map(fieldToShow => { return fieldToShow.name === tooltipField.name ? { name: tooltipField.name, - format: getFormatValue(result) + format: displayFormat } : fieldToShow; }); @@ -191,6 +197,7 @@ function TooltipChickletFactory( } }; onChange(newConfig); + onDisplayFormatChange(dataId, field.name, displayFormat); }} /> diff --git a/src/reducers/src/interaction-utils.ts b/src/reducers/src/interaction-utils.ts index dd31813dcb..cfbf83e6eb 100644 --- a/src/reducers/src/interaction-utils.ts +++ b/src/reducers/src/interaction-utils.ts @@ -103,18 +103,13 @@ function _mergeFieldPairs(pairs) { return pairs.reduce((prev, pair) => [...prev, ...pair], []); } -/** - * @type {typeof import('./interaction-utils').getTooltipDisplayDeltaValue} - */ export function getTooltipDisplayDeltaValue({ primaryData, field, compareType, data, - fieldIdx, - item + fieldIdx }: { - item: TooltipField; field: Field; data: DataRow; fieldIdx: number; @@ -135,7 +130,7 @@ export function getTooltipDisplayDeltaValue({ const deltaFormat = compareType === COMPARE_TYPES.RELATIVE ? TOOLTIP_FORMATS.DECIMAL_PERCENT_FULL_2[TOOLTIP_KEY] - : item.format || TOOLTIP_FORMATS.DECIMAL_DECIMAL_FIXED_3[TOOLTIP_KEY]; + : field.displayFormat || TOOLTIP_FORMATS.DECIMAL_DECIMAL_FIXED_3[TOOLTIP_KEY]; displayDeltaValue = getFormatter(deltaFormat, field)(deltaValue); @@ -168,6 +163,8 @@ export function getTooltipDisplayValue({ } return item?.format - ? getFormatter(item.format, field)(value) + ? getFormatter(item?.format, field)(value) + : field.displayFormat + ? getFormatter(field.displayFormat, field)(value) : parseFieldValue(value, field.type); } diff --git a/src/reducers/src/vis-state-updaters.ts b/src/reducers/src/vis-state-updaters.ts index fda7c5c125..2647affce6 100644 --- a/src/reducers/src/vis-state-updaters.ts +++ b/src/reducers/src/vis-state-updaters.ts @@ -2503,27 +2503,30 @@ export function copyTableColumnUpdater( } /** - * Set column display format from user selection + * Set display format from columns from user selection * @memberof visStateUpdaters * @public */ export function setColumnDisplayFormatUpdater( state: VisState, - {dataId, column, displayFormat}: VisStateActions.SetColumnDisplayFormatUpdaterAction + {dataId, formats}: VisStateActions.SetColumnDisplayFormatUpdaterAction ): VisState { const dataset = state.datasets[dataId]; if (!dataset) { return state; } - const fieldIdx = dataset.fields.findIndex(f => f.name === column); - if (fieldIdx < 0) { - return state; - } - const field = dataset.fields[fieldIdx]; + let newFields = dataset.fields; + Object.keys(formats).forEach(column => { + const fieldIdx = dataset.fields.findIndex(f => f.name === column); + if (fieldIdx >= 0) { + const displayFormat = formats[column]; + const field = newFields[fieldIdx]; + newFields = swap_(merge_({displayFormat})(field) as {id: string})( + newFields as {id: string}[] + ) as Field[]; + } + }); - const newFields = swap_(merge_({displayFormat})(field) as {id: string})( - dataset.fields as {id: string}[] - ); const newDataset = copyTableAndUpdate(dataset, {fields: newFields as Field[]}); return pick_('datasets')(merge_({[dataId]: newDataset}))(state); } diff --git a/src/types/components.d.ts b/src/types/components.d.ts index 8d935dc615..0e01f54095 100644 --- a/src/types/components.d.ts +++ b/src/types/components.d.ts @@ -11,6 +11,8 @@ type TooltipFields = { name?: string; fieldIdx?: number; type?: string; + displayFormat?: string; + displayName: string; }; export type ColMetaProps = { diff --git a/test/browser/components/modals/data-table-modal-test.js b/test/browser/components/modals/data-table-modal-test.js index 134c93e6ee..3d816bba93 100644 --- a/test/browser/components/modals/data-table-modal-test.js +++ b/test/browser/components/modals/data-table-modal-test.js @@ -23,7 +23,7 @@ import test from 'tape-catch'; import global from 'global'; import sinon from 'sinon'; import flatten from 'lodash.flattendeep'; -import {mountWithTheme} from 'test/helpers/component-utils'; +import {IntlWrapper, mountWithTheme} from 'test/helpers/component-utils'; import CloneDeep from 'lodash.clonedeep'; import {VisStateActions} from '@kepler.gl/actions'; import {visStateReducer} from '@kepler.gl/reducers'; @@ -37,13 +37,17 @@ import { DatasetModalTab, DataTableFactory, OptionDropdown, - appInjector + appInjector, + DropdownList, + InputLight, + DataTableConfigFactory, + NumberFormatConfig } from '@kepler.gl/components'; import {testFields, testAllData} from 'test/fixtures/test-csv-data'; import {geoStyleFields, geoStyleRows} from 'test/fixtures/geojson'; import {StateWFiles, testCsvDataId, testGeoJsonDataId} from 'test/helpers/mock-state'; -import {createDataContainer} from '@kepler.gl/utils'; +import {createDataContainer, getFieldFormatLabels} from '@kepler.gl/utils'; const {VertThreeDots} = Icons; const DataTableModal = appInjector.get(DataTableModalFactory); @@ -130,6 +134,100 @@ function restoreMockCanvas() { global.window.HTMLCanvasElement.prototype.getContext = oldGetContext; } +// eslint-disable-next-line max-statements +test('Compnents -> DataTableConfig', t => { + const expectedColumns = ['gps_data.utc_timestamp']; + + const expectedColMeta = { + 'gps_data.utc_timestamp': { + name: 'gps_data.utc_timestamp', + type: 'timestamp', + format: 'YYYY-M-D H:m:s' + } + }; + + const columns = expectedColumns; + const colMeta = expectedColMeta; + const setColumnDisplayFormat = sinon.spy(); + const onClose = sinon.spy(); + + const DataTableConfig = DataTableConfigFactory(); + let wrapper; + t.doesNotThrow(() => { + wrapper = mountWithTheme( + + + + ); + }, 'Show not fail without props'); + + const numberFormatConfigInput = wrapper.find(NumberFormatConfig); + t.equal(numberFormatConfigInput.length, 5, 'should render 5 NumberFormatConfig'); + + numberFormatConfigInput + .at(0) + .find(InputLight) + .simulate('click'); + + const formatDropdown = wrapper.find(DropdownList); + t.equal(formatDropdown.length, 1, 'should render 1 format dropdown'); + + const integerDisplayOptions = formatDropdown.at(0).props().options; + + t.deepEqual( + integerDisplayOptions, + getFieldFormatLabels('integer'), + 'should render integer type formats' + ); + + numberFormatConfigInput + .at(1) + .find(InputLight) + .simulate('click'); + + const floatDisplayOptions = wrapper + .find(DropdownList) + .at(1) + .props().options; + + t.deepEqual(floatDisplayOptions, getFieldFormatLabels('real'), 'should render real type formats'); + + numberFormatConfigInput + .at(2) + .find(InputLight) + .simulate('click'); + + const timeDisplayOptions = wrapper + .find(DropdownList) + .at(2) + .props().options; + + t.deepEqual( + timeDisplayOptions, + getFieldFormatLabels('timestamp'), + 'should render time type formats' + ); + + numberFormatConfigInput + .at(3) + .find(InputLight) + .simulate('click'); + + const dateDisplayOptions = wrapper + .find(DropdownList) + .at(3) + .props().options; + + t.deepEqual(dateDisplayOptions, getFieldFormatLabels('date'), 'should render date type formats'); + + t.end(); +}); + /* eslint-disable max-statements */ test('Components -> DataTableModal.render: csv 1', t => { t.doesNotThrow(() => { @@ -377,13 +475,12 @@ test('Components -> DataTableModal -> render DataTable: sort, pin and display fo const initialState = CloneDeep(StateWFiles.visState); // set display format + const formats = { + 'gps_data.lat': TOOLTIP_FORMATS.DECIMAL_DECIMAL_FIXED_2.format + }; const nextState = visStateReducer( initialState, - VisStateActions.setColumnDisplayFormat( - testCsvDataId, - 'gps_data.lat', - TOOLTIP_FORMATS.DECIMAL_DECIMAL_FIXED_2.format - ) + VisStateActions.setColumnDisplayFormat(testCsvDataId, formats) ); // sort column @@ -497,13 +594,13 @@ test('Components -> DatableModal -> sort/pin/copy and display format should be c sortTableColumn: testSortColumn, pinTableColumn: testPinColumn, copyTableColumn: testCopyColumn, - setDisplayFormat: testSetDisplayFormat + setColumnDisplayFormat: testSetColumnDisplayFormat } = wrapper.find('DataTable').props(); testSortColumn(column); testPinColumn(column); testCopyColumn(column); - testSetDisplayFormat(column, TOOLTIP_FORMATS.DECIMAL_DECIMAL_FIXED_2.format); + testSetColumnDisplayFormat({[column]: TOOLTIP_FORMATS.DECIMAL_DECIMAL_FIXED_2.format}); t.equal( sortTableColumn.calledWith(testCsvDataId, column), @@ -524,13 +621,11 @@ test('Components -> DatableModal -> sort/pin/copy and display format should be c ); t.equal( - setColumnDisplayFormat.calledWith( - testCsvDataId, - column, - TOOLTIP_FORMATS.DECIMAL_DECIMAL_FIXED_2.format - ), + setColumnDisplayFormat.calledWith(testCsvDataId, { + [column]: TOOLTIP_FORMATS.DECIMAL_DECIMAL_FIXED_2.format + }), true, - 'should call setDisplayFormat with dataId, column gps_data.lat, and format' + 'should call setColumnDisplayFormat with dataId, column gps_data.lat, and format' ); t.end(); diff --git a/test/browser/components/tooltip-config-test.js b/test/browser/components/tooltip-config-test.js index 6c98a30529..8c9e5ffb80 100644 --- a/test/browser/components/tooltip-config-test.js +++ b/test/browser/components/tooltip-config-test.js @@ -200,12 +200,19 @@ test('TooltipConfig - render -> tooltip format', t => { const tooltipConfig = StateWFiles.visState.interactionConfig.tooltip.config; const onChange = sinon.spy(); + const onDisplayFormatChange = sinon.spy(); + let wrapper; t.doesNotThrow(() => { wrapper = mountWithTheme( - + ); }, 'Should render'); diff --git a/test/node/utils/interaction-utils-test.js b/test/node/utils/interaction-utils-test.js index 5f13d21e1e..86beb202fd 100644 --- a/test/node/utils/interaction-utils-test.js +++ b/test/node/utils/interaction-utils-test.js @@ -140,7 +140,8 @@ test('interactionUtil -> getTooltipDisplayDeltaValue', t => { { input: { primaryData: dataset.dataContainer.row(0), - field: dataset.fields[testFieldIdx], + // field.displayFormat has been used to replace tooltipConfig.format + field: {...dataset.fields[testFieldIdx], displayFormat: item.format}, compareType: COMPARE_TYPES.ABSOLUTE, data: dataset.dataContainer.row(1), fieldIdx: testFieldIdx, @@ -152,7 +153,7 @@ test('interactionUtil -> getTooltipDisplayDeltaValue', t => { { input: { primaryData: dataset.dataContainer.row(0), - field: dataset.fields[testFieldIdx], + field: {...dataset.fields[testFieldIdx], displayFormat: item.format}, compareType: COMPARE_TYPES.RELATIVE, data: dataset.dataContainer.row(1), fieldIdx: testFieldIdx, @@ -164,7 +165,7 @@ test('interactionUtil -> getTooltipDisplayDeltaValue', t => { { input: { primaryData: dataset.dataContainer.row(3), - field: dataset.fields[testFieldIdx], + field: {...dataset.fields[testFieldIdx], displayFormat: item.format}, compareType: COMPARE_TYPES.ABSOLUTE, data: dataset.dataContainer.row(1), fieldIdx: testFieldIdx, @@ -176,7 +177,7 @@ test('interactionUtil -> getTooltipDisplayDeltaValue', t => { { input: { primaryData: dataset.dataContainer.row(0), - field: dataset.fields[testFieldIdx], + field: {...dataset.fields[testFieldIdx], displayFormat: item.format}, compareType: COMPARE_TYPES.ABSOLUTE, data: dataset.dataContainer.row(3), fieldIdx: testFieldIdx, @@ -188,7 +189,7 @@ test('interactionUtil -> getTooltipDisplayDeltaValue', t => { { input: { primaryData: dataset.dataContainer.row(4), - field: dataset.fields[testFieldIdx], + field: {...dataset.fields[testFieldIdx], displayFormat: item.format}, compareType: COMPARE_TYPES.ABSOLUTE, data: dataset.dataContainer.row(3), fieldIdx: testFieldIdx, @@ -230,7 +231,11 @@ test('interactionUtil -> getTooltipDisplayValue', t => { ]; TEST_CASES.forEach(tc => { - const field = dataset.fields.find(f => f.name === tc.input.name); + // field.displayFormat has been used to replace tooltipConfig.format + const field = { + ...dataset.fields.find(f => f.name === tc.input.name), + displayFormat: tc.input.format + }; const fieldIdx = dataset.fields.findIndex(f => f.name === tc.input.name); t.deepEqual(