309 lines
8.1 KiB
TypeScript
309 lines
8.1 KiB
TypeScript
/**
|
|
* 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,
|
|
} from '@superset-ui/core';
|
|
import { TreemapSeriesNodeItemOption } from 'echarts/types/src/chart/treemap/TreemapSeries';
|
|
import { EChartsCoreOption, TreemapSeriesOption } from 'echarts';
|
|
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';
|
|
import { getValueFormatter } from '../utils/valueFormatter';
|
|
|
|
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);
|
|
}
|
|
|
|
// groupby1/groupby2/...
|
|
// metric: value (percent of parent)
|
|
return [
|
|
`<div>${treePath.join(' ▸ ')}</div>`,
|
|
`${metricLabel}: ${formattedValue}`,
|
|
formattedPercent ? ` (${formattedPercent})` : '',
|
|
].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,
|
|
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,
|
|
);
|
|
|
|
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, {
|
|
numberFormatter,
|
|
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),
|
|
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),
|
|
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,
|
|
};
|
|
}
|