diff --git a/superset-frontend/packages/superset-ui-demo/storybook/stories/plugins/plugin-chart-echarts/Sunburst/Stories.tsx b/superset-frontend/packages/superset-ui-demo/storybook/stories/plugins/plugin-chart-echarts/Sunburst/Stories.tsx new file mode 100644 index 0000000000..7742f1ecfe --- /dev/null +++ b/superset-frontend/packages/superset-ui-demo/storybook/stories/plugins/plugin-chart-echarts/Sunburst/Stories.tsx @@ -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 }) => ( + +); diff --git a/superset-frontend/packages/superset-ui-demo/storybook/stories/plugins/plugin-chart-echarts/Sunburst/data.ts b/superset-frontend/packages/superset-ui-demo/storybook/stories/plugins/plugin-chart-echarts/Sunburst/data.ts new file mode 100644 index 0000000000..35675465df --- /dev/null +++ b/superset-frontend/packages/superset-ui-demo/storybook/stories/plugins/plugin-chart-echarts/Sunburst/data.ts @@ -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 }, +]; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/EchartsSunburst.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/EchartsSunburst.tsx index 3dd8dc931a..390c830c45 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/EchartsSunburst.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/EchartsSunburst.tsx @@ -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); } }, }; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/controlPanel.tsx index 7ad3618a5a..2c90dff452 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/controlPanel.tsx @@ -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 => ({ diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/images/Sunburst1.png b/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/images/Sunburst1.png new file mode 100644 index 0000000000..87c140e7c3 Binary files /dev/null and b/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/images/Sunburst1.png differ diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/images/Sunburst2.png b/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/images/Sunburst2.png new file mode 100644 index 0000000000..677b2a1966 Binary files /dev/null and b/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/images/Sunburst2.png differ diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/images/thumbnail.png b/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/images/thumbnail.png index 7afef30bd4..87c140e7c3 100644 Binary files a/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/images/thumbnail.png and b/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/images/thumbnail.png differ diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/index.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/index.ts index fe75a7916f..5ca8d5a8fc 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/index.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/index.ts @@ -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'), diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/transformProps.ts index 9873d1dc32..51e89f8c6c 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/transformProps.ts @@ -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 = [ - `
${absolutePercentage} of total
`, + `
`, + `
+ ${node.name} +
`, + ` + ${absolutePercentage} of total +
`, ]; if (parentNode) { const conditionalPercentage = percentFormatter( node.value / parentNode.value, ); result.push(` -
- ${conditionalPercentage} of parent +
+ ${conditionalPercentage} of ${parentNode.name}
`); } result.push( - `
+ `
${metricLabel}: ${formattedValue}${ colorByCategory ? '' : `, ${secondaryMetricLabel}: ${formattedSecondaryValue}` } -
`, +
`, colorByCategory ? '' - : `
- ${metricLabel}/${secondaryMetricLabel}: ${compareValuePercentage} -
`, + : `
${metricLabel}/${secondaryMetricLabel}: ${compareValuePercentage}
`, ); + result.push('
'); 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: [ diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/types.ts index 91030fd7b5..37844addea 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/types.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/types.ts @@ -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 & ContextMenuTransformedProps & CrossFilterTransformedProps; + +export type NodeItemOption = SunburstSeriesNodeItemOption & { + records: DataRecordValue[]; + secondaryValue: number; +}; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/defaults.ts b/superset-frontend/plugins/plugin-chart-echarts/src/defaults.ts index d76de5b53d..c5ada14932 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/defaults.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/defaults.ts @@ -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, }; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/utils/treeBuilder.ts b/superset-frontend/plugins/plugin-chart-echarts/src/utils/treeBuilder.ts index 32e0416a6b..97916997d4 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/utils/treeBuilder.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/utils/treeBuilder.ts @@ -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; -} diff --git a/superset-frontend/src/visualizations/presets/MainPreset.js b/superset-frontend/src/visualizations/presets/MainPreset.js index 88c1975d01..2bce5ae096 100644 --- a/superset-frontend/src/visualizations/presets/MainPreset.js +++ b/superset-frontend/src/visualizations/presets/MainPreset.js @@ -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, ],