mirror of
https://github.com/apache/superset.git
synced 2026-04-23 18:14:56 +00:00
394 lines
10 KiB
TypeScript
394 lines
10 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,
|
|
DataRecordValue,
|
|
getColumnLabel,
|
|
getMetricLabel,
|
|
getNumberFormatter,
|
|
getSequentialSchemeRegistry,
|
|
getTimeFormatter,
|
|
getValueFormatter,
|
|
NumberFormats,
|
|
t,
|
|
tooltipHtml,
|
|
ValueFormatter,
|
|
} from '@superset-ui/core';
|
|
import type { EChartsCoreOption } from 'echarts/core';
|
|
import type { CallbackDataParams } from 'echarts/types/src/util/types';
|
|
import { NULL_STRING, OpacityEnum } from '../constants';
|
|
import { defaultGrid } from '../defaults';
|
|
import { Refs } from '../types';
|
|
import { formatSeriesName, getColtypesMapping } from '../utils/series';
|
|
import { treeBuilder, TreeNode } from '../utils/treeBuilder';
|
|
import {
|
|
EchartsSunburstChartProps,
|
|
EchartsSunburstLabelType,
|
|
NodeItemOption,
|
|
SunburstTransformedProps,
|
|
} from './types';
|
|
import { getDefaultTooltip } from '../utils/tooltip';
|
|
|
|
export function getLinearDomain(
|
|
treeData: TreeNode[],
|
|
callback: (treeNode: TreeNode) => number,
|
|
) {
|
|
let min = 0;
|
|
let max = 0;
|
|
let temp = null;
|
|
function traverse(tree: TreeNode[]) {
|
|
tree.forEach(treeNode => {
|
|
if (treeNode.children?.length) {
|
|
traverse(treeNode.children);
|
|
}
|
|
temp = callback(treeNode);
|
|
if (temp !== null) {
|
|
if (min > temp) min = temp;
|
|
if (max < temp) max = temp;
|
|
}
|
|
});
|
|
}
|
|
traverse(treeData);
|
|
return [min, max];
|
|
}
|
|
|
|
export function formatLabel({
|
|
params,
|
|
labelType,
|
|
numberFormatter,
|
|
}: {
|
|
params: CallbackDataParams;
|
|
labelType: EchartsSunburstLabelType;
|
|
numberFormatter: ValueFormatter;
|
|
}): string {
|
|
const { name = '', value } = params;
|
|
const formattedValue = numberFormatter(value as number);
|
|
|
|
switch (labelType) {
|
|
case EchartsSunburstLabelType.Key:
|
|
return name;
|
|
case EchartsSunburstLabelType.Value:
|
|
return formattedValue;
|
|
case EchartsSunburstLabelType.KeyValue:
|
|
return `${name}: ${formattedValue}`;
|
|
default:
|
|
return name;
|
|
}
|
|
}
|
|
|
|
export function formatTooltip({
|
|
params,
|
|
primaryValueFormatter,
|
|
secondaryValueFormatter,
|
|
colorByCategory,
|
|
totalValue,
|
|
metricLabel,
|
|
secondaryMetricLabel,
|
|
}: {
|
|
params: CallbackDataParams & {
|
|
treePathInfo: {
|
|
name: string;
|
|
dataIndex: number;
|
|
value: number;
|
|
}[];
|
|
};
|
|
primaryValueFormatter: ValueFormatter;
|
|
secondaryValueFormatter: ValueFormatter | undefined;
|
|
colorByCategory: boolean;
|
|
totalValue: number;
|
|
metricLabel: string;
|
|
secondaryMetricLabel?: string;
|
|
}): string {
|
|
const { data, treePathInfo = [] } = params;
|
|
const node = data as TreeNode;
|
|
const formattedValue = primaryValueFormatter(node.value);
|
|
const formattedSecondaryValue = secondaryValueFormatter?.(
|
|
node.secondaryValue,
|
|
);
|
|
|
|
const percentFormatter = getNumberFormatter(NumberFormats.PERCENT_2_POINT);
|
|
const compareValuePercentage = percentFormatter(
|
|
node.secondaryValue / node.value,
|
|
);
|
|
const absolutePercentage = percentFormatter(node.value / totalValue);
|
|
const parentNode =
|
|
treePathInfo.length > 2 ? treePathInfo[treePathInfo.length - 2] : undefined;
|
|
|
|
const title = (node.name || NULL_STRING)
|
|
.toString()
|
|
.replaceAll('<', '<')
|
|
.replaceAll('>', '>');
|
|
const rows = [[t('% of total'), absolutePercentage]];
|
|
if (parentNode) {
|
|
const conditionalPercentage = percentFormatter(
|
|
node.value / parentNode.value,
|
|
);
|
|
rows.push([t('% of parent'), conditionalPercentage]);
|
|
}
|
|
rows.push([metricLabel, formattedValue]);
|
|
if (!colorByCategory) {
|
|
rows.push([
|
|
secondaryMetricLabel || NULL_STRING,
|
|
formattedSecondaryValue || NULL_STRING,
|
|
]);
|
|
rows.push([
|
|
`${metricLabel}/${secondaryMetricLabel}`,
|
|
compareValuePercentage,
|
|
]);
|
|
}
|
|
return tooltipHtml(rows, title);
|
|
}
|
|
|
|
export default function transformProps(
|
|
chartProps: EchartsSunburstChartProps,
|
|
): SunburstTransformedProps {
|
|
const {
|
|
formData,
|
|
height,
|
|
hooks,
|
|
filterState,
|
|
queriesData,
|
|
width,
|
|
theme,
|
|
inContextMenu,
|
|
emitCrossFilters,
|
|
datasource,
|
|
} = chartProps;
|
|
const { data = [] } = queriesData[0];
|
|
const coltypeMapping = getColtypesMapping(queriesData[0]);
|
|
const {
|
|
groupby = [],
|
|
columns = [],
|
|
metric = '',
|
|
secondaryMetric = '',
|
|
colorScheme,
|
|
linearColorScheme,
|
|
labelType,
|
|
numberFormat,
|
|
currencyFormat,
|
|
dateFormat,
|
|
showLabels,
|
|
showLabelsThreshold,
|
|
showTotal,
|
|
sliceId,
|
|
} = formData;
|
|
const { currencyFormats = {}, columnFormats = {} } = datasource;
|
|
const refs: Refs = {};
|
|
const primaryValueFormatter = getValueFormatter(
|
|
metric,
|
|
currencyFormats,
|
|
columnFormats,
|
|
numberFormat,
|
|
currencyFormat,
|
|
);
|
|
const secondaryValueFormatter = secondaryMetric
|
|
? getValueFormatter(
|
|
secondaryMetric,
|
|
currencyFormats,
|
|
columnFormats,
|
|
numberFormat,
|
|
currencyFormat,
|
|
)
|
|
: undefined;
|
|
|
|
const numberFormatter = getNumberFormatter(numberFormat);
|
|
const formatter = (params: CallbackDataParams) =>
|
|
formatLabel({
|
|
params,
|
|
numberFormatter: primaryValueFormatter,
|
|
labelType,
|
|
});
|
|
const minShowLabelAngle = (showLabelsThreshold || 0) * 3.6;
|
|
const padding = {
|
|
top: theme.gridUnit * 3,
|
|
right: theme.gridUnit,
|
|
bottom: theme.gridUnit * 3,
|
|
left: theme.gridUnit,
|
|
};
|
|
const containerWidth = width;
|
|
const containerHeight = height;
|
|
const visWidth = containerWidth - padding.left - padding.right;
|
|
const visHeight = containerHeight - padding.top - padding.bottom;
|
|
const radius = Math.min(visWidth, visHeight) / 2;
|
|
const { setDataMask = () => {}, onContextMenu } = hooks;
|
|
const columnsLabelMap = new Map<string, string[]>();
|
|
const metricLabel = getMetricLabel(metric);
|
|
const secondaryMetricLabel = secondaryMetric
|
|
? getMetricLabel(secondaryMetric)
|
|
: undefined;
|
|
const columnLabels = columns.map(getColumnLabel);
|
|
const treeData = treeBuilder(
|
|
data,
|
|
columnLabels,
|
|
metricLabel,
|
|
secondaryMetricLabel,
|
|
);
|
|
const totalValue = treeData.reduce(
|
|
(result, treeNode) => result + treeNode.value,
|
|
0,
|
|
);
|
|
const totalSecondaryValue = treeData.reduce(
|
|
(result, treeNode) => result + treeNode.secondaryValue,
|
|
0,
|
|
);
|
|
|
|
const categoricalColorScale = CategoricalColorNamespace.getScale(
|
|
colorScheme as string,
|
|
);
|
|
let linearColorScale: any;
|
|
let colorByCategory = true;
|
|
if (secondaryMetric && metric !== secondaryMetric) {
|
|
const domain = getLinearDomain(
|
|
treeData,
|
|
node => node.secondaryValue / node.value,
|
|
);
|
|
colorByCategory = false;
|
|
linearColorScale = getSequentialSchemeRegistry()
|
|
?.get(linearColorScheme)
|
|
?.createLinearScale(domain);
|
|
}
|
|
|
|
// add a base color to keep feature parity
|
|
if (colorByCategory) {
|
|
categoricalColorScale(metricLabel, sliceId);
|
|
} else {
|
|
linearColorScale(totalSecondaryValue / totalValue);
|
|
}
|
|
|
|
const traverse = (
|
|
treeNodes: TreeNode[],
|
|
path: string[],
|
|
pathRecords?: DataRecordValue[],
|
|
) =>
|
|
treeNodes.map(treeNode => {
|
|
const { name: nodeName, value, secondaryValue, groupBy } = treeNode;
|
|
const records = [...(pathRecords || []), nodeName];
|
|
let name = formatSeriesName(nodeName, {
|
|
numberFormatter,
|
|
timeFormatter: getTimeFormatter(dateFormat),
|
|
...(coltypeMapping[groupBy] && {
|
|
coltype: coltypeMapping[groupBy],
|
|
}),
|
|
});
|
|
const newPath = path.concat(name);
|
|
let item: NodeItemOption = {
|
|
records,
|
|
name,
|
|
value,
|
|
secondaryValue,
|
|
itemStyle: {
|
|
color: colorByCategory
|
|
? categoricalColorScale(name, sliceId)
|
|
: linearColorScale(secondaryValue / value),
|
|
},
|
|
};
|
|
if (treeNode.children?.length) {
|
|
item.children = traverse(treeNode.children, newPath, records);
|
|
} else {
|
|
name = newPath.join(',');
|
|
}
|
|
columnsLabelMap.set(name, newPath);
|
|
if (filterState.selectedValues?.[0]?.includes(name) === false) {
|
|
item = {
|
|
...item,
|
|
itemStyle: {
|
|
...item.itemStyle,
|
|
opacity: OpacityEnum.SemiTransparent,
|
|
},
|
|
label: {
|
|
color: `rgba(0, 0, 0, ${OpacityEnum.SemiTransparent})`,
|
|
},
|
|
};
|
|
}
|
|
return item;
|
|
});
|
|
|
|
const echartOptions: EChartsCoreOption = {
|
|
grid: {
|
|
...defaultGrid,
|
|
},
|
|
tooltip: {
|
|
...getDefaultTooltip(refs),
|
|
show: !inContextMenu,
|
|
trigger: 'item',
|
|
formatter: (params: any) =>
|
|
formatTooltip({
|
|
params,
|
|
primaryValueFormatter,
|
|
secondaryValueFormatter,
|
|
colorByCategory,
|
|
totalValue,
|
|
metricLabel,
|
|
secondaryMetricLabel,
|
|
}),
|
|
},
|
|
series: [
|
|
{
|
|
type: 'sunburst',
|
|
...padding,
|
|
nodeClick: false,
|
|
emphasis: {
|
|
focus: 'ancestor',
|
|
label: {
|
|
show: showLabels,
|
|
},
|
|
},
|
|
label: {
|
|
width: (radius * 0.6) / (columns.length || 1),
|
|
show: showLabels,
|
|
formatter,
|
|
color: theme.colors.grayscale.dark2,
|
|
minAngle: minShowLabelAngle,
|
|
overflow: 'breakAll',
|
|
},
|
|
radius: [radius * 0.3, radius],
|
|
data: traverse(treeData, []),
|
|
},
|
|
],
|
|
graphic: showTotal
|
|
? {
|
|
type: 'text',
|
|
top: 'center',
|
|
left: 'center',
|
|
style: {
|
|
text: t('Total: %s', primaryValueFormatter(totalValue)),
|
|
fontSize: 16,
|
|
fontWeight: 'bold',
|
|
},
|
|
z: 10,
|
|
}
|
|
: null,
|
|
};
|
|
|
|
return {
|
|
formData,
|
|
width,
|
|
height,
|
|
echartOptions,
|
|
setDataMask,
|
|
emitCrossFilters,
|
|
labelMap: Object.fromEntries(columnsLabelMap),
|
|
groupby,
|
|
selectedValues: filterState.selectedValues || [],
|
|
onContextMenu,
|
|
refs,
|
|
coltypeMapping,
|
|
};
|
|
}
|