feat: Adds the ECharts Sunburst chart (#22833)

This commit is contained in:
Michael S. Molina
2023-01-31 11:39:18 -05:00
committed by GitHub
parent cd6fc35f60
commit 30abefb519
13 changed files with 166 additions and 103 deletions

View File

@@ -94,22 +94,22 @@ export default function EchartsSunburst(props: SunburstTransformedProps) {
contextmenu: eventParams => {
if (onContextMenu) {
eventParams.event.stop();
const { data } = eventParams;
const { records } = data;
const treePath = extractTreePathInfo(eventParams.treePathInfo);
if (treePath.length > 0) {
const pointerEvent = eventParams.event.event;
const filters: BinaryQueryObjectFilterClause[] = [];
if (columns) {
treePath.forEach((path, i) =>
filters.push({
col: columns[i],
op: '==',
val: path,
formattedVal: path,
}),
);
}
onContextMenu(pointerEvent.clientX, pointerEvent.clientY, filters);
const pointerEvent = eventParams.event.event;
const filters: BinaryQueryObjectFilterClause[] = [];
if (columns?.length) {
treePath.forEach((path, i) =>
filters.push({
col: columns[i],
op: '==',
val: records[i],
formattedVal: path,
}),
);
}
onContextMenu(pointerEvent.clientX, pointerEvent.clientY, filters);
}
},
};

View File

@@ -189,9 +189,10 @@ const config: ControlPanelConfig = {
controls?.secondary_metric?.value !== controls?.metric.value,
),
},
groupby: {
columns: {
label: t('Hierarchy'),
description: t('This defines the level of the hierarchy'),
description: t(`Sets the hierarchy levels of the chart. Each level is
represented by one ring with the innermost circle as the top of the hierarchy.`),
},
},
formDataOverrides: formData => ({

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 127 KiB

View File

@@ -21,6 +21,8 @@ import transformProps from './transformProps';
import thumbnail from './images/thumbnail.png';
import controlPanel from './controlPanel';
import buildQuery from './buildQuery';
import example1 from './images/Sunburst1.png';
import example2 from './images/Sunburst2.png';
export default class EchartsSunburstChartPlugin extends ChartPlugin {
constructor() {
@@ -29,13 +31,13 @@ export default class EchartsSunburstChartPlugin extends ChartPlugin {
controlPanel,
loadChart: () => import('./EchartsSunburst'),
metadata: new ChartMetadata({
behaviors: [Behavior.INTERACTIVE_CHART],
behaviors: [Behavior.INTERACTIVE_CHART, Behavior.DRILL_TO_DETAIL],
category: t('Part of a Whole'),
credits: ['https://echarts.apache.org'],
description: t(
'Uses circles to visualize the flow of data through different stages of a system. Hover over individual paths in the visualization to understand the stages a value took. Useful for multi-stage, multi-group visualizing funnels and pipelines.',
),
exampleGallery: [],
exampleGallery: [{ url: example1 }, { url: example2 }],
name: t('Sunburst Chart v2'),
tags: [
t('ECharts'),

View File

@@ -16,9 +16,9 @@
* specific language governing permissions and limitations
* under the License.
*/
import {
CategoricalColorNamespace,
DataRecordValue,
getColumnLabel,
getMetricLabel,
getNumberFormatter,
@@ -26,21 +26,23 @@ import {
getTimeFormatter,
NumberFormats,
NumberFormatter,
SupersetTheme,
t,
} from '@superset-ui/core';
import { EChartsCoreOption } from 'echarts';
import { SunburstSeriesNodeItemOption } from 'echarts/types/src/chart/sunburst/SunburstSeries';
import { CallbackDataParams } from 'echarts/types/src/util/types';
import { OpacityEnum } from '../constants';
import { defaultGrid, defaultTooltip } from '../defaults';
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[],
@@ -96,6 +98,7 @@ export function formatTooltip({
totalValue,
metricLabel,
secondaryMetricLabel,
theme,
}: {
params: CallbackDataParams & {
treePathInfo: {
@@ -109,9 +112,9 @@ export function formatTooltip({
totalValue: number;
metricLabel: string;
secondaryMetricLabel?: string;
theme: SupersetTheme;
}): string {
const { data, treePathInfo = [] } = params;
treePathInfo.shift();
const node = data as TreeNode;
const formattedValue = numberFormatter(node.value);
const formattedSecondaryValue = numberFormatter(node.secondaryValue);
@@ -121,33 +124,43 @@ export function formatTooltip({
node.secondaryValue / node.value,
);
const absolutePercentage = percentFormatter(node.value / totalValue);
const parentNode = treePathInfo[treePathInfo.length - 1];
const parentNode =
treePathInfo.length > 2 ? treePathInfo[treePathInfo.length - 2] : undefined;
const result = [
`<div style="font-size: 14px;font-weight: 600">${absolutePercentage} of total</div>`,
`<div style="
font-size: ${theme.typography.sizes.m}px;
color: ${theme.colors.grayscale.base}"
>`,
`<div style="font-weight: ${theme.typography.weights.bold}">
${node.name}
</div>`,
`<div">
${absolutePercentage} of total
</div>`,
];
if (parentNode) {
const conditionalPercentage = percentFormatter(
node.value / parentNode.value,
);
result.push(`
<div style="font-size: 12px;">
${conditionalPercentage} of parent
<div>
${conditionalPercentage} of ${parentNode.name}
</div>`);
}
result.push(
`<div style="color: '#666666'">
`<div>
${metricLabel}: ${formattedValue}${
colorByCategory
? ''
: `, ${secondaryMetricLabel}: ${formattedSecondaryValue}`
}
</div>`,
</div>`,
colorByCategory
? ''
: `<div style="color: '#666666'">
${metricLabel}/${secondaryMetricLabel}: ${compareValuePercentage}
</div>`,
: `<div>${metricLabel}/${secondaryMetricLabel}: ${compareValuePercentage}</div>`,
);
result.push('</div>');
return result.join('\n');
}
@@ -247,9 +260,14 @@ export default function transformProps(
linearColorScale(totalSecondaryValue / totalValue);
}
const traverse = (treeNodes: TreeNode[], path: string[]) =>
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),
@@ -258,10 +276,10 @@ export default function transformProps(
}),
});
const newPath = path.concat(name);
let item: SunburstSeriesNodeItemOption = {
let item: NodeItemOption = {
records,
name,
value,
// @ts-ignore
secondaryValue,
itemStyle: {
color: colorByCategory
@@ -270,7 +288,7 @@ export default function transformProps(
},
};
if (treeNode.children?.length) {
item.children = traverse(treeNode.children, newPath);
item.children = traverse(treeNode.children, newPath, records);
} else {
name = newPath.join(',');
}
@@ -295,7 +313,7 @@ export default function transformProps(
...defaultGrid,
},
tooltip: {
...defaultTooltip,
...getDefaultTooltip(refs),
show: !inContextMenu,
trigger: 'item',
formatter: (params: any) =>
@@ -306,6 +324,7 @@ export default function transformProps(
totalValue,
metricLabel,
secondaryMetricLabel,
theme,
}),
},
series: [

View File

@@ -20,10 +20,12 @@
import {
ChartDataResponseResult,
ChartProps,
DataRecordValue,
QueryFormColumn,
QueryFormData,
QueryFormMetric,
} from '@superset-ui/core';
import { SunburstSeriesNodeItemOption } from 'echarts/types/src/chart/sunburst/SunburstSeries';
import {
BaseTransformedProps,
ContextMenuTransformedProps,
@@ -62,3 +64,8 @@ export type SunburstTransformedProps =
BaseTransformedProps<EchartsSunburstFormData> &
ContextMenuTransformedProps &
CrossFilterTransformedProps;
export type NodeItemOption = SunburstSeriesNodeItemOption & {
records: DataRecordValue[];
secondaryValue: number;
};