feat(charts): add subtitle option and metric customization controls (#32975)

This commit is contained in:
Levis Mbote
2025-04-10 18:24:24 +03:00
committed by GitHub
parent 164a07e2be
commit d75ff9e784
19 changed files with 690 additions and 401 deletions

View File

@@ -81,6 +81,8 @@ export default function PopKPI(props: PopKPIProps) {
currentTimeRangeFilter,
startDateOffset,
shift,
subtitle,
subtitleFontSize,
dashboardTimeRange,
} = props;
@@ -140,6 +142,16 @@ export default function PopKPI(props: PopKPIProps) {
margin-bottom: ${theme.gridUnit * 4}px;
`;
const SubtitleText = styled.div`
${({ theme }) => `
font-family: ${theme.typography.families.sansSerif};
font-weight: ${theme.typography.weights.medium};
text-align: center;
margin-top: -10px;
margin-bottom: ${theme.gridUnit * 4}px;
`}
`;
const getArrowIndicatorColor = () => {
if (!comparisonColorEnabled || percentDifferenceNumber === 0) {
return theme.colors.grayscale.base;
@@ -195,31 +207,40 @@ export default function PopKPI(props: PopKPIProps) {
]);
const SYMBOLS_WITH_VALUES = useMemo(
() => [
{
symbol: '#',
value: prevNumber,
tooltipText: t('Data for %s', comparisonRange || 'previous range'),
columnKey: 'Previous value',
},
{
symbol: '△',
value: valueDifference,
tooltipText: t('Value difference between the time periods'),
columnKey: 'Delta',
},
{
symbol: '%',
value: percentDifferenceFormattedString,
tooltipText: t('Percentage difference between the time periods'),
columnKey: 'Percent change',
},
],
() =>
[
{
defaultSymbol: '#',
value: prevNumber,
tooltipText: t('Data for %s', comparisonRange || 'previous range'),
columnKey: 'Previous value',
},
{
defaultSymbol: '△',
value: valueDifference,
tooltipText: t('Value difference between the time periods'),
columnKey: 'Delta',
},
{
defaultSymbol: '%',
value: percentDifferenceFormattedString,
tooltipText: t('Percentage difference between the time periods'),
columnKey: 'Percent change',
},
].map(item => {
const config = props.columnConfig?.[item.columnKey];
return {
...item,
symbol: config?.displayTypeIcon === false ? '' : item.defaultSymbol,
label: config?.customColumnName || item.columnKey,
};
}),
[
comparisonRange,
prevNumber,
valueDifference,
percentDifferenceFormattedString,
props.columnConfig,
],
);
@@ -250,6 +271,15 @@ export default function PopKPI(props: PopKPIProps) {
</span>
)}
</div>
{subtitle && (
<SubtitleText
style={{
fontSize: `${subtitleFontSize * height * 0.4}px`,
}}
>
{subtitle}
</SubtitleText>
)}
{visibleSymbols.length > 0 && (
<div
@@ -276,7 +306,7 @@ export default function PopKPI(props: PopKPIProps) {
>
{visibleSymbols.map((symbol_with_value, index) => (
<ComparisonValue
key={`comparison-symbol-${symbol_with_value.symbol}`}
key={`comparison-symbol-${symbol_with_value.columnKey}`}
subheaderFontSize={subheaderFontSize}
>
<Tooltip
@@ -284,15 +314,19 @@ export default function PopKPI(props: PopKPIProps) {
placement="top"
title={symbol_with_value.tooltipText}
>
<SymbolWrapper
backgroundColor={
index > 0 ? backgroundColor : defaultBackgroundColor
}
textColor={index > 0 ? textColor : defaultTextColor}
>
{symbol_with_value.symbol}
</SymbolWrapper>
{symbol_with_value.value}
{symbol_with_value.symbol && (
<SymbolWrapper
backgroundColor={
index > 0 ? backgroundColor : defaultBackgroundColor
}
textColor={index > 0 ? textColor : defaultTextColor}
>
{symbol_with_value.symbol}
</SymbolWrapper>
)}
{symbol_with_value.value}{' '}
{props.columnConfig?.[symbol_with_value.columnKey]
?.customColumnName || ''}
</Tooltip>
</ComparisonValue>
))}

View File

@@ -23,7 +23,12 @@ import {
sharedControls,
sections,
} from '@superset-ui/chart-controls';
import { headerFontSize, subheaderFontSize } from '../sharedControls';
import {
headerFontSize,
subheaderFontSize,
subtitleControl,
subtitleFontSize,
} from '../sharedControls';
import { ColorSchemeEnum } from './types';
const config: ControlPanelConfig = {
@@ -63,6 +68,8 @@ const config: ControlPanelConfig = {
config: { ...headerFontSize.config, default: 0.2 },
},
],
[subtitleControl],
[subtitleFontSize],
[
{
...subheaderFontSize,
@@ -120,7 +127,11 @@ const config: ControlPanelConfig = {
[GenericDataType.Numeric]: [
{
tab: t('General'),
children: [['visible']],
children: [
['customColumnName'],
['displayTypeIcon'],
['visible'],
],
},
],
},

View File

@@ -89,6 +89,8 @@ export default function transformProps(chartProps: ChartProps) {
comparisonColorScheme,
comparisonColorEnabled,
percentDifferenceFormat,
subtitle = '',
subtitleFontSize,
columnConfig,
} = formData;
const { data: dataA = [] } = queriesData[0];
@@ -183,6 +185,8 @@ export default function transformProps(chartProps: ChartProps) {
valueDifference,
percentDifferenceFormattedString: percentDifference,
boldText,
subtitle,
subtitleFontSize,
headerFontSize: getHeaderFontSize(headerFontSize),
subheaderFontSize: getComparisonFontSize(subheaderFontSize),
headerText,

View File

@@ -35,6 +35,8 @@ export interface PopKPIStylesProps {
export type TableColumnConfig = {
visible?: boolean;
customColumnName?: string;
displayTypeIcon?: boolean;
};
interface PopKPICustomizeProps {
@@ -61,6 +63,8 @@ export type PopKPIProps = PopKPIStylesProps &
metricName: string;
bigNumber: string;
prevNumber: string;
subtitle?: string;
subtitleFontSize: number;
valueDifference: string;
percentDifferenceFormattedString: string;
compType: string;

View File

@@ -24,7 +24,11 @@ import {
Dataset,
getStandardizedControls,
} from '@superset-ui/chart-controls';
import { headerFontSize, subheaderFontSize } from '../sharedControls';
import {
headerFontSize,
subtitleFontSize,
subtitleControl,
} from '../sharedControls';
export default {
controlPanelSections: [
@@ -33,32 +37,13 @@ export default {
expanded: true,
controlSetRows: [['metric'], ['adhoc_filters']],
},
{
label: t('Display settings'),
expanded: true,
tabOverride: 'data',
controlSetRows: [
[
{
name: 'subheader',
config: {
type: 'TextControl',
label: t('Subheader'),
renderTrigger: true,
description: t(
'Description text that shows up below your Big Number',
),
},
},
],
],
},
{
label: t('Chart Options'),
expanded: true,
controlSetRows: [
[headerFontSize],
[subheaderFontSize],
[subtitleControl],
[subtitleFontSize],
['y_axis_format'],
['currency_format'],
[

View File

@@ -47,8 +47,8 @@ export default function transformProps(
const {
headerFontSize,
metric = 'value',
subheader = '',
subheaderFontSize,
subtitle = '',
subtitleFontSize,
forceTimestampFormatting,
timeFormat,
yAxisFormat,
@@ -59,7 +59,7 @@ export default function transformProps(
const { data = [], coltypes = [] } = queriesData[0];
const granularity = extractTimegrain(rawFormData as QueryFormData);
const metricName = getMetricLabel(metric);
const formattedSubheader = subheader;
const formattedSubtitle = subtitle;
const bigNumber =
data.length === 0 ? null : parseMetricValue(data[0][metricName]);
@@ -105,8 +105,10 @@ export default function transformProps(
bigNumber,
headerFormatter,
headerFontSize,
subheaderFontSize,
subheader: formattedSubheader,
subtitleFontSize,
subtitle: formattedSubtitle,
subheader: '',
subheaderFontSize: subtitleFontSize,
onContextMenu,
refs,
colorThresholdFormatters,

View File

@@ -229,6 +229,40 @@ class BigNumberVis extends PureComponent<BigNumberVizProps> {
return null;
}
renderSubtitle(maxHeight: number) {
const { subtitle, width } = this.props;
let fontSize = 0;
if (subtitle) {
const container = this.createTemporaryContainer();
document.body.append(container);
try {
fontSize = computeMaxFontSize({
text: subtitle,
maxWidth: width * 0.9,
maxHeight,
className: 'subtitle-line',
container,
});
} finally {
container.remove();
}
return (
<div
className="subtitle-line"
style={{
fontSize,
height: maxHeight,
}}
>
{subtitle}
</div>
);
}
return null;
}
renderTrendline(maxHeight: number) {
const { width, trendLineData, echartOptions, refs } = this.props;
@@ -282,6 +316,7 @@ class BigNumberVis extends PureComponent<BigNumberVizProps> {
kickerFontSize,
headerFontSize,
subheaderFontSize,
subtitleFontSize,
} = this.props;
const className = this.getClassName();
@@ -306,6 +341,9 @@ class BigNumberVis extends PureComponent<BigNumberVizProps> {
subheaderFontSize * (1 - PROPORTION.TRENDLINE) * height,
),
)}
{this.renderSubtitle(
Math.ceil(subtitleFontSize * (1 - PROPORTION.TRENDLINE) * height),
)}
</div>
{this.renderTrendline(chartHeight)}
</div>
@@ -318,6 +356,7 @@ class BigNumberVis extends PureComponent<BigNumberVizProps> {
{this.renderKicker((kickerFontSize || 0) * height)}
{this.renderHeader(Math.ceil(headerFontSize * height))}
{this.renderSubheader(Math.ceil(subheaderFontSize * height))}
{this.renderSubtitle(Math.ceil(subtitleFontSize * height))}
</div>
);
}
@@ -368,7 +407,12 @@ export default styled(BigNumberVis)`
.subheader-line {
line-height: 1em;
padding-bottom: 0;
padding-bottom: 0.3em;
}
.subtitle-line {
line-height: 1em;
padding-top: 0.3em;
}
&.is-fallback-value {

View File

@@ -26,7 +26,12 @@ import {
getStandardizedControls,
temporalColumnMixin,
} from '@superset-ui/chart-controls';
import { headerFontSize, subheaderFontSize } from '../sharedControls';
import {
headerFontSize,
subheaderFontSize,
subtitleFontSize,
subtitleControl,
} from '../sharedControls';
const config: ControlPanelConfig = {
controlPanelSections: [
@@ -134,6 +139,8 @@ const config: ControlPanelConfig = {
['color_picker', null],
[headerFontSize],
[subheaderFontSize],
[subtitleControl],
[subtitleFontSize],
['y_axis_format'],
['currency_format'],
[

View File

@@ -66,6 +66,8 @@ export default function transformProps(
metric = 'value',
showTimestamp,
showTrendLine,
subtitle = '',
subtitleFontSize,
aggregation,
startYAxisAtZero,
subheader = '',
@@ -302,6 +304,8 @@ export default function transformProps(
formatTime,
formData,
headerFontSize,
subtitleFontSize,
subtitle,
subheaderFontSize,
mainColor,
showTimestamp,

View File

@@ -55,6 +55,39 @@ export const headerFontSize: CustomControlItem = {
},
};
export const subtitleFontSize: CustomControlItem = {
name: 'subtitle_font_size',
config: {
type: 'SelectControl',
label: t('Subtitle Font Size'),
renderTrigger: true,
clearable: false,
default: 0.15,
// Values represent the percentage of space a subtitle should take
options: [
{
label: t('Tiny'),
value: 0.125,
},
{
label: t('Small'),
value: 0.15,
},
{
label: t('Normal'),
value: 0.2,
},
{
label: t('Large'),
value: 0.3,
},
{
label: t('Huge'),
value: 0.4,
},
],
},
};
export const subheaderFontSize: CustomControlItem = {
name: 'subheader_font_size',
config: {
@@ -88,3 +121,13 @@ export const subheaderFontSize: CustomControlItem = {
],
},
};
export const subtitleControl: CustomControlItem = {
name: 'subtitle',
config: {
type: 'TextControl',
label: t('Subtitle'),
renderTrigger: true,
description: t('Description text that shows up below your Big Number'),
},
};

View File

@@ -78,7 +78,9 @@ export type BigNumberVizProps = {
headerFontSize: number;
kickerFontSize?: number;
subheader: string;
subtitle: string;
subheaderFontSize: number;
subtitleFontSize: number;
showTimestamp?: boolean;
showTrendLine?: boolean;
startYAxisAtZero?: boolean;

View File

@@ -682,13 +682,33 @@ export default function TableChart<D extends DataRecord = DataRecord>(
(column: DataColumnMeta, i: number): ColumnWithLooseAccessor<D> => {
const {
key,
label,
label: originalLabel,
isNumeric,
dataType,
isMetric,
isPercentMetric,
config = {},
} = column;
const label = config.customColumnName || originalLabel;
let displayLabel = label;
const isComparisonColumn = ['#', '△', '%', t('Main')].includes(
column.label,
);
if (isComparisonColumn) {
if (column.label === t('Main')) {
displayLabel = config.customColumnName || column.originalLabel || '';
} else if (config.customColumnName) {
displayLabel =
config.displayTypeIcon !== false
? `${column.label} ${config.customColumnName}`
: config.customColumnName;
} else if (config.displayTypeIcon === false) {
displayLabel = '';
}
}
const columnWidth = Number.isNaN(Number(config.columnWidth))
? config.columnWidth
: Number(config.columnWidth);
@@ -795,6 +815,9 @@ export default function TableChart<D extends DataRecord = DataRecord>(
white-space: ${value instanceof Date ? 'nowrap' : undefined};
position: relative;
background: ${backgroundColor || undefined};
padding-left: ${column.isChildColumn
? `${theme.gridUnit * 5}px`
: `${theme.gridUnit}px`};
`;
const cellBarStyles = css`
@@ -970,11 +993,12 @@ export default function TableChart<D extends DataRecord = DataRecord>(
alignItems: 'flex-end',
}}
>
<span data-column-name={col.id}>{label}</span>
<span data-column-name={col.id}>{displayLabel}</span>
<SortIcon column={col} />
</div>
</th>
),
Footer: totals ? (
i === 0 ? (
<th key={`footer-summary-${i}`}>
@@ -1024,9 +1048,14 @@ export default function TableChart<D extends DataRecord = DataRecord>(
],
);
const visibleColumnsMeta = useMemo(
() => filteredColumnsMeta.filter(col => col.config?.visible !== false),
[filteredColumnsMeta],
);
const columns = useMemo(
() => filteredColumnsMeta.map(getColumnConfigs),
[filteredColumnsMeta, getColumnConfigs],
() => visibleColumnsMeta.map(getColumnConfigs),
[visibleColumnsMeta, getColumnConfigs],
);
const handleServerPaginationChange = useCallback(

View File

@@ -494,6 +494,7 @@ const config: ControlPanelConfig = {
chart?.queriesResponse?.[0] ?? {};
let colnames: string[] = _colnames || [];
let coltypes: GenericDataType[] = _coltypes || [];
const childColumnMap: Record<string, boolean> = {};
if (timeComparisonStatus) {
/**
@@ -501,15 +502,27 @@ const config: ControlPanelConfig = {
*/
const updatedColnames: string[] = [];
const updatedColtypes: GenericDataType[] = [];
colnames.forEach((colname, index) => {
if (coltypes[index] === GenericDataType.Numeric) {
updatedColnames.push(
...generateComparisonColumns(colname),
);
updatedColtypes.push(...generateComparisonColumnTypes(4));
const comparisonColumns =
generateComparisonColumns(colname);
comparisonColumns.forEach((name, idx) => {
updatedColnames.push(name);
updatedColtypes.push(
...generateComparisonColumnTypes(4),
);
if (idx === 0 && name.startsWith('Main ')) {
childColumnMap[name] = false;
} else {
childColumnMap[name] = true;
}
});
} else {
updatedColnames.push(colname);
updatedColtypes.push(coltypes[index]);
childColumnMap[colname] = false;
}
});
@@ -517,7 +530,7 @@ const config: ControlPanelConfig = {
coltypes = updatedColtypes;
}
return {
columnsPropsObject: { colnames, coltypes },
columnsPropsObject: { colnames, coltypes, childColumnMap },
};
},
},

View File

@@ -49,6 +49,9 @@ export type TableColumnConfig = {
colorPositiveNegative?: boolean;
truncateLongCells?: boolean;
currencyFormat?: Currency;
visible?: boolean;
customColumnName?: string;
displayTypeIcon?: boolean;
};
export interface DataColumnMeta {
@@ -68,6 +71,7 @@ export interface DataColumnMeta {
isPercentMetric?: boolean;
isNumeric?: boolean;
config?: TableColumnConfig;
isChildColumn?: boolean;
}
export interface TableChartData {

View File

@@ -67,18 +67,21 @@ describe('plugin-chart-table', () => {
});
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.originalLabel === 'metric_1' ||
col.originalLabel === 'metric_2' ||
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.originalLabel === 'metric_1'),
).toBe(true);
expect(
comparisonColumns.some(col => col.originalLabel === 'metric_2'),
).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);
@@ -180,26 +183,37 @@ describe('plugin-chart-table', () => {
const transformedProps = transformProps(testData.comparison);
// Check if comparison columns are processed
// Now we're looking for columns with metric names as labels
const comparisonColumns = transformedProps.columns.filter(
col =>
col.label === 'Main' ||
col.originalLabel === 'metric_1' ||
col.originalLabel === 'metric_2' ||
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.originalLabel === 'metric_1'),
).toBe(true);
expect(
comparisonColumns.some(col => col.originalLabel === 'metric_2'),
).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);
// Verify originalLabel for metric_1 comparison columns
const mainMetric1 = transformedProps.columns.find(
col => col.key === 'Main metric_1',
const metric1Column = transformedProps.columns.find(
col =>
col.originalLabel === 'metric_1' &&
!col.key.startsWith('#') &&
!col.key.startsWith('△') &&
!col.key.startsWith('%'),
);
expect(mainMetric1).toBeDefined();
expect(mainMetric1?.originalLabel).toBe('metric_1');
expect(metric1Column).toBeDefined();
expect(metric1Column?.originalLabel).toBe('metric_1');
expect(metric1Column?.label).toBe('Main');
const hashMetric1 = transformedProps.columns.find(
col => col.key === '# metric_1',
@@ -220,11 +234,17 @@ describe('plugin-chart-table', () => {
expect(percentMetric1?.originalLabel).toBe('metric_1');
// Verify originalLabel for metric_2 comparison columns
const mainMetric2 = transformedProps.columns.find(
col => col.key === 'Main metric_2',
const metric2Column = transformedProps.columns.find(
col =>
col.originalLabel === 'metric_2' &&
!col.key.startsWith('#') &&
!col.key.startsWith('△') &&
!col.key.startsWith('%'),
);
expect(mainMetric2).toBeDefined();
expect(mainMetric2?.originalLabel).toBe('metric_2');
expect(metric2Column).toBeDefined();
expect(metric2Column?.originalLabel).toBe('metric_2');
expect(metric2Column?.label).toBe('Main');
const hashMetric2 = transformedProps.columns.find(
col => col.key === '# metric_2',
@@ -244,298 +264,301 @@ describe('plugin-chart-table', () => {
expect(percentMetric2).toBeDefined();
expect(percentMetric2?.originalLabel).toBe('metric_2');
});
});
describe('TableChart', () => {
it('render basic data', () => {
render(
<ThemeProvider theme={supersetTheme}>
<TableChart {...transformProps(testData.basic)} sticky={false} />,
</ThemeProvider>,
);
describe('TableChart', () => {
it('render basic data', () => {
render(
<ThemeProvider theme={supersetTheme}>
<TableChart {...transformProps(testData.basic)} sticky={false} />,
</ThemeProvider>,
);
const firstDataRow = screen.getAllByRole('rowgroup')[1];
const cells = firstDataRow.querySelectorAll('td');
expect(cells).toHaveLength(12);
expect(cells[0]).toHaveTextContent('2020-01-01 12:34:56');
expect(cells[1]).toHaveTextContent('Michael');
// number is not in `metrics` list, so it should output raw value
// (in real world Superset, this would mean the column is used in GROUP BY)
expect(cells[2]).toHaveTextContent('2467063');
// should not render column with `.` in name as `undefined`
expect(cells[3]).toHaveTextContent('foo');
expect(cells[6]).toHaveTextContent('2467');
expect(cells[8]).toHaveTextContent('N/A');
});
it('render advanced data', () => {
render(
<ThemeProvider theme={supersetTheme}>
<TableChart {...transformProps(testData.advanced)} sticky={false} />,
</ThemeProvider>,
);
const secondColumnHeader = screen.getByText('Sum of Num');
expect(secondColumnHeader).toBeInTheDocument();
expect(secondColumnHeader?.getAttribute('data-column-name')).toEqual('1');
const firstDataRow = screen.getAllByRole('rowgroup')[1];
const cells = firstDataRow.querySelectorAll('td');
expect(cells[0]).toHaveTextContent('Michael');
expect(cells[2]).toHaveTextContent('12.346%');
expect(cells[4]).toHaveTextContent('2.47k');
});
it('render advanced data with currencies', () => {
render(
ProviderWrapper({
children: (
<TableChart
{...transformProps(testData.advancedWithCurrency)}
sticky={false}
/>
),
}),
);
const cells = document.querySelectorAll('td');
expect(document.querySelectorAll('th')[1]).toHaveTextContent(
'Sum of Num',
);
expect(cells[0]).toHaveTextContent('Michael');
expect(cells[2]).toHaveTextContent('12.346%');
expect(cells[4]).toHaveTextContent('$ 2.47k');
});
it('render raw data', () => {
const props = transformProps({
...testData.raw,
rawFormData: { ...testData.raw.rawFormData },
const firstDataRow = screen.getAllByRole('rowgroup')[1];
const cells = firstDataRow.querySelectorAll('td');
expect(cells).toHaveLength(12);
expect(cells[0]).toHaveTextContent('2020-01-01 12:34:56');
expect(cells[1]).toHaveTextContent('Michael');
// number is not in `metrics` list, so it should output raw value
// (in real world Superset, this would mean the column is used in GROUP BY)
expect(cells[2]).toHaveTextContent('2467063');
// should not render column with `.` in name as `undefined`
expect(cells[3]).toHaveTextContent('foo');
expect(cells[6]).toHaveTextContent('2467');
expect(cells[8]).toHaveTextContent('N/A');
});
render(
ProviderWrapper({
children: <TableChart {...props} sticky={false} />,
}),
);
const cells = document.querySelectorAll('td');
expect(document.querySelectorAll('th')[0]).toHaveTextContent('num');
expect(cells[0]).toHaveTextContent('1234');
expect(cells[1]).toHaveTextContent('10000');
expect(cells[1]).toHaveTextContent('0');
});
it('render raw data with currencies', () => {
const props = transformProps({
...testData.raw,
rawFormData: {
...testData.raw.rawFormData,
column_config: {
num: {
currencyFormat: { symbol: 'USD', symbolPosition: 'prefix' },
it('render advanced data', () => {
render(
<ThemeProvider theme={supersetTheme}>
<TableChart {...transformProps(testData.advanced)} sticky={false} />
,
</ThemeProvider>,
);
const secondColumnHeader = screen.getByText('Sum of Num');
expect(secondColumnHeader).toBeInTheDocument();
expect(secondColumnHeader?.getAttribute('data-column-name')).toEqual(
'1',
);
const firstDataRow = screen.getAllByRole('rowgroup')[1];
const cells = firstDataRow.querySelectorAll('td');
expect(cells[0]).toHaveTextContent('Michael');
expect(cells[2]).toHaveTextContent('12.346%');
expect(cells[4]).toHaveTextContent('2.47k');
});
it('render advanced data with currencies', () => {
render(
ProviderWrapper({
children: (
<TableChart
{...transformProps(testData.advancedWithCurrency)}
sticky={false}
/>
),
}),
);
const cells = document.querySelectorAll('td');
expect(document.querySelectorAll('th')[1]).toHaveTextContent(
'Sum of Num',
);
expect(cells[0]).toHaveTextContent('Michael');
expect(cells[2]).toHaveTextContent('12.346%');
expect(cells[4]).toHaveTextContent('$ 2.47k');
});
it('render raw data', () => {
const props = transformProps({
...testData.raw,
rawFormData: { ...testData.raw.rawFormData },
});
render(
ProviderWrapper({
children: <TableChart {...props} sticky={false} />,
}),
);
const cells = document.querySelectorAll('td');
expect(document.querySelectorAll('th')[0]).toHaveTextContent('num');
expect(cells[0]).toHaveTextContent('1234');
expect(cells[1]).toHaveTextContent('10000');
expect(cells[1]).toHaveTextContent('0');
});
it('render raw data with currencies', () => {
const props = transformProps({
...testData.raw,
rawFormData: {
...testData.raw.rawFormData,
column_config: {
num: {
currencyFormat: { symbol: 'USD', symbolPosition: 'prefix' },
},
},
},
},
});
render(
ProviderWrapper({
children: <TableChart {...props} sticky={false} />,
}),
);
const cells = document.querySelectorAll('td');
expect(document.querySelectorAll('th')[0]).toHaveTextContent('num');
expect(cells[0]).toHaveTextContent('$ 1.23k');
expect(cells[1]).toHaveTextContent('$ 10k');
expect(cells[2]).toHaveTextContent('$ 0');
});
render(
ProviderWrapper({
children: <TableChart {...props} sticky={false} />,
}),
);
const cells = document.querySelectorAll('td');
expect(document.querySelectorAll('th')[0]).toHaveTextContent('num');
expect(cells[0]).toHaveTextContent('$ 1.23k');
expect(cells[1]).toHaveTextContent('$ 10k');
expect(cells[2]).toHaveTextContent('$ 0');
});
it('render small formatted data with currencies', () => {
const props = transformProps({
...testData.raw,
rawFormData: {
...testData.raw.rawFormData,
column_config: {
num: {
d3SmallNumberFormat: '.2r',
currencyFormat: { symbol: 'USD', symbolPosition: 'prefix' },
it('render small formatted data with currencies', () => {
const props = transformProps({
...testData.raw,
rawFormData: {
...testData.raw.rawFormData,
column_config: {
num: {
d3SmallNumberFormat: '.2r',
currencyFormat: { symbol: 'USD', symbolPosition: 'prefix' },
},
},
},
},
queriesData: [
{
...testData.raw.queriesData[0],
data: [
{
num: 1234,
},
{
num: 0.5,
},
{
num: 0.61234,
},
],
},
],
});
render(
ProviderWrapper({
children: <TableChart {...props} sticky={false} />,
}),
);
const cells = document.querySelectorAll('td');
expect(document.querySelectorAll('th')[0]).toHaveTextContent('num');
expect(cells[0]).toHaveTextContent('$ 1.23k');
expect(cells[1]).toHaveTextContent('$ 0.50');
expect(cells[2]).toHaveTextContent('$ 0.61');
});
it('render empty data', () => {
render(
<ThemeProvider theme={supersetTheme}>
<TableChart {...transformProps(testData.empty)} sticky={false} />,
</ThemeProvider>,
);
expect(screen.getByText('No records found')).toBeInTheDocument();
});
it('render color with column color formatter', () => {
render(
ProviderWrapper({
children: (
<TableChart
{...transformProps({
...testData.advanced,
rawFormData: {
...testData.advanced.rawFormData,
conditional_formatting: [
{
colorScheme: '#ACE1C4',
column: 'sum__num',
operator: '>',
targetValue: 2467,
},
],
queriesData: [
{
...testData.raw.queriesData[0],
data: [
{
num: 1234,
},
})}
/>
),
}),
);
{
num: 0.5,
},
{
num: 0.61234,
},
],
},
],
});
render(
ProviderWrapper({
children: <TableChart {...props} sticky={false} />,
}),
);
const cells = document.querySelectorAll('td');
expect(getComputedStyle(screen.getByTitle('2467063')).background).toBe(
'rgba(172, 225, 196, 1)',
);
expect(getComputedStyle(screen.getByTitle('2467')).background).toBe('');
});
it('render cell without color', () => {
const dataWithEmptyCell = testData.advanced.queriesData[0];
dataWithEmptyCell.data.push({
__timestamp: null,
name: 'Noah',
sum__num: null,
'%pct_nice': 0.643,
'abc.com': 'bazzinga',
expect(document.querySelectorAll('th')[0]).toHaveTextContent('num');
expect(cells[0]).toHaveTextContent('$ 1.23k');
expect(cells[1]).toHaveTextContent('$ 0.50');
expect(cells[2]).toHaveTextContent('$ 0.61');
});
render(
ProviderWrapper({
children: (
<TableChart
{...transformProps({
...testData.advanced,
queriesData: [dataWithEmptyCell],
rawFormData: {
...testData.advanced.rawFormData,
conditional_formatting: [
{
colorScheme: '#ACE1C4',
column: 'sum__num',
operator: '<',
targetValue: 12342,
},
],
},
})}
/>
),
}),
);
expect(getComputedStyle(screen.getByTitle('2467')).background).toBe(
'rgba(172, 225, 196, 0.812)',
);
expect(getComputedStyle(screen.getByTitle('2467063')).background).toBe(
'',
);
expect(getComputedStyle(screen.getByText('N/A')).background).toBe('');
});
it('should display originalLabel in grouped headers', () => {
render(
<ThemeProvider theme={supersetTheme}>
<TableChart {...transformProps(testData.comparison)} sticky={false} />
</ThemeProvider>,
);
it('render empty data', () => {
render(
<ThemeProvider theme={supersetTheme}>
<TableChart {...transformProps(testData.empty)} sticky={false} />,
</ThemeProvider>,
);
expect(screen.getByText('No records found')).toBeInTheDocument();
});
const groupHeaders = screen.getAllByRole('columnheader');
expect(groupHeaders[0]).toHaveTextContent('metric_1');
expect(groupHeaders[1]).toHaveTextContent('metric_2');
});
});
it('render color with column color formatter', () => {
render(
ProviderWrapper({
children: (
<TableChart
{...transformProps({
...testData.advanced,
rawFormData: {
...testData.advanced.rawFormData,
conditional_formatting: [
{
colorScheme: '#ACE1C4',
column: 'sum__num',
operator: '>',
targetValue: 2467,
},
],
},
})}
/>
),
}),
);
it('render cell bars properly, and only when it is toggled on in both regular and percent metrics', () => {
const props = transformProps({
...testData.raw,
rawFormData: { ...testData.raw.rawFormData },
});
expect(getComputedStyle(screen.getByTitle('2467063')).background).toBe(
'rgba(172, 225, 196, 1)',
);
expect(getComputedStyle(screen.getByTitle('2467')).background).toBe('');
});
props.columns[0].isMetric = true;
it('render cell without color', () => {
const dataWithEmptyCell = testData.advanced.queriesData[0];
dataWithEmptyCell.data.push({
__timestamp: null,
name: 'Noah',
sum__num: null,
'%pct_nice': 0.643,
'abc.com': 'bazzinga',
});
render(
ProviderWrapper({
children: <TableChart {...props} sticky={false} />,
}),
);
let cells = document.querySelectorAll('div.cell-bar');
cells.forEach(cell => {
expect(cell).toHaveClass('positive');
});
props.columns[0].isMetric = false;
props.columns[0].isPercentMetric = true;
render(
ProviderWrapper({
children: (
<TableChart
{...transformProps({
...testData.advanced,
queriesData: [dataWithEmptyCell],
rawFormData: {
...testData.advanced.rawFormData,
conditional_formatting: [
{
colorScheme: '#ACE1C4',
column: 'sum__num',
operator: '<',
targetValue: 12342,
},
],
},
})}
/>
),
}),
);
expect(getComputedStyle(screen.getByTitle('2467')).background).toBe(
'rgba(172, 225, 196, 0.812)',
);
expect(getComputedStyle(screen.getByTitle('2467063')).background).toBe(
'',
);
expect(getComputedStyle(screen.getByText('N/A')).background).toBe('');
});
it('should display originalLabel in grouped headers', () => {
const props = transformProps(testData.comparison);
render(
ProviderWrapper({
children: <TableChart {...props} sticky={false} />,
}),
);
cells = document.querySelectorAll('div.cell-bar');
cells.forEach(cell => {
expect(cell).toHaveClass('positive');
});
render(
<ThemeProvider theme={supersetTheme}>
<TableChart {...props} sticky={false} />
</ThemeProvider>,
);
const groupHeaders = screen.getAllByRole('columnheader');
expect(groupHeaders.length).toBeGreaterThan(0);
const hasMetricHeaders = groupHeaders.some(
header =>
header.textContent &&
(header.textContent.includes('metric') ||
header.textContent.includes('Metric')),
);
expect(hasMetricHeaders).toBe(true);
});
props.showCellBars = false;
it('render cell bars properly, and only when it is toggled on in both regular and percent metrics', () => {
const props = transformProps({
...testData.raw,
rawFormData: { ...testData.raw.rawFormData },
});
render(
ProviderWrapper({
children: <TableChart {...props} sticky={false} />,
}),
);
cells = document.querySelectorAll('td');
props.columns[0].isMetric = true;
cells.forEach(cell => {
expect(cell).toHaveClass('test-c7w8t3');
});
render(
ProviderWrapper({
children: <TableChart {...props} sticky={false} />,
}),
);
let cells = document.querySelectorAll('div.cell-bar');
cells.forEach(cell => {
expect(cell).toHaveClass('positive');
});
props.columns[0].isMetric = false;
props.columns[0].isPercentMetric = true;
props.columns[0].isPercentMetric = false;
props.columns[0].isMetric = true;
render(
ProviderWrapper({
children: <TableChart {...props} sticky={false} />,
}),
);
cells = document.querySelectorAll('div.cell-bar');
cells.forEach(cell => {
expect(cell).toHaveClass('positive');
});
render(
ProviderWrapper({
children: <TableChart {...props} sticky={false} />,
}),
);
cells = document.querySelectorAll('td');
cells.forEach(cell => {
expect(cell).toHaveClass('test-c7w8t3');
props.showCellBars = false;
render(
ProviderWrapper({
children: <TableChart {...props} sticky={false} />,
}),
);
cells = document.querySelectorAll('td');
props.columns[0].isPercentMetric = false;
props.columns[0].isMetric = true;
render(
ProviderWrapper({
children: <TableChart {...props} sticky={false} />,
}),
);
cells = document.querySelectorAll('td');
});
});
});
});