mirror of https://github.com/apache/superset.git
feat: Adds the ECharts Sunburst chart (#22833)
This commit is contained in:
parent
cd6fc35f60
commit
30abefb519
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { SuperChart, getChartTransformPropsRegistry } from '@superset-ui/core';
|
||||
import { boolean, withKnobs } from '@storybook/addon-knobs';
|
||||
import {
|
||||
EchartsSunburstChartPlugin,
|
||||
SunburstTransformProps,
|
||||
} from '@superset-ui/plugin-chart-echarts';
|
||||
import { withResizableChartDemo } from '../../../../shared/components/ResizableChartDemo';
|
||||
import data from './data';
|
||||
|
||||
new EchartsSunburstChartPlugin()
|
||||
.configure({ key: 'echarts-sunburst' })
|
||||
.register();
|
||||
|
||||
getChartTransformPropsRegistry().registerValue(
|
||||
'echarts-sunburst',
|
||||
SunburstTransformProps,
|
||||
);
|
||||
|
||||
export default {
|
||||
title: 'Chart Plugins/plugin-chart-echarts/Sunburst',
|
||||
decorators: [withKnobs, withResizableChartDemo],
|
||||
};
|
||||
|
||||
export const Sunburst = ({ width, height }) => (
|
||||
<SuperChart
|
||||
chartType="echarts-sunburst"
|
||||
width={width}
|
||||
height={height}
|
||||
queriesData={[{ data }]}
|
||||
formData={{
|
||||
columns: ['genre', 'platform'],
|
||||
metric: 'count',
|
||||
showLabels: boolean('Show labels', true),
|
||||
showTotal: boolean('Show total', true),
|
||||
}}
|
||||
/>
|
||||
);
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
export default [
|
||||
{ genre: 'Adventure', platform: 'Wii', count: 84 },
|
||||
{ genre: 'Adventure', platform: 'N64', count: 14 },
|
||||
{ genre: 'Adventure', platform: 'XOne', count: 12 },
|
||||
{ genre: 'Adventure', platform: 'PS4', count: 19 },
|
||||
{ genre: 'Strategy', platform: 'Wii', count: 25 },
|
||||
{ genre: 'Strategy', platform: 'PS4', count: 15 },
|
||||
{ genre: 'Strategy', platform: 'N64', count: 29 },
|
||||
{ genre: 'Strategy', platform: 'XOne', count: 23 },
|
||||
{ genre: 'Simulation', platform: 'PS4', count: 15 },
|
||||
{ genre: 'Simulation', platform: 'XOne', count: 36 },
|
||||
{ genre: 'Simulation', platform: 'N64', count: 20 },
|
||||
{ genre: 'Simulation', platform: 'Wii', count: 50 },
|
||||
];
|
|
@ -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);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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 |
|
@ -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'),
|
||||
|
|
|
@ -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: [
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -1,7 +1,3 @@
|
|||
import { CallbackDataParams } from 'echarts/types/src/util/types';
|
||||
import { LegendOrientation } from './types';
|
||||
import { TOOLTIP_POINTER_MARGIN, TOOLTIP_OVERFLOW_MARGIN } from './constants';
|
||||
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
|
@ -20,66 +16,12 @@ import { TOOLTIP_POINTER_MARGIN, TOOLTIP_OVERFLOW_MARGIN } from './constants';
|
|||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { LegendOrientation } from './types';
|
||||
|
||||
export const defaultGrid = {
|
||||
containLabel: true,
|
||||
};
|
||||
|
||||
export const defaultTooltip = {
|
||||
position: (
|
||||
canvasMousePos: [number, number],
|
||||
params: CallbackDataParams,
|
||||
tooltipDom: HTMLElement,
|
||||
rect: any,
|
||||
sizes: { contentSize: [number, number]; viewSize: [number, number] },
|
||||
) => {
|
||||
// algorithm copy-pasted from here:
|
||||
// https://github.com/apache/echarts/issues/5004#issuecomment-559668309
|
||||
|
||||
// The chart canvas position
|
||||
const canvasRect = tooltipDom.parentElement
|
||||
?.getElementsByTagName('canvas')[0]
|
||||
.getBoundingClientRect();
|
||||
|
||||
// The mouse coordinates relative to the whole window
|
||||
// The first parameter to the position function is the mouse position relative to the canvas
|
||||
const mouseX = canvasMousePos[0] + (canvasRect?.x || 0);
|
||||
const mouseY = canvasMousePos[1] + (canvasRect?.y || 0);
|
||||
|
||||
// The width and height of the tooltip dom element
|
||||
const tooltipWidth = sizes.contentSize[0];
|
||||
const tooltipHeight = sizes.contentSize[1];
|
||||
|
||||
// Start by placing the tooltip top and right relative to the mouse position
|
||||
let xPos = mouseX + TOOLTIP_POINTER_MARGIN;
|
||||
let yPos = mouseY - TOOLTIP_POINTER_MARGIN - tooltipHeight;
|
||||
|
||||
// The tooltip is overflowing past the right edge of the window
|
||||
if (xPos + tooltipWidth >= document.documentElement.clientWidth) {
|
||||
// Attempt to place the tooltip to the left of the mouse position
|
||||
xPos = mouseX - TOOLTIP_POINTER_MARGIN - tooltipWidth;
|
||||
|
||||
// The tooltip is overflowing past the left edge of the window
|
||||
if (xPos <= 0)
|
||||
// Place the tooltip a fixed distance from the left edge of the window
|
||||
xPos = TOOLTIP_OVERFLOW_MARGIN;
|
||||
}
|
||||
|
||||
// The tooltip is overflowing past the top edge of the window
|
||||
if (yPos <= 0) {
|
||||
// Attempt to place the tooltip to the bottom of the mouse position
|
||||
yPos = mouseY + TOOLTIP_POINTER_MARGIN;
|
||||
|
||||
// The tooltip is overflowing past the bottom edge of the window
|
||||
if (yPos + tooltipHeight >= document.documentElement.clientHeight)
|
||||
// Place the tooltip a fixed distance from the top edge of the window
|
||||
yPos = TOOLTIP_OVERFLOW_MARGIN;
|
||||
}
|
||||
|
||||
// Return the position (converted back to a relative position on the canvas)
|
||||
return [xPos - (canvasRect?.x || 0), yPos - (canvasRect?.y || 0)];
|
||||
},
|
||||
};
|
||||
|
||||
export const defaultYAxis = {
|
||||
scale: true,
|
||||
};
|
||||
|
|
|
@ -16,17 +16,21 @@
|
|||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { DataRecord } from '@superset-ui/core';
|
||||
import { DataRecord, DataRecordValue } from '@superset-ui/core';
|
||||
import _ from 'lodash';
|
||||
|
||||
export type TreeNode = {
|
||||
name: string;
|
||||
name: DataRecordValue;
|
||||
value: number;
|
||||
secondaryValue: number;
|
||||
groupBy: string;
|
||||
children?: TreeNode[];
|
||||
};
|
||||
|
||||
function getMetricValue(datum: DataRecord, metric: string) {
|
||||
return _.isNumber(datum[metric]) ? (datum[metric] as number) : 0;
|
||||
}
|
||||
|
||||
export function treeBuilder(
|
||||
data: DataRecord[],
|
||||
groupBy: string[],
|
||||
|
@ -37,7 +41,8 @@ export function treeBuilder(
|
|||
const curData = _.groupBy(data, curGroupBy);
|
||||
return _.transform(
|
||||
curData,
|
||||
(result, value, name) => {
|
||||
(result, value, key) => {
|
||||
const name = curData[key][0][curGroupBy]!;
|
||||
if (!restGroupby.length) {
|
||||
(value ?? []).forEach(datum => {
|
||||
const metricValue = getMetricValue(datum, metric);
|
||||
|
@ -81,7 +86,3 @@ export function treeBuilder(
|
|||
[] as TreeNode[],
|
||||
);
|
||||
}
|
||||
|
||||
function getMetricValue(datum: DataRecord, metric: string) {
|
||||
return _.isNumber(datum[metric]) ? (datum[metric] as number) : 0;
|
||||
}
|
||||
|
|
|
@ -68,6 +68,7 @@ import {
|
|||
EchartsTreemapChartPlugin,
|
||||
EchartsMixedTimeseriesChartPlugin,
|
||||
EchartsTreeChartPlugin,
|
||||
EchartsSunburstChartPlugin,
|
||||
} from '@superset-ui/plugin-chart-echarts';
|
||||
import {
|
||||
SelectFilterPlugin,
|
||||
|
@ -165,6 +166,7 @@ export default class MainPreset extends Preset {
|
|||
new TimeColumnFilterPlugin().configure({ key: 'filter_timecolumn' }),
|
||||
new TimeGrainFilterPlugin().configure({ key: 'filter_timegrain' }),
|
||||
new EchartsTreeChartPlugin().configure({ key: 'tree_chart' }),
|
||||
new EchartsSunburstChartPlugin().configure({ key: 'sunburst_v2' }),
|
||||
new HandlebarsChartPlugin().configure({ key: 'handlebars' }),
|
||||
...experimentalplugins,
|
||||
],
|
||||
|
|
Loading…
Reference in New Issue