mirror of
https://github.com/apache/superset.git
synced 2026-04-19 08:04:53 +00:00
feat(table): Table with Time Comparison (#28057)
Co-authored-by: Lily Kuang <lily@preset.io> Co-authored-by: lilykuang <jialikuang@gmail.com> Co-authored-by: Kamil Gabryjelski <kamil.gabryjelski@gmail.com>
This commit is contained in:
@@ -37,6 +37,7 @@
|
||||
"xss": "^1.0.15"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ant-design/icons": "^5.0.1",
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"@testing-library/dom": "^7.29.4",
|
||||
|
||||
@@ -69,6 +69,8 @@ export interface DataTableProps<D extends object> extends TableOptions<D> {
|
||||
rowCount: number;
|
||||
wrapperRef?: MutableRefObject<HTMLDivElement>;
|
||||
onColumnOrderChange: () => void;
|
||||
renderGroupingHeaders?: () => JSX.Element;
|
||||
renderTimeComparisonDropdown?: () => JSX.Element;
|
||||
}
|
||||
|
||||
export interface RenderHTMLCellProps extends HTMLProps<HTMLTableCellElement> {
|
||||
@@ -101,6 +103,8 @@ export default typedMemo(function DataTable<D extends object>({
|
||||
serverPagination,
|
||||
wrapperRef: userWrapperRef,
|
||||
onColumnOrderChange,
|
||||
renderGroupingHeaders,
|
||||
renderTimeComparisonDropdown,
|
||||
...moreUseTableOptions
|
||||
}: DataTableProps<D>): JSX.Element {
|
||||
const tableHooks: PluginHook<D>[] = [
|
||||
@@ -117,7 +121,8 @@ export default typedMemo(function DataTable<D extends object>({
|
||||
const sortByRef = useRef([]); // cache initial `sortby` so sorting doesn't trigger page reset
|
||||
const pageSizeRef = useRef([initialPageSize, resultsSize]);
|
||||
const hasPagination = initialPageSize > 0 && resultsSize > 0; // pageSize == 0 means no pagination
|
||||
const hasGlobalControl = hasPagination || !!searchInput;
|
||||
const hasGlobalControl =
|
||||
hasPagination || !!searchInput || renderTimeComparisonDropdown;
|
||||
const initialState = {
|
||||
...initialState_,
|
||||
// zero length means all pages, the `usePagination` plugin does not
|
||||
@@ -253,6 +258,7 @@ export default typedMemo(function DataTable<D extends object>({
|
||||
const renderTable = () => (
|
||||
<table {...getTableProps({ className: tableClassName })}>
|
||||
<thead>
|
||||
{renderGroupingHeaders ? renderGroupingHeaders() : null}
|
||||
{headerGroups.map(headerGroup => {
|
||||
const { key: headerGroupKey, ...headerGroupProps } =
|
||||
headerGroup.getHeaderGroupProps();
|
||||
@@ -357,7 +363,9 @@ export default typedMemo(function DataTable<D extends object>({
|
||||
{hasGlobalControl ? (
|
||||
<div ref={globalControlRef} className="form-inline dt-controls">
|
||||
<div className="row">
|
||||
<div className="col-sm-6">
|
||||
<div
|
||||
className={renderTimeComparisonDropdown ? 'col-sm-5' : 'col-sm-6'}
|
||||
>
|
||||
{hasPagination ? (
|
||||
<SelectPageSize
|
||||
total={resultsSize}
|
||||
@@ -384,6 +392,14 @@ export default typedMemo(function DataTable<D extends object>({
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
{renderTimeComparisonDropdown ? (
|
||||
<div
|
||||
className="col-sm-1"
|
||||
style={{ float: 'right', marginTop: '6px' }}
|
||||
>
|
||||
{renderTimeComparisonDropdown()}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
@@ -183,7 +183,9 @@ function StickyWrap({
|
||||
}
|
||||
const fullTableHeight = (bodyThead.parentNode as HTMLTableElement)
|
||||
.clientHeight;
|
||||
const ths = bodyThead.childNodes[0]
|
||||
// instead of always using the first tr, we use the last one to support
|
||||
// multi-level headers assuming the last one is the more detailed one
|
||||
const ths = bodyThead.childNodes?.[bodyThead.childNodes?.length - 1 || 0]
|
||||
.childNodes as NodeListOf<HTMLTableHeaderCellElement>;
|
||||
const widths = Array.from(ths).map(
|
||||
th => th.getBoundingClientRect()?.width || th.clientWidth,
|
||||
|
||||
@@ -111,5 +111,12 @@ export default styled.div`
|
||||
text-align: center;
|
||||
padding: 1em 0.6em;
|
||||
}
|
||||
|
||||
.right-border-only {
|
||||
border-right: 2px solid ${theme.colors.grayscale.light2};
|
||||
}
|
||||
table .right-border-only:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
@@ -50,9 +50,23 @@ import {
|
||||
css,
|
||||
t,
|
||||
tn,
|
||||
useTheme,
|
||||
} from '@superset-ui/core';
|
||||
import { Dropdown, Menu } from '@superset-ui/chart-controls';
|
||||
import {
|
||||
CheckOutlined,
|
||||
DownOutlined,
|
||||
MinusCircleOutlined,
|
||||
PlusCircleOutlined,
|
||||
TableOutlined,
|
||||
} from '@ant-design/icons';
|
||||
|
||||
import { DataColumnMeta, TableChartTransformedProps } from './types';
|
||||
import { isEmpty } from 'lodash';
|
||||
import {
|
||||
ColorSchemeEnum,
|
||||
DataColumnMeta,
|
||||
TableChartTransformedProps,
|
||||
} from './types';
|
||||
import DataTable, {
|
||||
DataTableProps,
|
||||
SearchInputProps,
|
||||
@@ -242,7 +256,16 @@ export default function TableChart<D extends DataRecord = DataRecord>(
|
||||
allowRenderHtml = true,
|
||||
onContextMenu,
|
||||
emitCrossFilters,
|
||||
isUsingTimeComparison,
|
||||
basicColorFormatters,
|
||||
basicColorColumnFormatters,
|
||||
} = props;
|
||||
const comparisonColumns = [
|
||||
{ key: 'all', label: t('Display all') },
|
||||
{ key: '#', label: '#' },
|
||||
{ key: '△', label: '△' },
|
||||
{ key: '%', label: '%' },
|
||||
];
|
||||
const timestampFormatter = useCallback(
|
||||
value => getTimeFormatterForGranularity(timeGrain)(value),
|
||||
[timeGrain],
|
||||
@@ -253,6 +276,12 @@ export default function TableChart<D extends DataRecord = DataRecord>(
|
||||
});
|
||||
// keep track of whether column order changed, so that column widths can too
|
||||
const [columnOrderToggle, setColumnOrderToggle] = useState(false);
|
||||
const [showComparisonDropdown, setShowComparisonDropdown] = useState(false);
|
||||
const [selectedComparisonColumns, setSelectedComparisonColumns] = useState([
|
||||
comparisonColumns[0].key,
|
||||
]);
|
||||
const [hideComparisonKeys, setHideComparisonKeys] = useState<string[]>([]);
|
||||
const theme = useTheme();
|
||||
|
||||
// only take relevant page size options
|
||||
const pageSizeOptions = useMemo(() => {
|
||||
@@ -362,16 +391,46 @@ export default function TableChart<D extends DataRecord = DataRecord>(
|
||||
|
||||
const getSharedStyle = (column: DataColumnMeta): CSSProperties => {
|
||||
const { isNumeric, config = {} } = column;
|
||||
const textAlign = config.horizontalAlign
|
||||
? config.horizontalAlign
|
||||
: isNumeric
|
||||
? 'right'
|
||||
: 'left';
|
||||
const textAlign =
|
||||
config.horizontalAlign ||
|
||||
(isNumeric && !isUsingTimeComparison ? 'right' : 'left');
|
||||
return {
|
||||
textAlign,
|
||||
};
|
||||
};
|
||||
|
||||
const comparisonLabels = [t('Main'), '#', '△', '%'];
|
||||
const filteredColumnsMeta = useMemo(() => {
|
||||
if (!isUsingTimeComparison) {
|
||||
return columnsMeta;
|
||||
}
|
||||
const allColumns = comparisonColumns[0].key;
|
||||
const main = comparisonLabels[0];
|
||||
const showAllColumns = selectedComparisonColumns.includes(allColumns);
|
||||
|
||||
return columnsMeta.filter(({ label, key }) => {
|
||||
// Extract the key portion after the space, assuming the format is always "label key"
|
||||
const keyPortion = key.substring(label.length);
|
||||
const isKeyHidded = hideComparisonKeys.includes(keyPortion);
|
||||
const isLableMain = label === main;
|
||||
|
||||
return (
|
||||
isLableMain ||
|
||||
(!isKeyHidded &&
|
||||
(!comparisonLabels.includes(label) ||
|
||||
showAllColumns ||
|
||||
selectedComparisonColumns.includes(label)))
|
||||
);
|
||||
});
|
||||
}, [
|
||||
columnsMeta,
|
||||
comparisonColumns,
|
||||
comparisonLabels,
|
||||
isUsingTimeComparison,
|
||||
hideComparisonKeys,
|
||||
selectedComparisonColumns,
|
||||
]);
|
||||
|
||||
const handleContextMenu =
|
||||
onContextMenu && !isRawRecords
|
||||
? (
|
||||
@@ -385,7 +444,7 @@ export default function TableChart<D extends DataRecord = DataRecord>(
|
||||
clientY: number,
|
||||
) => {
|
||||
const drillToDetailFilters: BinaryQueryObjectFilterClause[] = [];
|
||||
columnsMeta.forEach(col => {
|
||||
filteredColumnsMeta.forEach(col => {
|
||||
if (!col.isMetric) {
|
||||
const dataRecordValue = value[col.key];
|
||||
drillToDetailFilters.push({
|
||||
@@ -417,6 +476,198 @@ export default function TableChart<D extends DataRecord = DataRecord>(
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const getHeaderColumns = (
|
||||
columnsMeta: DataColumnMeta[],
|
||||
enableTimeComparison?: boolean,
|
||||
) => {
|
||||
const resultMap: Record<string, number[]> = {};
|
||||
|
||||
if (!enableTimeComparison) {
|
||||
return resultMap;
|
||||
}
|
||||
|
||||
columnsMeta.forEach((element, index) => {
|
||||
// Check if element's label is one of the comparison labels
|
||||
if (comparisonLabels.includes(element.label)) {
|
||||
// Extract the key portion after the space, assuming the format is always "label key"
|
||||
const keyPortion = element.key.substring(element.label.length);
|
||||
|
||||
// If the key portion is not in the map, initialize it with the current index
|
||||
if (!resultMap[keyPortion]) {
|
||||
resultMap[keyPortion] = [index];
|
||||
} else {
|
||||
// Add the index to the existing array
|
||||
resultMap[keyPortion].push(index);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return resultMap;
|
||||
};
|
||||
|
||||
const renderTimeComparisonDropdown = (): JSX.Element => {
|
||||
const allKey = comparisonColumns[0].key;
|
||||
const handleOnClick = (data: any) => {
|
||||
const { key } = data;
|
||||
// Toggle 'All' key selection
|
||||
if (key === allKey) {
|
||||
setSelectedComparisonColumns([allKey]);
|
||||
} else if (selectedComparisonColumns.includes(allKey)) {
|
||||
setSelectedComparisonColumns([key]);
|
||||
} else {
|
||||
// Toggle selection for other keys
|
||||
setSelectedComparisonColumns(
|
||||
selectedComparisonColumns.includes(key)
|
||||
? selectedComparisonColumns.filter(k => k !== key) // Deselect if already selected
|
||||
: [...selectedComparisonColumns, key],
|
||||
); // Select if not already selected
|
||||
}
|
||||
};
|
||||
|
||||
const handleOnBlur = () => {
|
||||
if (selectedComparisonColumns.length === 3) {
|
||||
setSelectedComparisonColumns([comparisonColumns[0].key]);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
placement="bottomRight"
|
||||
visible={showComparisonDropdown}
|
||||
onVisibleChange={(flag: boolean) => {
|
||||
setShowComparisonDropdown(flag);
|
||||
}}
|
||||
overlay={
|
||||
<Menu
|
||||
multiple
|
||||
onClick={handleOnClick}
|
||||
onBlur={handleOnBlur}
|
||||
selectedKeys={selectedComparisonColumns}
|
||||
>
|
||||
<div
|
||||
css={css`
|
||||
max-width: 242px;
|
||||
padding: 0 ${theme.gridUnit * 2}px;
|
||||
color: ${theme.colors.grayscale.base};
|
||||
font-size: ${theme.typography.sizes.s}px;
|
||||
`}
|
||||
>
|
||||
{t(
|
||||
'Select columns that will be displayed in the table. You can multiselect columns.',
|
||||
)}
|
||||
</div>
|
||||
{comparisonColumns.map(column => (
|
||||
<Menu.Item key={column.key}>
|
||||
<span
|
||||
css={css`
|
||||
color: ${theme.colors.grayscale.dark2};
|
||||
`}
|
||||
>
|
||||
{column.label}
|
||||
</span>
|
||||
<span
|
||||
css={css`
|
||||
float: right;
|
||||
font-size: ${theme.typography.sizes.s}px;
|
||||
`}
|
||||
>
|
||||
{selectedComparisonColumns.includes(column.key) && (
|
||||
<CheckOutlined />
|
||||
)}
|
||||
</span>
|
||||
</Menu.Item>
|
||||
))}
|
||||
</Menu>
|
||||
}
|
||||
trigger={['click']}
|
||||
>
|
||||
<span>
|
||||
<TableOutlined /> <DownOutlined />
|
||||
</span>
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
|
||||
const renderGroupingHeaders = (): JSX.Element => {
|
||||
// TODO: Make use of ColumnGroup to render the aditional headers
|
||||
const headers: any = [];
|
||||
let currentColumnIndex = 0;
|
||||
|
||||
Object.entries(groupHeaderColumns || {}).forEach(([key, value]) => {
|
||||
// Calculate the number of placeholder columns needed before the current header
|
||||
const startPosition = value[0];
|
||||
const colSpan = value.length;
|
||||
|
||||
// Add placeholder <th> for columns before this header
|
||||
for (let i = currentColumnIndex; i < startPosition; i += 1) {
|
||||
headers.push(
|
||||
<th
|
||||
key={`placeholder-${i}`}
|
||||
style={{ borderBottom: 0 }}
|
||||
aria-label={`Header-${i}`}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
// Add the current header <th>
|
||||
headers.push(
|
||||
<th key={`header-${key}`} colSpan={colSpan} style={{ borderBottom: 0 }}>
|
||||
{key}
|
||||
<span
|
||||
css={css`
|
||||
float: right;
|
||||
& svg {
|
||||
color: ${theme.colors.grayscale.base} !important;
|
||||
}
|
||||
`}
|
||||
>
|
||||
{hideComparisonKeys.includes(key) ? (
|
||||
<PlusCircleOutlined
|
||||
onClick={() =>
|
||||
setHideComparisonKeys(
|
||||
hideComparisonKeys.filter(k => k !== key),
|
||||
)
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<MinusCircleOutlined
|
||||
onClick={() =>
|
||||
setHideComparisonKeys([...hideComparisonKeys, key])
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
</th>,
|
||||
);
|
||||
|
||||
// Update the current column index
|
||||
currentColumnIndex = startPosition + colSpan;
|
||||
});
|
||||
|
||||
return (
|
||||
<tr
|
||||
css={css`
|
||||
th {
|
||||
border-right: 2px solid ${theme.colors.grayscale.light2};
|
||||
}
|
||||
th:first-child {
|
||||
border-left: none;
|
||||
}
|
||||
th:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
`}
|
||||
>
|
||||
{headers}
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
|
||||
const groupHeaderColumns = useMemo(
|
||||
() => getHeaderColumns(filteredColumnsMeta, isUsingTimeComparison),
|
||||
[filteredColumnsMeta, isUsingTimeComparison],
|
||||
);
|
||||
|
||||
const getColumnConfigs = useCallback(
|
||||
(column: DataColumnMeta, i: number): ColumnWithLooseAccessor<D> => {
|
||||
const {
|
||||
@@ -451,7 +702,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
|
||||
@@ -464,6 +721,16 @@ export default function TableChart<D extends DataRecord = DataRecord>(
|
||||
className += ' dt-is-filter';
|
||||
}
|
||||
|
||||
if (!isMetric && !isPercentMetric) {
|
||||
className += ' right-border-only';
|
||||
} else if (comparisonLabels.includes(label)) {
|
||||
const groupinHeader = key.substring(label.length);
|
||||
const columnsUnderHeader = groupHeaderColumns[groupinHeader] || [];
|
||||
if (i === columnsUnderHeader[columnsUnderHeader.length - 1]) {
|
||||
className += ' right-border-only';
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: String(i), // to allow duplicate column keys
|
||||
// must use custom accessor to allow `.` in column names
|
||||
@@ -475,6 +742,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)
|
||||
@@ -489,6 +767,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};
|
||||
@@ -520,6 +811,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',
|
||||
@@ -589,10 +902,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>
|
||||
);
|
||||
@@ -676,8 +993,8 @@ export default function TableChart<D extends DataRecord = DataRecord>(
|
||||
);
|
||||
|
||||
const columns = useMemo(
|
||||
() => columnsMeta.map(getColumnConfigs),
|
||||
[columnsMeta, getColumnConfigs],
|
||||
() => filteredColumnsMeta.map(getColumnConfigs),
|
||||
[filteredColumnsMeta, getColumnConfigs],
|
||||
);
|
||||
|
||||
const handleServerPaginationChange = useCallback(
|
||||
@@ -744,6 +1061,12 @@ export default function TableChart<D extends DataRecord = DataRecord>(
|
||||
selectPageSize={pageSize !== null && SelectPageSize}
|
||||
// not in use in Superset, but needed for unit tests
|
||||
sticky={sticky}
|
||||
renderGroupingHeaders={
|
||||
!isEmpty(groupHeaderColumns) ? renderGroupingHeaders : undefined
|
||||
}
|
||||
renderTimeComparisonDropdown={
|
||||
isUsingTimeComparison ? renderTimeComparisonDropdown : undefined
|
||||
}
|
||||
/>
|
||||
</Styles>
|
||||
);
|
||||
|
||||
@@ -21,13 +21,21 @@ import {
|
||||
buildQueryContext,
|
||||
ensureIsArray,
|
||||
getMetricLabel,
|
||||
getTimeOffset,
|
||||
isPhysicalColumn,
|
||||
parseDttmToDate,
|
||||
QueryMode,
|
||||
QueryObject,
|
||||
removeDuplicates,
|
||||
SimpleAdhocFilter,
|
||||
} from '@superset-ui/core';
|
||||
import { PostProcessingRule } from '@superset-ui/core/src/query/types/PostProcessing';
|
||||
import { BuildQuery } from '@superset-ui/core/src/chart/registries/ChartBuildQueryRegistrySingleton';
|
||||
import {
|
||||
isTimeComparison,
|
||||
timeCompareOperator,
|
||||
} from '@superset-ui/chart-controls';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { TableChartFormData } from './types';
|
||||
import { updateExternalFormData } from './DataTable/utils/externalAPIs';
|
||||
|
||||
@@ -69,9 +77,51 @@ const buildQuery: BuildQuery<TableChartFormData> = (
|
||||
};
|
||||
}
|
||||
|
||||
const addComparisonPercentMetrics = (metrics: string[], suffixes: string[]) =>
|
||||
metrics.reduce<string[]>((acc, metric) => {
|
||||
const newMetrics = suffixes.map(suffix => `${metric}__${suffix}`);
|
||||
return acc.concat([metric, ...newMetrics]);
|
||||
}, []);
|
||||
|
||||
return buildQueryContext(formDataCopy, baseQueryObject => {
|
||||
let { metrics, orderby = [], columns = [] } = baseQueryObject;
|
||||
const { extras = {} } = baseQueryObject;
|
||||
let postProcessing: PostProcessingRule[] = [];
|
||||
const TimeRangeFilters =
|
||||
formData.adhoc_filters?.filter(
|
||||
(filter: SimpleAdhocFilter) => filter.operator === 'TEMPORAL_RANGE',
|
||||
) || [];
|
||||
|
||||
// In case the viz is using all version of controls, we try to load them
|
||||
const previousCustomTimeRangeFilters: any =
|
||||
formData.adhoc_custom?.filter(
|
||||
(filter: SimpleAdhocFilter) => filter.operator === 'TEMPORAL_RANGE',
|
||||
) || [];
|
||||
|
||||
let previousCustomStartDate = '';
|
||||
if (
|
||||
!isEmpty(previousCustomTimeRangeFilters) &&
|
||||
previousCustomTimeRangeFilters[0]?.comparator !== 'No Filter'
|
||||
) {
|
||||
previousCustomStartDate =
|
||||
previousCustomTimeRangeFilters[0]?.comparator.split(' : ')[0];
|
||||
}
|
||||
|
||||
const timeOffsets = ensureIsArray(
|
||||
isTimeComparison(formData, baseQueryObject)
|
||||
? getTimeOffset({
|
||||
timeRangeFilter: TimeRangeFilters[0],
|
||||
shifts: formData.time_compare,
|
||||
startDate:
|
||||
previousCustomStartDate && !formData.start_date_offset
|
||||
? parseDttmToDate(previousCustomStartDate)?.toUTCString()
|
||||
: formData.start_date_offset,
|
||||
})
|
||||
: [],
|
||||
);
|
||||
|
||||
let temporalColumAdded = false;
|
||||
let temporalColum = null;
|
||||
|
||||
if (queryMode === QueryMode.Aggregate) {
|
||||
metrics = metrics || [];
|
||||
@@ -85,8 +135,17 @@ const buildQuery: BuildQuery<TableChartFormData> = (
|
||||
}
|
||||
// add postprocessing for percent metrics only when in aggregation mode
|
||||
if (percentMetrics && percentMetrics.length > 0) {
|
||||
const percentMetricsLabelsWithTimeComparison = isTimeComparison(
|
||||
formData,
|
||||
baseQueryObject,
|
||||
)
|
||||
? addComparisonPercentMetrics(
|
||||
percentMetrics.map(getMetricLabel),
|
||||
timeOffsets,
|
||||
)
|
||||
: percentMetrics.map(getMetricLabel);
|
||||
const percentMetricLabels = removeDuplicates(
|
||||
percentMetrics.map(getMetricLabel),
|
||||
percentMetricsLabelsWithTimeComparison,
|
||||
);
|
||||
metrics = removeDuplicates(
|
||||
metrics.concat(percentMetrics),
|
||||
@@ -102,23 +161,38 @@ const buildQuery: BuildQuery<TableChartFormData> = (
|
||||
},
|
||||
];
|
||||
}
|
||||
// Add the operator for the time comparison if some is selected
|
||||
if (!isEmpty(timeOffsets)) {
|
||||
postProcessing.push(timeCompareOperator(formData, baseQueryObject));
|
||||
}
|
||||
|
||||
columns = columns.map(col => {
|
||||
if (
|
||||
const temporalColumnsLookup = formData?.temporal_columns_lookup;
|
||||
// Filter out the column if needed and prepare the temporal column object
|
||||
|
||||
columns = columns.filter(col => {
|
||||
const shouldBeAdded =
|
||||
isPhysicalColumn(col) &&
|
||||
time_grain_sqla &&
|
||||
formData?.temporal_columns_lookup?.[col]
|
||||
) {
|
||||
return {
|
||||
temporalColumnsLookup?.[col];
|
||||
|
||||
if (shouldBeAdded && !temporalColumAdded) {
|
||||
temporalColum = {
|
||||
timeGrain: time_grain_sqla,
|
||||
columnType: 'BASE_AXIS',
|
||||
sqlExpression: col,
|
||||
label: col,
|
||||
expressionType: 'SQL',
|
||||
} as AdhocColumn;
|
||||
temporalColumAdded = true;
|
||||
return false; // Do not include this in the output; it's added separately
|
||||
}
|
||||
return col;
|
||||
return true;
|
||||
});
|
||||
|
||||
// So we ensure the temporal column is added first
|
||||
if (temporalColum) {
|
||||
columns = [temporalColum, ...columns];
|
||||
}
|
||||
}
|
||||
|
||||
const moreProps: Partial<QueryObject> = {};
|
||||
@@ -133,9 +207,11 @@ const buildQuery: BuildQuery<TableChartFormData> = (
|
||||
let queryObject = {
|
||||
...baseQueryObject,
|
||||
columns,
|
||||
extras: !isEmpty(timeOffsets) && !temporalColum ? {} : extras,
|
||||
orderby,
|
||||
metrics,
|
||||
post_processing: postProcessing,
|
||||
time_offsets: timeOffsets,
|
||||
...moreProps,
|
||||
};
|
||||
|
||||
@@ -169,6 +245,7 @@ const buildQuery: BuildQuery<TableChartFormData> = (
|
||||
row_limit: 0,
|
||||
row_offset: 0,
|
||||
post_processing: [],
|
||||
extras: undefined, // we don't need time grain here
|
||||
order_desc: undefined, // we don't need orderby stuff here,
|
||||
orderby: undefined, // because this query will be used for get total aggregation.
|
||||
});
|
||||
@@ -186,6 +263,7 @@ const buildQuery: BuildQuery<TableChartFormData> = (
|
||||
{ ...queryObject },
|
||||
{
|
||||
...queryObject,
|
||||
time_offsets: [],
|
||||
row_limit: 0,
|
||||
row_offset: 0,
|
||||
post_processing: [],
|
||||
|
||||
@@ -43,9 +43,12 @@ import {
|
||||
ColumnMeta,
|
||||
defineSavedMetrics,
|
||||
getStandardizedControls,
|
||||
sections,
|
||||
} from '@superset-ui/chart-controls';
|
||||
|
||||
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;
|
||||
@@ -142,6 +145,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: [
|
||||
{
|
||||
@@ -397,44 +427,6 @@ const config: ControlPanelConfig = {
|
||||
description: t('Whether to include a client-side search box'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'show_cell_bars',
|
||||
config: {
|
||||
type: 'CheckboxControl',
|
||||
label: t('Cell bars'),
|
||||
renderTrigger: true,
|
||||
default: true,
|
||||
description: t(
|
||||
'Whether to display a bar chart background in table columns',
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'align_pn',
|
||||
config: {
|
||||
type: 'CheckboxControl',
|
||||
label: t('Align +/-'),
|
||||
renderTrigger: true,
|
||||
default: false,
|
||||
description: t(
|
||||
'Whether to align background charts with both positive and negative values at 0',
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'color_pn',
|
||||
config: {
|
||||
type: 'CheckboxControl',
|
||||
label: t('Color +/-'),
|
||||
renderTrigger: true,
|
||||
default: true,
|
||||
description: t(
|
||||
'Whether to colorize numeric values by whether they are positive or negative',
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
@@ -447,6 +439,8 @@ const config: ControlPanelConfig = {
|
||||
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),
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -485,13 +479,111 @@ const config: ControlPanelConfig = {
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: t('Visual formatting'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
[
|
||||
{
|
||||
name: 'show_cell_bars',
|
||||
config: {
|
||||
type: 'CheckboxControl',
|
||||
label: t('Show Cell bars'),
|
||||
renderTrigger: true,
|
||||
default: true,
|
||||
description: t(
|
||||
'Whether to display a bar chart background in table columns',
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'align_pn',
|
||||
config: {
|
||||
type: 'CheckboxControl',
|
||||
label: t('Align +/-'),
|
||||
renderTrigger: true,
|
||||
default: false,
|
||||
description: t(
|
||||
'Whether to align background charts with both positive and negative values at 0',
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'color_pn',
|
||||
config: {
|
||||
type: 'CheckboxControl',
|
||||
label: t('add colors to cell bars for +/-'),
|
||||
renderTrigger: true,
|
||||
default: true,
|
||||
description: t(
|
||||
'Whether to colorize numeric values by whether they are positive or negative',
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'comparison_color_enabled',
|
||||
config: {
|
||||
type: 'CheckboxControl',
|
||||
label: t('basic conditional formatting'),
|
||||
renderTrigger: true,
|
||||
visibility: ({ controls }) =>
|
||||
!isEmpty(controls?.time_compare?.value),
|
||||
default: false,
|
||||
description: t(
|
||||
'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.',
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'comparison_color_scheme',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
label: t('color type'),
|
||||
default: ColorSchemeEnum.Green,
|
||||
renderTrigger: true,
|
||||
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.',
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'conditional_formatting',
|
||||
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',
|
||||
),
|
||||
@@ -519,9 +611,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,
|
||||
};
|
||||
},
|
||||
@@ -530,6 +631,14 @@ const config: ControlPanelConfig = {
|
||||
],
|
||||
],
|
||||
},
|
||||
{
|
||||
...sections.timeComparisonControls({
|
||||
multi: false,
|
||||
showCalculationType: false,
|
||||
showFullChoices: false,
|
||||
}),
|
||||
visibility: isAggMode,
|
||||
},
|
||||
],
|
||||
formDataOverrides: formData => ({
|
||||
...formData,
|
||||
|
||||
@@ -18,8 +18,10 @@
|
||||
*/
|
||||
import memoizeOne from 'memoize-one';
|
||||
import {
|
||||
ComparisonType,
|
||||
CurrencyFormatter,
|
||||
DataRecord,
|
||||
ensureIsArray,
|
||||
extractTimegrain,
|
||||
GenericDataType,
|
||||
getMetricLabel,
|
||||
@@ -28,18 +30,26 @@ import {
|
||||
getTimeFormatterForGranularity,
|
||||
NumberFormats,
|
||||
QueryMode,
|
||||
t,
|
||||
SMART_DATE_ID,
|
||||
TimeFormats,
|
||||
TimeFormatter,
|
||||
SimpleAdhocFilter,
|
||||
getTimeOffset,
|
||||
parseDttmToDate,
|
||||
} from '@superset-ui/core';
|
||||
import {
|
||||
ColorFormatters,
|
||||
ConditionalFormattingConfig,
|
||||
getColorFormatters,
|
||||
} from '@superset-ui/chart-controls';
|
||||
|
||||
import { isEmpty } from 'lodash';
|
||||
import isEqualColumns from './utils/isEqualColumns';
|
||||
import DateWithFormatter from './utils/DateWithFormatter';
|
||||
import {
|
||||
BasicColorFormatterType,
|
||||
ColorSchemeEnum,
|
||||
DataColumnMeta,
|
||||
TableChartProps,
|
||||
TableChartTransformedProps,
|
||||
@@ -81,6 +91,103 @@ const processDataRecords = memoizeOne(function processDataRecords(
|
||||
return data;
|
||||
});
|
||||
|
||||
const calculateDifferences = (
|
||||
originalValue: number,
|
||||
comparisonValue: number,
|
||||
) => {
|
||||
const valueDifference = originalValue - comparisonValue;
|
||||
let percentDifferenceNum;
|
||||
if (!originalValue && !comparisonValue) {
|
||||
percentDifferenceNum = 0;
|
||||
} else if (!originalValue || !comparisonValue) {
|
||||
percentDifferenceNum = originalValue ? 1 : -1;
|
||||
} else {
|
||||
percentDifferenceNum =
|
||||
(originalValue - comparisonValue) / Math.abs(comparisonValue);
|
||||
}
|
||||
return { valueDifference, percentDifferenceNum };
|
||||
};
|
||||
|
||||
const processComparisonTotals = (
|
||||
comparisonSuffix: string,
|
||||
totals?: DataRecord[],
|
||||
): DataRecord | undefined => {
|
||||
if (!totals) {
|
||||
return totals;
|
||||
}
|
||||
const transformedTotals: DataRecord = {};
|
||||
totals.map((totalRecord: DataRecord) =>
|
||||
Object.keys(totalRecord).forEach(key => {
|
||||
if (totalRecord[key] !== undefined && !key.includes(comparisonSuffix)) {
|
||||
transformedTotals[`Main ${key}`] =
|
||||
parseInt(transformedTotals[`Main ${key}`]?.toString() || '0', 10) +
|
||||
parseInt(totalRecord[key]?.toString() || '0', 10);
|
||||
transformedTotals[`# ${key}`] =
|
||||
parseInt(transformedTotals[`# ${key}`]?.toString() || '0', 10) +
|
||||
parseInt(
|
||||
totalRecord[`${key}__${comparisonSuffix}`]?.toString() || '0',
|
||||
10,
|
||||
);
|
||||
const { valueDifference, percentDifferenceNum } = calculateDifferences(
|
||||
transformedTotals[`Main ${key}`] as number,
|
||||
transformedTotals[`# ${key}`] as number,
|
||||
);
|
||||
transformedTotals[`△ ${key}`] = valueDifference;
|
||||
transformedTotals[`% ${key}`] = percentDifferenceNum;
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
return transformedTotals;
|
||||
};
|
||||
|
||||
const processComparisonDataRecords = memoizeOne(
|
||||
function processComparisonDataRecords(
|
||||
originalData: DataRecord[] | undefined,
|
||||
originalColumns: DataColumnMeta[],
|
||||
comparisonSuffix: string,
|
||||
) {
|
||||
// Transform data
|
||||
return originalData?.map(originalItem => {
|
||||
const transformedItem: DataRecord = {};
|
||||
originalColumns.forEach(origCol => {
|
||||
if (
|
||||
(origCol.isMetric || origCol.isPercentMetric) &&
|
||||
!origCol.key.includes(comparisonSuffix) &&
|
||||
origCol.isNumeric
|
||||
) {
|
||||
const originalValue = originalItem[origCol.key] || 0;
|
||||
const comparisonValue = origCol.isMetric
|
||||
? originalItem?.[`${origCol.key}__${comparisonSuffix}`] || 0
|
||||
: originalItem[`%${origCol.key.slice(1)}__${comparisonSuffix}`] ||
|
||||
0;
|
||||
const { valueDifference, percentDifferenceNum } =
|
||||
calculateDifferences(
|
||||
originalValue as number,
|
||||
comparisonValue as number,
|
||||
);
|
||||
|
||||
transformedItem[`Main ${origCol.key}`] = originalValue;
|
||||
transformedItem[`# ${origCol.key}`] = comparisonValue;
|
||||
transformedItem[`△ ${origCol.key}`] = valueDifference;
|
||||
transformedItem[`% ${origCol.key}`] = percentDifferenceNum;
|
||||
}
|
||||
});
|
||||
|
||||
Object.keys(originalItem).forEach(key => {
|
||||
const isMetricOrPercentMetric = originalColumns.some(
|
||||
col => col.key === key && (col.isMetric || col.isPercentMetric),
|
||||
);
|
||||
if (!isMetricOrPercentMetric) {
|
||||
transformedItem[key] = originalItem[key];
|
||||
}
|
||||
});
|
||||
|
||||
return transformedItem;
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
const processColumns = memoizeOne(function processColumns(
|
||||
props: TableChartProps,
|
||||
) {
|
||||
@@ -186,6 +293,60 @@ const processColumns = memoizeOne(function processColumns(
|
||||
];
|
||||
}, isEqualColumns);
|
||||
|
||||
const processComparisonColumns = (
|
||||
columns: DataColumnMeta[],
|
||||
props: TableChartProps,
|
||||
comparisonSuffix: string,
|
||||
) =>
|
||||
columns
|
||||
.map(col => {
|
||||
const {
|
||||
datasource: { columnFormats },
|
||||
rawFormData: { column_config: columnConfig = {} },
|
||||
} = props;
|
||||
const config = columnConfig[col.key] || {};
|
||||
const savedFormat = columnFormats?.[col.key];
|
||||
const numberFormat = config.d3NumberFormat || savedFormat;
|
||||
if (
|
||||
(col.isMetric || col.isPercentMetric) &&
|
||||
!col.key.includes(comparisonSuffix) &&
|
||||
col.isNumeric
|
||||
) {
|
||||
return [
|
||||
{
|
||||
...col,
|
||||
label: t('Main'),
|
||||
key: `${t('Main')} ${col.key}`,
|
||||
},
|
||||
{
|
||||
...col,
|
||||
label: `#`,
|
||||
key: `# ${col.key}`,
|
||||
},
|
||||
{
|
||||
...col,
|
||||
label: `△`,
|
||||
key: `△ ${col.key}`,
|
||||
},
|
||||
{
|
||||
...col,
|
||||
formatter: getNumberFormatter(numberFormat || PERCENT_3_POINT),
|
||||
label: `%`,
|
||||
key: `% ${col.key}`,
|
||||
},
|
||||
];
|
||||
}
|
||||
if (
|
||||
!col.isMetric &&
|
||||
!col.isPercentMetric &&
|
||||
!col.key.includes(comparisonSuffix)
|
||||
) {
|
||||
return [col];
|
||||
}
|
||||
return [];
|
||||
})
|
||||
.flat();
|
||||
|
||||
/**
|
||||
* Automatically set page size based on number of cells.
|
||||
*/
|
||||
@@ -239,10 +400,168 @@ const transformProps = (
|
||||
conditional_formatting: conditionalFormatting,
|
||||
allow_rearrange_columns: allowRearrangeColumns,
|
||||
allow_render_html: allowRenderHtml,
|
||||
time_compare,
|
||||
comparison_color_enabled: comparisonColorEnabled = false,
|
||||
comparison_color_scheme: comparisonColorScheme = ColorSchemeEnum.Green,
|
||||
comparison_type,
|
||||
} = formData;
|
||||
const isUsingTimeComparison =
|
||||
!isEmpty(time_compare) &&
|
||||
queryMode === QueryMode.Aggregate &&
|
||||
comparison_type === ComparisonType.Values;
|
||||
|
||||
const calculateBasicStyle = (
|
||||
percentDifferenceNum: number,
|
||||
colorOption: ColorSchemeEnum,
|
||||
) => {
|
||||
if (percentDifferenceNum === 0) {
|
||||
return {
|
||||
arrow: '',
|
||||
arrowColor: '',
|
||||
// eslint-disable-next-line theme-colors/no-literal-colors
|
||||
backgroundColor: 'rgba(0,0,0,0.2)',
|
||||
};
|
||||
}
|
||||
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(timeOffsets)[0]) &&
|
||||
origCol.isNumeric
|
||||
) {
|
||||
const originalValue = originalItem[origCol.key] || 0;
|
||||
const comparisonValue = origCol.isMetric
|
||||
? originalItem?.[
|
||||
`${origCol.key}__${ensureIsArray(timeOffsets)[0]}`
|
||||
] || 0
|
||||
: originalItem[
|
||||
`%${origCol.key.slice(1)}__${ensureIsArray(timeOffsets)[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 TimeRangeFilters =
|
||||
chartProps.rawFormData?.adhoc_filters?.filter(
|
||||
(filter: SimpleAdhocFilter) => filter.operator === 'TEMPORAL_RANGE',
|
||||
) || [];
|
||||
const previousCustomTimeRangeFilters: any =
|
||||
chartProps.rawFormData?.adhoc_custom?.filter(
|
||||
(filter: SimpleAdhocFilter) => filter.operator === 'TEMPORAL_RANGE',
|
||||
) || [];
|
||||
|
||||
let previousCustomStartDate = '';
|
||||
if (
|
||||
!isEmpty(previousCustomTimeRangeFilters) &&
|
||||
previousCustomTimeRangeFilters[0]?.comparator !== 'No Filter'
|
||||
) {
|
||||
previousCustomStartDate =
|
||||
previousCustomTimeRangeFilters[0]?.comparator.split(' : ')[0];
|
||||
}
|
||||
|
||||
const timeOffsets = getTimeOffset({
|
||||
timeRangeFilter: TimeRangeFilters[0],
|
||||
shifts: formData.time_compare,
|
||||
startDate:
|
||||
previousCustomStartDate && !formData.start_date_offset
|
||||
? parseDttmToDate(previousCustomStartDate)?.toUTCString()
|
||||
: formData.start_date_offset,
|
||||
});
|
||||
const comparisonSuffix = isUsingTimeComparison
|
||||
? ensureIsArray(timeOffsets)[0]
|
||||
: '';
|
||||
|
||||
const [metrics, percentMetrics, columns] = processColumns(chartProps);
|
||||
let comparisonColumns: DataColumnMeta[] = [];
|
||||
if (isUsingTimeComparison) {
|
||||
comparisonColumns = processComparisonColumns(
|
||||
columns,
|
||||
chartProps,
|
||||
comparisonSuffix,
|
||||
);
|
||||
}
|
||||
|
||||
let baseQuery;
|
||||
let countQuery;
|
||||
@@ -256,20 +575,41 @@ const transformProps = (
|
||||
rowCount = baseQuery?.rowcount ?? 0;
|
||||
}
|
||||
const data = processDataRecords(baseQuery?.data, columns);
|
||||
const comparisonData = processComparisonDataRecords(
|
||||
baseQuery?.data,
|
||||
columns,
|
||||
comparisonSuffix,
|
||||
);
|
||||
const totals =
|
||||
showTotals && queryMode === QueryMode.Aggregate
|
||||
? totalQuery?.data[0]
|
||||
? isUsingTimeComparison
|
||||
? 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,
|
||||
);
|
||||
|
||||
const startDateOffset = chartProps.rawFormData?.start_date_offset;
|
||||
return {
|
||||
height,
|
||||
width,
|
||||
isRawRecords: queryMode === QueryMode.Raw,
|
||||
data,
|
||||
data: passedData,
|
||||
totals,
|
||||
columns,
|
||||
columns: passedColumns,
|
||||
serverPagination,
|
||||
metrics,
|
||||
percentMetrics,
|
||||
@@ -294,6 +634,10 @@ const transformProps = (
|
||||
allowRearrangeColumns,
|
||||
allowRenderHtml,
|
||||
onContextMenu,
|
||||
isUsingTimeComparison,
|
||||
basicColorFormatters,
|
||||
startDateOffset,
|
||||
basicColorColumnFormatters,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -136,6 +142,15 @@ export interface TableChartTransformedProps<D extends DataRecord = DataRecord> {
|
||||
clientY: number,
|
||||
filters?: ContextMenuFilters,
|
||||
) => void;
|
||||
isUsingTimeComparison?: boolean;
|
||||
basicColorFormatters?: { [Key: string]: BasicColorFormatterType }[];
|
||||
basicColorColumnFormatters?: { [Key: string]: BasicColorFormatterType }[];
|
||||
startDateOffset?: string;
|
||||
}
|
||||
|
||||
export enum ColorSchemeEnum {
|
||||
'Green' = 'Green',
|
||||
'Red' = 'Red',
|
||||
}
|
||||
|
||||
export default {};
|
||||
|
||||
Reference in New Issue
Block a user