This is an automated email from the ASF dual-hosted git repository. arivero pushed a commit to branch table-time-comparison-offset in repository https://gitbox.apache.org/repos/asf/superset.git
commit 08173d95ae7d505ac62194b0cde00bee14fc2be8 Author: Lily Kuang <[email protected]> AuthorDate: Tue Apr 2 13:19:53 2024 -0700 feat(plugin): color option for table with time comparison (#27716) - cherry picked from afe7eaf8b3d16ef8aefdc5b2bf8f0953a59d9b60 --- .../plugins/plugin-chart-table/src/TableChart.tsx | 66 +++++++- .../plugin-chart-table/src/controlPanel.tsx | 171 ++++++++++++++++----- .../plugin-chart-table/src/transformProps.ts | 134 +++++++++++++++- .../plugins/plugin-chart-table/src/types.ts | 13 ++ .../ConditionalFormattingControl.tsx | 3 + .../FormattingPopover.tsx | 2 + .../FormattingPopoverContent.tsx | 39 ++++- .../controls/ConditionalFormattingControl/types.ts | 2 + 8 files changed, 383 insertions(+), 47 deletions(-) diff --git a/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx b/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx index 5c0e99b203..dc8221821a 100644 --- a/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx +++ b/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx @@ -59,7 +59,11 @@ import { } from '@ant-design/icons'; import { isEmpty } from 'lodash'; -import { DataColumnMeta, TableChartTransformedProps } from './types'; +import { + ColorSchemeEnum, + DataColumnMeta, + TableChartTransformedProps, +} from './types'; import DataTable, { DataTableProps, SearchInputProps, @@ -250,6 +254,8 @@ export default function TableChart<D extends DataRecord = DataRecord>( onContextMenu, emitCrossFilters, isUsingTimeComparison, + basicColorFormatters, + basicColorColumnFormatters, } = props; const comparisonColumns = [ { key: 'all', label: t('Display all') }, @@ -693,7 +699,13 @@ export default function TableChart<D extends DataRecord = DataRecord>( Array.isArray(columnColorFormatters) && columnColorFormatters.length > 0; + const hasBasicColorFormatters = + isUsingTimeComparison && + Array.isArray(basicColorFormatters) && + basicColorFormatters.length > 0; + const valueRange = + !hasBasicColorFormatters && !hasColumnColorFormatters && (config.showCellBars === undefined ? showCellBars @@ -727,6 +739,17 @@ export default function TableChart<D extends DataRecord = DataRecord>( const html = isHtml && allowRenderHtml ? { __html: text } : undefined; let backgroundColor; + let arrow = ''; + const originKey = column.key.substring(column.label.length).trim(); + if (!hasColumnColorFormatters && hasBasicColorFormatters) { + backgroundColor = + basicColorFormatters[row.index][originKey]?.backgroundColor; + arrow = + column.label === comparisonLabels[0] + ? basicColorFormatters[row.index][originKey]?.mainArrow + : ''; + } + if (hasColumnColorFormatters) { columnColorFormatters! .filter(formatter => formatter.column === column.key) @@ -741,6 +764,19 @@ export default function TableChart<D extends DataRecord = DataRecord>( }); } + if ( + basicColorColumnFormatters && + basicColorColumnFormatters?.length > 0 + ) { + backgroundColor = + basicColorColumnFormatters[row.index][column.key] + ?.backgroundColor || backgroundColor; + arrow = + column.label === comparisonLabels[0] + ? basicColorColumnFormatters[row.index][column.key]?.mainArrow + : ''; + } + const StyledCell = styled.td` text-align: ${sharedStyle.textAlign}; white-space: ${value instanceof Date ? 'nowrap' : undefined}; @@ -772,6 +808,28 @@ export default function TableChart<D extends DataRecord = DataRecord>( `} `; + let arrowStyles = css` + color: ${basicColorFormatters && + basicColorFormatters[row.index][originKey]?.arrowColor === + ColorSchemeEnum.Green + ? theme.colors.success.base + : theme.colors.error.base}; + margin-right: ${theme.gridUnit}px; + `; + + if ( + basicColorColumnFormatters && + basicColorColumnFormatters?.length > 0 + ) { + arrowStyles = css` + color: ${basicColorColumnFormatters[row.index][column.key] + ?.arrowColor === ColorSchemeEnum.Green + ? theme.colors.success.base + : theme.colors.error.base}; + margin-right: ${theme.gridUnit}px; + `; + } + const cellProps = { 'aria-labelledby': `header-${column.key}`, role: 'cell', @@ -841,10 +899,14 @@ export default function TableChart<D extends DataRecord = DataRecord>( className="dt-truncate-cell" style={columnWidth ? { width: columnWidth } : undefined} > + {arrow && <span css={arrowStyles}>{arrow}</span>} {text} </div> ) : ( - text + <> + {arrow && <span css={arrowStyles}>{arrow}</span>} + {text} + </> )} </StyledCell> ); diff --git a/superset-frontend/plugins/plugin-chart-table/src/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-table/src/controlPanel.tsx index f5f28d5d38..5859a6a0bf 100644 --- a/superset-frontend/plugins/plugin-chart-table/src/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-table/src/controlPanel.tsx @@ -49,6 +49,7 @@ import { import { isEmpty } from 'lodash'; import { PAGE_SIZE_OPTIONS } from './consts'; +import { ColorSchemeEnum } from './types'; function getQueryMode(controls: ControlStateMapping): QueryMode { const mode = controls?.query_mode?.value; @@ -145,6 +146,33 @@ const percentMetricsControl: typeof sharedControls.metrics = { validators: [], }; +const processComparisonColumns = (columns: any[], suffix: string) => + columns + .map(col => { + if (!col.label.includes(suffix)) { + return [ + { + label: `${t('Main')} ${col.label}`, + value: `${t('Main')} ${col.value}`, + }, + { + label: `# ${col.label}`, + value: `# ${col.value}`, + }, + { + label: `△ ${col.label}`, + value: `△ ${col.value}`, + }, + { + label: `% ${col.label}`, + value: `% ${col.value}`, + }, + ]; + } + return []; + }) + .flat(); + const config: ControlPanelConfig = { controlPanelSections: [ { @@ -400,11 +428,70 @@ const config: ControlPanelConfig = { description: t('Whether to include a client-side search box'), }, }, + ], + [ + { + name: 'allow_rearrange_columns', + config: { + type: 'CheckboxControl', + label: t('Allow columns to be rearranged'), + renderTrigger: true, + default: false, + description: t( + "Allow end user to drag-and-drop column headers to rearrange them. Note their changes won't persist for the next time they open the chart.", + ), + visibility: ({ controls }) => + isEmpty(controls?.time_compare?.value), + }, + }, + ], + [ + { + name: 'allow_render_html', + config: { + type: 'CheckboxControl', + label: t('Render columns in HTML format'), + renderTrigger: true, + default: true, + description: t('Render data in HTML format if applicable.'), + }, + }, + ], + [ + { + name: 'column_config', + config: { + type: 'ColumnConfigControl', + label: t('Customize columns'), + description: t('Further customize how to display each column'), + width: 400, + height: 320, + renderTrigger: true, + shouldMapStateToProps() { + return true; + }, + mapStateToProps(explore, _, chart) { + return { + queryResponse: chart?.queriesResponse?.[0] as + | ChartDataResponseResult + | undefined, + }; + }, + }, + }, + ], + ], + }, + { + label: t('Visual formatting'), + expanded: true, + controlSetRows: [ + [ { name: 'show_cell_bars', config: { type: 'CheckboxControl', - label: t('Cell bars'), + label: t('Show Cell bars'), renderTrigger: true, default: true, description: t( @@ -426,11 +513,13 @@ const config: ControlPanelConfig = { ), }, }, + ], + [ { name: 'color_pn', config: { type: 'CheckboxControl', - label: t('Color +/-'), + label: t('add colors to cell bars for +/-'), renderTrigger: true, default: true, description: t( @@ -441,52 +530,41 @@ const config: ControlPanelConfig = { ], [ { - name: 'allow_rearrange_columns', + name: 'comparison_color_enabled', config: { type: 'CheckboxControl', - label: t('Allow columns to be rearranged'), + label: t('basic conditional formatting'), renderTrigger: true, + visibility: ({ controls }) => + !isEmpty(controls?.time_compare?.value), default: false, description: t( - "Allow end user to drag-and-drop column headers to rearrange them. Note their changes won't persist for the next time they open the chart.", + 'This will be applied to the whole table. Arrows (↑ and ↓) will be added to ' + + 'main columns for increase and decrease. Basic conditional formatting can be ' + + 'overwritten by conditional formatting below.', ), - visibility: ({ controls }) => - isEmpty(controls?.time_compare?.value), - }, - }, - ], - [ - { - name: 'allow_render_html', - config: { - type: 'CheckboxControl', - label: t('Render columns in HTML format'), - renderTrigger: true, - default: true, - description: t('Render data in HTML format if applicable.'), }, }, ], [ { - name: 'column_config', + name: 'comparison_color_scheme', config: { - type: 'ColumnConfigControl', - label: t('Customize columns'), - description: t('Further customize how to display each column'), - width: 400, - height: 320, + type: 'SelectControl', + label: t('color type'), + default: ColorSchemeEnum.Green, renderTrigger: true, - shouldMapStateToProps() { - return true; - }, - mapStateToProps(explore, _, chart) { - return { - queryResponse: chart?.queriesResponse?.[0] as - | ChartDataResponseResult - | undefined, - }; - }, + choices: [ + [ColorSchemeEnum.Green, 'Green for increase, red for decrease'], + [ColorSchemeEnum.Red, 'Red for increase, green for decrease'], + ], + visibility: ({ controls }) => + !isEmpty(controls?.time_compare?.value) && + Boolean(controls?.comparison_color_enabled?.value), + description: t( + 'Adds color to the chart symbols based on the positive or ' + + 'negative change from the comparison value.', + ), }, }, ], @@ -496,7 +574,17 @@ const config: ControlPanelConfig = { config: { type: 'ConditionalFormattingControl', renderTrigger: true, - label: t('Conditional formatting'), + label: t('Custom Conditional Formatting'), + extraColorChoices: [ + { + value: ColorSchemeEnum.Green, + label: t('Green for increase, red for decrease'), + }, + { + value: ColorSchemeEnum.Red, + label: t('Red for increase, green for decrease'), + }, + ], description: t( 'Apply conditional color formatting to numeric columns', ), @@ -524,9 +612,18 @@ const config: ControlPanelConfig = { label: verboseMap[colname] ?? colname, })) : []; + const columnOptions = explore?.controls?.time_compare?.value + ? processComparisonColumns( + numericColumns || [], + ensureIsArray( + explore?.controls?.time_compare?.value, + )[0]?.toString() || '', + ) + : numericColumns; + return { removeIrrelevantConditions: chartStatus === 'success', - columnOptions: numericColumns, + columnOptions, verboseMap, }; }, diff --git a/superset-frontend/plugins/plugin-chart-table/src/transformProps.ts b/superset-frontend/plugins/plugin-chart-table/src/transformProps.ts index cf70bfed16..a9ead497c3 100644 --- a/superset-frontend/plugins/plugin-chart-table/src/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-table/src/transformProps.ts @@ -36,6 +36,7 @@ import { } from '@superset-ui/core'; import { ColorFormatters, + ConditionalFormattingConfig, getColorFormatters, } from '@superset-ui/chart-controls'; @@ -43,6 +44,8 @@ import { isEmpty } from 'lodash'; import isEqualColumns from './utils/isEqualColumns'; import DateWithFormatter from './utils/DateWithFormatter'; import { + BasicColorFormatterType, + ColorSchemeEnum, DataColumnMeta, TableChartProps, TableChartTransformedProps, @@ -390,9 +393,124 @@ const transformProps = ( allow_rearrange_columns: allowRearrangeColumns, allow_render_html: allowRenderHtml, time_compare, + comparison_color_enabled: comparisonColorEnabled = false, + comparison_color_scheme: comparisonColorScheme = ColorSchemeEnum.Green, } = formData; const isUsingTimeComparison = !isEmpty(time_compare) && queryMode === QueryMode.Aggregate; + + const calculateBasicStyle = ( + percentDifferenceNum: number, + colorOption: ColorSchemeEnum, + ) => { + if (percentDifferenceNum === 0) { + return { + arrow: '', + arrowColor: '', + // eslint-disable-next-line theme-colors/no-literal-colors + backgroundColor: '#FFBFA133', + }; + } + const isPositive = percentDifferenceNum > 0; + const arrow = isPositive ? '↑' : '↓'; + const arrowColor = + colorOption === ColorSchemeEnum.Green + ? isPositive + ? ColorSchemeEnum.Green + : ColorSchemeEnum.Red + : isPositive + ? ColorSchemeEnum.Red + : ColorSchemeEnum.Green; + const backgroundColor = + colorOption === ColorSchemeEnum.Green + ? `rgba(${isPositive ? '0,150,0' : '150,0,0'},0.2)` + : `rgba(${isPositive ? '150,0,0' : '0,150,0'},0.2)`; + + return { arrow, arrowColor, backgroundColor }; + }; + + const getBasicColorFormatter = memoizeOne(function getBasicColorFormatter( + originalData: DataRecord[] | undefined, + originalColumns: DataColumnMeta[], + selectedColumns?: ConditionalFormattingConfig[], + ) { + // Transform data + const relevantColumns = selectedColumns + ? originalColumns.filter(col => + selectedColumns.some(scol => scol?.column?.includes(col.key)), + ) + : originalColumns; + + return originalData?.map(originalItem => { + const item: { [key: string]: BasicColorFormatterType } = {}; + relevantColumns.forEach(origCol => { + if ( + (origCol.isMetric || origCol.isPercentMetric) && + !origCol.key.includes(ensureIsArray(time_compare)[0]) && + origCol.isNumeric + ) { + const originalValue = originalItem[origCol.key] || 0; + const comparisonValue = origCol.isMetric + ? originalItem?.[ + `${origCol.key}__${ensureIsArray(time_compare)[0]}` + ] || 0 + : originalItem[ + `%${origCol.key.slice(1)}__${ensureIsArray(time_compare)[0]}` + ] || 0; + const { percentDifferenceNum } = calculateDifferences( + originalValue as number, + comparisonValue as number, + ); + + if (selectedColumns) { + selectedColumns.forEach(col => { + if (col?.column?.includes(origCol.key)) { + const { arrow, arrowColor, backgroundColor } = + calculateBasicStyle( + percentDifferenceNum, + col.colorScheme || comparisonColorScheme, + ); + item[col.column] = { + mainArrow: arrow, + arrowColor, + backgroundColor, + }; + } + }); + } else { + const { arrow, arrowColor, backgroundColor } = calculateBasicStyle( + percentDifferenceNum, + comparisonColorScheme, + ); + item[`${origCol.key}`] = { + mainArrow: arrow, + arrowColor, + backgroundColor, + }; + } + } + }); + return item; + }); + }); + + const getBasicColorFormatterForColumn = ( + originalData: DataRecord[] | undefined, + originalColumns: DataColumnMeta[], + conditionalFormatting?: ConditionalFormattingConfig[], + ) => { + const selectedColumns = conditionalFormatting?.filter( + (config: ConditionalFormattingConfig) => + config.column && + (config.colorScheme === ColorSchemeEnum.Green || + config.colorScheme === ColorSchemeEnum.Red), + ); + + return selectedColumns?.length + ? getBasicColorFormatter(originalData, originalColumns, selectedColumns) + : undefined; + }; + const timeGrain = extractTimegrain(formData); const comparisonSuffix = isUsingTimeComparison ? ensureIsArray(time_compare)[0] @@ -431,12 +549,22 @@ const transformProps = ( ? processComparisonTotals(comparisonSuffix, totalQuery?.data) : totalQuery?.data[0] : undefined; - const columnColorFormatters = - getColorFormatters(conditionalFormatting, data) ?? defaultColorFormatters; const passedData = isUsingTimeComparison ? comparisonData || [] : data; const passedColumns = isUsingTimeComparison ? comparisonColumns : columns; + const basicColorFormatters = + comparisonColorEnabled && getBasicColorFormatter(baseQuery?.data, columns); + const columnColorFormatters = + getColorFormatters(conditionalFormatting, passedData) ?? + defaultColorFormatters; + + const basicColorColumnFormatters = getBasicColorFormatterForColumn( + baseQuery?.data, + columns, + conditionalFormatting, + ); + return { height, width, @@ -469,6 +597,8 @@ const transformProps = ( allowRenderHtml, onContextMenu, isUsingTimeComparison, + basicColorFormatters, + basicColorColumnFormatters, }; }; diff --git a/superset-frontend/plugins/plugin-chart-table/src/types.ts b/superset-frontend/plugins/plugin-chart-table/src/types.ts index 2abba6815d..7aa14d06ac 100644 --- a/superset-frontend/plugins/plugin-chart-table/src/types.ts +++ b/superset-frontend/plugins/plugin-chart-table/src/types.ts @@ -102,6 +102,12 @@ export interface TableChartProps extends ChartProps { queriesData: ChartDataResponseResult[]; } +export type BasicColorFormatterType = { + backgroundColor: string; + arrowColor: string; + mainArrow: string; +}; + export interface TableChartTransformedProps<D extends DataRecord = DataRecord> { timeGrain?: TimeGranularity; height: number; @@ -137,6 +143,13 @@ export interface TableChartTransformedProps<D extends DataRecord = DataRecord> { filters?: ContextMenuFilters, ) => void; isUsingTimeComparison?: boolean; + basicColorFormatters?: { [Key: string]: BasicColorFormatterType }[]; + basicColorColumnFormatters?: { [Key: string]: BasicColorFormatterType }[]; +} + +export enum ColorSchemeEnum { + 'Green' = 'Green', + 'Red' = 'Red', } export default {}; diff --git a/superset-frontend/src/explore/components/controls/ConditionalFormattingControl/ConditionalFormattingControl.tsx b/superset-frontend/src/explore/components/controls/ConditionalFormattingControl/ConditionalFormattingControl.tsx index 6c4ae9f412..9bf0c90f7c 100644 --- a/superset-frontend/src/explore/components/controls/ConditionalFormattingControl/ConditionalFormattingControl.tsx +++ b/superset-frontend/src/explore/components/controls/ConditionalFormattingControl/ConditionalFormattingControl.tsx @@ -71,6 +71,7 @@ const ConditionalFormattingControl = ({ columnOptions, verboseMap, removeIrrelevantConditions, + extraColorChoices, ...props }: ConditionalFormattingControlProps) => { const theme = useTheme(); @@ -155,6 +156,7 @@ const ConditionalFormattingControl = ({ onEdit(newConfig, index) } destroyTooltipOnHide + extraColorChoices={extraColorChoices} > <OptionControlContainer withCaret> <Label>{createLabel(config)}</Label> @@ -170,6 +172,7 @@ const ConditionalFormattingControl = ({ columns={columnOptions} onChange={onSave} destroyTooltipOnHide + extraColorChoices={extraColorChoices} > <AddControlLabel> <Icons.PlusSmall iconColor={theme.colors.grayscale.light1} /> diff --git a/superset-frontend/src/explore/components/controls/ConditionalFormattingControl/FormattingPopover.tsx b/superset-frontend/src/explore/components/controls/ConditionalFormattingControl/FormattingPopover.tsx index c44ca76235..fda09d4e61 100644 --- a/superset-frontend/src/explore/components/controls/ConditionalFormattingControl/FormattingPopover.tsx +++ b/superset-frontend/src/explore/components/controls/ConditionalFormattingControl/FormattingPopover.tsx @@ -27,6 +27,7 @@ export const FormattingPopover = ({ onChange, config, children, + extraColorChoices, ...props }: FormattingPopoverProps) => { const [visible, setVisible] = useState(false); @@ -47,6 +48,7 @@ export const FormattingPopover = ({ onChange={handleSave} config={config} columns={columns} + extraColorChoices={extraColorChoices} /> } visible={visible} diff --git a/superset-frontend/src/explore/components/controls/ConditionalFormattingControl/FormattingPopoverContent.tsx b/superset-frontend/src/explore/components/controls/ConditionalFormattingControl/FormattingPopoverContent.tsx index 533e185475..1f17019d8c 100644 --- a/superset-frontend/src/explore/components/controls/ConditionalFormattingControl/FormattingPopoverContent.tsx +++ b/superset-frontend/src/explore/components/controls/ConditionalFormattingControl/FormattingPopoverContent.tsx @@ -16,8 +16,9 @@ * specific language governing permissions and limitations * under the License. */ -import React from 'react'; +import React, { useState } from 'react'; import { styled, SupersetTheme, t, useTheme } from '@superset-ui/core'; +import { ColorSchemeEnum } from '@superset-ui/plugin-chart-table'; import { Comparator, MultipleValueComparators, @@ -123,21 +124,24 @@ const shouldFormItemUpdate = ( isOperatorMultiValue(prevValues.operator) !== isOperatorMultiValue(currentValues.operator); -const operatorField = ( +const operatorField = (showOnlyNone?: boolean) => ( <FormItem name="operator" label={t('Operator')} rules={rulesRequired} initialValue={operatorOptions[0].value} > - <Select ariaLabel={t('Operator')} options={operatorOptions} /> + <Select + ariaLabel={t('Operator')} + options={showOnlyNone ? [operatorOptions[0]] : operatorOptions} + /> </FormItem> ); const renderOperatorFields = ({ getFieldValue }: GetFieldValue) => isOperatorNone(getFieldValue('operator')) ? ( <Row gutter={12}> - <Col span={6}>{operatorField}</Col> + <Col span={6}>{operatorField()}</Col> </Row> ) : isOperatorMultiValue(getFieldValue('operator')) ? ( <Row gutter={12}> @@ -186,13 +190,26 @@ export const FormattingPopoverContent = ({ config, onChange, columns = [], + extraColorChoices = [], }: { config?: ConditionalFormattingConfig; onChange: (config: ConditionalFormattingConfig) => void; columns: { label: string; value: string }[]; + extraColorChoices?: { label: string; value: string }[]; }) => { const theme = useTheme(); const colorScheme = colorSchemeOptions(theme); + const [showOperatorFields, setShowOperatorFields] = useState( + config === undefined || + (config?.colorScheme !== ColorSchemeEnum.Green && + config?.colorScheme !== ColorSchemeEnum.Red), + ); + const handleChange = (event: any) => { + setShowOperatorFields( + !(event === ColorSchemeEnum.Green || event === ColorSchemeEnum.Red), + ); + }; + return ( <Form onFinish={onChange} @@ -218,12 +235,22 @@ export const FormattingPopoverContent = ({ rules={rulesRequired} initialValue={colorScheme[0].value} > - <Select ariaLabel={t('Color scheme')} options={colorScheme} /> + <Select + onChange={event => handleChange(event)} + ariaLabel={t('Color scheme')} + options={[...colorScheme, ...extraColorChoices]} + /> </FormItem> </Col> </Row> <FormItem noStyle shouldUpdate={shouldFormItemUpdate}> - {renderOperatorFields} + {showOperatorFields ? ( + renderOperatorFields + ) : ( + <Row gutter={12}> + <Col span={6}>{operatorField(true)}</Col> + </Row> + )} </FormItem> <FormItem> <JustifyEnd> diff --git a/superset-frontend/src/explore/components/controls/ConditionalFormattingControl/types.ts b/superset-frontend/src/explore/components/controls/ConditionalFormattingControl/types.ts index 4edadf99b4..c352ca818b 100644 --- a/superset-frontend/src/explore/components/controls/ConditionalFormattingControl/types.ts +++ b/superset-frontend/src/explore/components/controls/ConditionalFormattingControl/types.ts @@ -38,6 +38,7 @@ export type ConditionalFormattingControlProps = ControlComponentProps< verboseMap: Record<string, string>; label: string; description: string; + extraColorChoices?: { label: string; value: string }[]; }; export type FormattingPopoverProps = PopoverProps & { @@ -46,4 +47,5 @@ export type FormattingPopoverProps = PopoverProps & { config?: ConditionalFormattingConfig; title: string; children: ReactNode; + extraColorChoices?: { label: string; value: string }[]; };
