superset-frontend/plugins/plugin-chart-echarts/src/Treemap/transformProps.ts
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import {
CategoricalColorNamespace,
getColumnLabel,
getMetricLabel,
getNumberFormatter,
getTimeFormatter,
NumberFormats,
ValueFormatter,
getValueFormatter,
tooltipHtml,
} from '@superset-ui/core';
import type { TreemapSeriesNodeItemOption } from 'echarts/types/src/chart/treemap/TreemapSeries';
import type { EChartsCoreOption } from 'echarts/core';
import type { TreemapSeriesOption } from 'echarts/charts';
import {
DEFAULT_FORM_DATA as DEFAULT_TREEMAP_FORM_DATA,
EchartsTreemapChartProps,
EchartsTreemapFormData,
EchartsTreemapLabelType,
TreemapSeriesCallbackDataParams,
TreemapTransformedProps,
} from './types';
import { formatSeriesName, getColtypesMapping } from '../utils/series';
import {
COLOR_SATURATION,
BORDER_WIDTH,
GAP_WIDTH,
LABEL_FONTSIZE,
extractTreePathInfo,
BORDER_COLOR,
} from './constants';
import { OpacityEnum } from '../constants';
import { getDefaultTooltip } from '../utils/tooltip';
import { Refs } from '../types';
import { treeBuilder, TreeNode } from '../utils/treeBuilder';
export function formatLabel({
params,
labelType,
numberFormatter,
}: {
params: TreemapSeriesCallbackDataParams;
labelType: EchartsTreemapLabelType;
numberFormatter: ValueFormatter;
}): string {
const { name = '', value } = params;
const formattedValue = numberFormatter(value as number);
switch (labelType) {
case EchartsTreemapLabelType.Key:
return name;
case EchartsTreemapLabelType.Value:
return formattedValue;
case EchartsTreemapLabelType.KeyValue:
return `${name}: ${formattedValue}`;
default:
return name;
}
}
export function formatTooltip({
params,
numberFormatter,
}: {
params: TreemapSeriesCallbackDataParams;
numberFormatter: ValueFormatter;
}): string {
const { value, treePathInfo = [] } = params;
const formattedValue = numberFormatter(value as number);
const { metricLabel, treePath } = extractTreePathInfo(treePathInfo);
const percentFormatter = getNumberFormatter(NumberFormats.PERCENT_2_POINT);
let formattedPercent = '';
// the last item is current node, here we should find the parent node
const currentNode = treePathInfo[treePathInfo.length - 1];
const parentNode = treePathInfo[treePathInfo.length - 2];
if (parentNode) {
const percent: number = parentNode.value
? (currentNode.value as number) / (parentNode.value as number)
: 0;
formattedPercent = percentFormatter(percent);
}
const row = [metricLabel, formattedValue];
if (formattedPercent) {
row.push(formattedPercent);
}
return tooltipHtml([row], treePath.join(' ▸ '));
}
export default function transformProps(
chartProps: EchartsTreemapChartProps,
): TreemapTransformedProps {
const {
formData,
height,
queriesData,
width,
hooks,
filterState,
theme,
inContextMenu,
emitCrossFilters,
datasource,
} = chartProps;
const { data = [] } = queriesData[0];
const { columnFormats = {}, currencyFormats = {} } = datasource;
const { setDataMask = () => {}, onContextMenu } = hooks;
const coltypeMapping = getColtypesMapping(queriesData[0]);
const {
colorScheme,
groupby = [],
metric = '',
labelType,
labelPosition,
numberFormat,
currencyFormat,
dateFormat,
showLabels,
showUpperLabels,
dashboardId,
sliceId,
}: EchartsTreemapFormData = {
...DEFAULT_TREEMAP_FORM_DATA,
...formData,
};
const refs: Refs = {};
const colorFn = CategoricalColorNamespace.getScale(colorScheme as string);
const numberFormatter = getValueFormatter(
metric,
currencyFormats,
columnFormats,
numberFormat,
currencyFormat,
);
const formatter = (params: TreemapSeriesCallbackDataParams) =>
formatLabel({
params,
numberFormatter,
labelType,
});
const columnsLabelMap = new Map<string, string[]>();
const metricLabel = getMetricLabel(metric);
const groupbyLabels = groupby.map(getColumnLabel);
const treeData = treeBuilder(data, groupbyLabels, metricLabel);
const traverse = (treeNodes: TreeNode[], path: string[]) =>
treeNodes.map(treeNode => {
const { name: nodeName, value, groupBy } = treeNode;
const name = formatSeriesName(nodeName, {
timeFormatter: getTimeFormatter(dateFormat),
...(coltypeMapping[groupBy] && {
coltype: coltypeMapping[groupBy],
}),
});
const newPath = path.concat(name);
let item: TreemapSeriesNodeItemOption = {
name,
value,
};
if (treeNode.children?.length) {
item = {
...item,
children: traverse(treeNode.children, newPath),
colorSaturation: COLOR_SATURATION,
itemStyle: {
borderColor: BORDER_COLOR,
color: colorFn(name, sliceId, colorScheme),
borderWidth: BORDER_WIDTH,
gapWidth: GAP_WIDTH,
},
};
} else {
const joinedName = newPath.join(',');
// map(joined_name: [columnLabel_1, columnLabel_2, ...])
columnsLabelMap.set(joinedName, newPath);
if (
filterState.selectedValues &&
!filterState.selectedValues.includes(joinedName)
) {
item = {
...item,
itemStyle: {
colorAlpha: OpacityEnum.SemiTransparent,
},
label: {
color: `rgba(0, 0, 0, ${OpacityEnum.SemiTransparent})`,
},
};
}
}
return item;
});
const transformedData: TreemapSeriesNodeItemOption[] = [
{
name: metricLabel,
colorSaturation: COLOR_SATURATION,
itemStyle: {
borderColor: BORDER_COLOR,
color: colorFn(`${metricLabel}`, sliceId, colorScheme),
borderWidth: BORDER_WIDTH,
gapWidth: GAP_WIDTH,
},
upperLabel: {
show: false,
},
children: traverse(treeData, []),
},
];
// set a default color when metric values are 0 over all.
const levels = [
{
upperLabel: {
show: false,
},
label: {
show: false,
},
itemStyle: {
color: theme.colors.primary.base,
},
},
];
const series: TreemapSeriesOption[] = [
{
type: 'treemap',
width: '100%',
height: '100%',
nodeClick: undefined,
roam: !dashboardId,
breadcrumb: {
show: false,
emptyItemWidth: 25,
},
emphasis: {
label: {
show: true,
},
},
levels,
label: {
show: showLabels,
position: labelPosition,
formatter,
color: theme.colors.grayscale.dark2,
fontSize: LABEL_FONTSIZE,
},
upperLabel: {
show: showUpperLabels,
formatter,
textBorderColor: 'transparent',
fontSize: LABEL_FONTSIZE,
},
data: transformedData,
},
];
const echartOptions: EChartsCoreOption = {
tooltip: {
...getDefaultTooltip(refs),
show: !inContextMenu,
trigger: 'item',
formatter: (params: any) =>
formatTooltip({
params,
numberFormatter,
}),
},
series,
};
return {
formData,
width,
height,
echartOptions,
setDataMask,
emitCrossFilters,
labelMap: Object.fromEntries(columnsLabelMap),
groupby,
selectedValues: filterState.selectedValues || [],
onContextMenu,
refs,
coltypeMapping,
};
}