feat(explorer): Add configs and formatting to discrete comparison columns (#29553)

This commit is contained in:
Ross Mabbett 2024-07-25 18:43:56 -04:00 committed by GitHub
parent 72caec10fe
commit dac69e2092
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 365 additions and 17 deletions

View File

@ -18,7 +18,6 @@
* under the License.
*/
import {
ChartDataResponseResult,
ensureIsArray,
GenericDataType,
isAdhocColumn,
@ -145,6 +144,21 @@ const percentMetricsControl: typeof sharedControls.metrics = {
validators: [],
};
/**
* Generate comparison column names for a given column.
*/
const generateComparisonColumns = (colname: string) => [
`${t('Main')} ${colname}`,
`# ${colname}`,
`${colname}`,
`% ${colname}`,
];
/**
* Generate column types for the comparison columns.
*/
const generateComparisonColumnTypes = (count: number) =>
Array(count).fill(GenericDataType.Numeric);
const processComparisonColumns = (columns: any[], suffix: string) =>
columns
.map(col => {
@ -470,10 +484,37 @@ const config: ControlPanelConfig = {
return true;
},
mapStateToProps(explore, _, chart) {
const timeComparisonStatus =
!!explore?.controls?.time_compare?.value;
const { colnames: _colnames, coltypes: _coltypes } =
chart?.queriesResponse?.[0] ?? {};
let colnames: string[] = _colnames || [];
let coltypes: GenericDataType[] = _coltypes || [];
if (timeComparisonStatus) {
/**
* Replace numeric columns with sets of comparison columns.
*/
const updatedColnames: string[] = [];
const updatedColtypes: GenericDataType[] = [];
colnames.forEach((colname, index) => {
if (coltypes[index] === GenericDataType.Numeric) {
updatedColnames.push(
...generateComparisonColumns(colname),
);
updatedColtypes.push(...generateComparisonColumnTypes(4));
} else {
updatedColnames.push(colname);
updatedColtypes.push(coltypes[index]);
}
});
colnames = updatedColnames;
coltypes = updatedColtypes;
}
return {
queryResponse: chart?.queriesResponse?.[0] as
| ChartDataResponseResult
| undefined,
columnsPropsObject: { colnames, coltypes },
};
},
},

View File

@ -20,6 +20,7 @@ import memoizeOne from 'memoize-one';
import {
ComparisonType,
CurrencyFormatter,
Currency,
DataRecord,
ensureIsArray,
extractTimegrain,
@ -53,6 +54,7 @@ import {
DataColumnMeta,
TableChartProps,
TableChartTransformedProps,
TableColumnConfig,
} from './types';
const { PERCENT_3_POINT } = NumberFormats;
@ -293,6 +295,48 @@ const processColumns = memoizeOne(function processColumns(
];
}, isEqualColumns);
const getComparisonColConfig = (
label: string,
parentColKey: string,
columnConfig: Record<string, TableColumnConfig>,
) => {
const comparisonKey = `${label} ${parentColKey}`;
const comparisonColConfig = columnConfig[comparisonKey] || {};
return comparisonColConfig;
};
const getComparisonColFormatter = (
label: string,
parentCol: DataColumnMeta,
columnConfig: Record<string, TableColumnConfig>,
savedFormat: string | undefined,
savedCurrency: Currency | undefined,
) => {
const currentColConfig = getComparisonColConfig(
label,
parentCol.key,
columnConfig,
);
const hasCurrency = currentColConfig.currencyFormat?.symbol;
const currentColNumberFormat =
// fallback to parent's number format if not set
currentColConfig.d3NumberFormat || parentCol.config?.d3NumberFormat;
let { formatter } = parentCol;
if (label === '%') {
formatter = getNumberFormatter(currentColNumberFormat || PERCENT_3_POINT);
} else if (currentColNumberFormat || hasCurrency) {
const currency = currentColConfig.currencyFormat || savedCurrency;
const numberFormat = currentColNumberFormat || savedFormat;
formatter = currency
? new CurrencyFormatter({
d3Format: numberFormat,
currency,
})
: getNumberFormatter(numberFormat);
}
return formatter;
};
const processComparisonColumns = (
columns: DataColumnMeta[],
props: TableChartProps,
@ -301,12 +345,11 @@ const processComparisonColumns = (
columns
.map(col => {
const {
datasource: { columnFormats },
datasource: { columnFormats, currencyFormats },
rawFormData: { column_config: columnConfig = {} },
} = props;
const config = columnConfig[col.key] || {};
const savedFormat = columnFormats?.[col.key];
const numberFormat = config.d3NumberFormat || savedFormat;
const savedCurrency = currencyFormats?.[col.key];
if (
(col.isMetric || col.isPercentMetric) &&
!col.key.includes(comparisonSuffix) &&
@ -317,22 +360,53 @@ const processComparisonColumns = (
...col,
label: t('Main'),
key: `${t('Main')} ${col.key}`,
config: getComparisonColConfig(t('Main'), col.key, columnConfig),
formatter: getComparisonColFormatter(
t('Main'),
col,
columnConfig,
savedFormat,
savedCurrency,
),
},
{
...col,
label: `#`,
key: `# ${col.key}`,
config: getComparisonColConfig(`#`, col.key, columnConfig),
formatter: getComparisonColFormatter(
`#`,
col,
columnConfig,
savedFormat,
savedCurrency,
),
},
{
...col,
label: ``,
key: `${col.key}`,
config: getComparisonColConfig(``, col.key, columnConfig),
formatter: getComparisonColFormatter(
``,
col,
columnConfig,
savedFormat,
savedCurrency,
),
},
{
...col,
formatter: getNumberFormatter(numberFormat || PERCENT_3_POINT),
label: `%`,
key: `% ${col.key}`,
config: getComparisonColConfig(`%`, col.key, columnConfig),
formatter: getComparisonColFormatter(
`%`,
col,
columnConfig,
savedFormat,
savedCurrency,
),
},
];
}

View File

@ -65,6 +65,116 @@ describe('plugin-chart-table', () => {
expect(String(parsedDate)).toBe('2020-01-01 12:34:56');
expect(parsedDate.getTime()).toBe(1577882096000);
});
it('should process comparison columns when time_compare and comparison_type are set', () => {
const transformedProps = transformProps(testData.comparison);
// Check if comparison columns are processed
const comparisonColumns = transformedProps.columns.filter(
col =>
col.label === 'Main' ||
col.label === '#' ||
col.label === '△' ||
col.label === '%',
);
expect(comparisonColumns.length).toBeGreaterThan(0);
expect(comparisonColumns.some(col => col.label === 'Main')).toBe(true);
expect(comparisonColumns.some(col => col.label === '#')).toBe(true);
expect(comparisonColumns.some(col => col.label === '△')).toBe(true);
expect(comparisonColumns.some(col => col.label === '%')).toBe(true);
});
it('should not process comparison columns when time_compare is empty', () => {
const propsWithoutTimeCompare = {
...testData.comparison,
rawFormData: {
...testData.comparison.rawFormData,
time_compare: [],
},
};
const transformedProps = transformProps(propsWithoutTimeCompare);
// Check if comparison columns are not processed
const comparisonColumns = transformedProps.columns.filter(
col =>
col.label === 'Main' ||
col.label === '#' ||
col.label === '△' ||
col.label === '%',
);
expect(comparisonColumns.length).toBe(0);
});
it('should correctly apply column configuration for comparison columns', () => {
const transformedProps = transformProps(testData.comparisonWithConfig);
const comparisonColumns = transformedProps.columns.filter(
col =>
col.key.startsWith('Main') ||
col.key.startsWith('#') ||
col.key.startsWith('△') ||
col.key.startsWith('%'),
);
expect(comparisonColumns).toHaveLength(4);
const mainMetricConfig = comparisonColumns.find(
col => col.key === 'Main metric_1',
);
expect(mainMetricConfig).toBeDefined();
expect(mainMetricConfig?.config).toEqual({ d3NumberFormat: '.2f' });
const hashMetricConfig = comparisonColumns.find(
col => col.key === '# metric_1',
);
expect(hashMetricConfig).toBeDefined();
expect(hashMetricConfig?.config).toEqual({ d3NumberFormat: '.1f' });
const deltaMetricConfig = comparisonColumns.find(
col => col.key === '△ metric_1',
);
expect(deltaMetricConfig).toBeDefined();
expect(deltaMetricConfig?.config).toEqual({ d3NumberFormat: '.0f' });
const percentMetricConfig = comparisonColumns.find(
col => col.key === '% metric_1',
);
expect(percentMetricConfig).toBeDefined();
expect(percentMetricConfig?.config).toEqual({ d3NumberFormat: '.3f' });
});
it('should correctly format comparison columns using getComparisonColFormatter', () => {
const transformedProps = transformProps(testData.comparisonWithConfig);
const comparisonColumns = transformedProps.columns.filter(
col =>
col.key.startsWith('Main') ||
col.key.startsWith('#') ||
col.key.startsWith('△') ||
col.key.startsWith('%'),
);
const formattedMainMetric = comparisonColumns
.find(col => col.key === 'Main metric_1')
?.formatter?.(12345.678);
expect(formattedMainMetric).toBe('12345.68');
const formattedHashMetric = comparisonColumns
.find(col => col.key === '# metric_1')
?.formatter?.(12345.678);
expect(formattedHashMetric).toBe('12345.7');
const formattedDeltaMetric = comparisonColumns
.find(col => col.key === '△ metric_1')
?.formatter?.(12345.678);
expect(formattedDeltaMetric).toBe('12346');
const formattedPercentMetric = comparisonColumns
.find(col => col.key === '% metric_1')
?.formatter?.(0.123456);
expect(formattedPercentMetric).toBe('0.123');
});
});
describe('TableChart', () => {

View File

@ -23,6 +23,7 @@ import {
GenericDataType,
QueryMode,
supersetTheme,
ComparisonType,
} from '@superset-ui/core';
import { TableChartProps, TableChartFormData } from '../src/types';
@ -175,6 +176,131 @@ const advanced: TableChartProps = {
],
};
const comparison: TableChartProps = {
...basic,
rawFormData: {
...basicFormData,
table_timestamp_format: 'smart_date',
metrics: ['metric_1', 'metric_2'],
percent_metrics: ['percent_metric_1'],
column_config: {},
align_pn: true,
color_pn: true,
show_cell_bars: true,
include_search: true,
page_length: 10,
server_pagination: false,
order_desc: false,
query_mode: QueryMode.Aggregate,
show_totals: true,
conditional_formatting: [],
allow_rearrange_columns: true,
allow_render_html: false,
time_compare: ['P1D'],
comparison_color_enabled: true,
comparison_color_scheme: 'Green',
comparison_type: ComparisonType.Values,
},
queriesData: [
{
...basicQueryResult,
data: [
{
metric_1: 100,
metric_2: 200,
percent_metric_1: 0.5,
date: '2023-01-01',
},
{
metric_1: 110,
metric_2: 210,
percent_metric_1: 0.55,
date: '2023-01-02',
},
],
colnames: ['metric_1', 'metric_2', 'percent_metric_1', 'date'],
coltypes: [
GenericDataType.Numeric,
GenericDataType.Numeric,
GenericDataType.Numeric,
GenericDataType.Temporal,
],
},
{
...basicQueryResult,
data: [
{
metric_1: 10,
metric_2: 20,
percent_metric_1: 0.05,
date: '2023-01-01',
},
{
metric_1: 11,
metric_2: 21,
percent_metric_1: 0.055,
date: '2023-01-02',
},
],
},
],
filterState: { filters: {} },
ownState: {},
hooks: {
onAddFilter: jest.fn(),
setDataMask: jest.fn(),
onContextMenu: jest.fn(),
},
emitCrossFilters: true,
};
const comparisonWithConfig: TableChartProps = {
...comparison,
height: 400,
width: 400,
rawFormData: {
...comparison.rawFormData,
table_timestamp_format: 'smart_date',
metrics: ['metric_1'],
percent_metrics: ['percent_metric_1'],
column_config: {
'Main metric_1': { d3NumberFormat: '.2f' },
'# metric_1': { d3NumberFormat: '.1f' },
'△ metric_1': { d3NumberFormat: '.0f' },
'% metric_1': { d3NumberFormat: '.3f' },
},
time_compare: ['1 year ago'],
comparison_color_enabled: true,
comparison_type: ComparisonType.Values,
},
datasource: {
...comparison.datasource,
columnFormats: { metric_1: '.2f' },
currencyFormats: {},
verboseMap: { metric_1: 'Metric 1' },
},
queriesData: [
{
...basicQueryResult,
data: [{ metric_1: 100, 'metric_1__1 year ago': 80 }],
colnames: ['metric_1', 'metric_1__1 year ago'],
coltypes: [GenericDataType.Numeric, GenericDataType.Numeric],
},
{
...basicQueryResult,
data: [{ rowcount: 1 }],
},
],
filterState: { filters: {} },
ownState: {},
hooks: {
onAddFilter: jest.fn(),
setDataMask: jest.fn(),
onContextMenu: jest.fn(),
},
emitCrossFilters: false,
};
const raw = {
...advanced,
rawFormData: {
@ -226,6 +352,8 @@ export default {
basic,
advanced,
advancedWithCurrency,
comparison,
comparisonWithConfig,
empty,
raw,
};

View File

@ -17,12 +17,7 @@
* under the License.
*/
import { useMemo, useState } from 'react';
import {
ChartDataResponseResult,
useTheme,
t,
GenericDataType,
} from '@superset-ui/core';
import { useTheme, t, GenericDataType } from '@superset-ui/core';
import {
COLUMN_NAME_ALIASES,
@ -39,7 +34,7 @@ import ControlHeader from '../../ControlHeader';
export type ColumnConfigControlProps<T extends ColumnConfig> =
ControlComponentProps<Record<string, T>> & {
queryResponse?: ChartDataResponseResult;
columnsPropsObject?: { colnames: string[]; coltypes: GenericDataType[] };
configFormLayout?: ColumnConfigFormLayout;
appliedColumnNames?: string[];
width?: number | string;
@ -55,7 +50,7 @@ const MAX_NUM_COLS = 10;
* Add per-column config to queried results.
*/
export default function ColumnConfigControl<T extends ColumnConfig>({
queryResponse,
columnsPropsObject,
appliedColumnNames = [],
value,
onChange,
@ -64,7 +59,7 @@ export default function ColumnConfigControl<T extends ColumnConfig>({
height,
...props
}: ColumnConfigControlProps<T>) {
const { colnames: _colnames, coltypes: _coltypes } = queryResponse || {};
const { colnames: _colnames, coltypes: _coltypes } = columnsPropsObject || {};
let colnames: string[] = [];
let coltypes: GenericDataType[] = [];
if (appliedColumnNames.length === 0) {