diff --git a/superset-frontend/plugins/legacy-plugin-chart-sankey/src/index.js b/superset-frontend/plugins/legacy-plugin-chart-sankey/src/index.js index 5b90e4b4b3845..e679441187a10 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-sankey/src/index.js +++ b/superset-frontend/plugins/legacy-plugin-chart-sankey/src/index.js @@ -33,7 +33,7 @@ const metadata = new ChartMetadata({ { url: example1, description: t('Demographics') }, { url: example2, description: t('Survey Responses') }, ], - name: t('Sankey Diagram'), + name: t('Sankey Diagram (legacy)'), tags: [ t('Categorical'), t('Directional'), diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Sankey/Sankey.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Sankey/Sankey.tsx new file mode 100644 index 0000000000000..b01da214c3704 --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Sankey/Sankey.tsx @@ -0,0 +1,33 @@ +/** + * 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 { SankeyTransformedProps } from './types'; +import Echart from '../components/Echart'; + +export default function Sankey(props: SankeyTransformedProps) { + const { height, width, echartOptions, refs } = props; + + return ( + + ); +} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Sankey/buildQuery.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Sankey/buildQuery.ts new file mode 100644 index 0000000000000..0ec7ec85bf0f8 --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Sankey/buildQuery.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. + */ +import { buildQueryContext } from '@superset-ui/core'; +import { SankeyFormData } from './types'; + +export default function buildQuery(formData: SankeyFormData) { + const { metric, sort_by_metric, source, target } = formData; + const groupby = [source, target]; + return buildQueryContext(formData, baseQueryObject => [ + { + ...baseQueryObject, + groupby, + ...(sort_by_metric && { orderby: [[metric, false]] }), + }, + ]); +} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Sankey/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Sankey/controlPanel.tsx new file mode 100644 index 0000000000000..05a7ac57cf610 --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Sankey/controlPanel.tsx @@ -0,0 +1,75 @@ +/** + * 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 { t, validateNonEmpty } from '@superset-ui/core'; +import { + ControlPanelConfig, + dndGroupByControl, +} from '@superset-ui/chart-controls'; + +const config: ControlPanelConfig = { + controlPanelSections: [ + { + label: t('Query'), + expanded: true, + controlSetRows: [ + [ + { + name: 'source', + config: { + ...dndGroupByControl, + label: t('Source'), + multi: false, + description: t( + 'The column to be used as the source of the edge.', + ), + validators: [validateNonEmpty], + freeForm: false, + }, + }, + ], + [ + { + name: 'target', + config: { + ...dndGroupByControl, + label: t('Target'), + multi: false, + description: t( + 'The column to be used as the target of the edge.', + ), + validators: [validateNonEmpty], + freeForm: false, + }, + }, + ], + ['metric'], + ['adhoc_filters'], + ['row_limit'], + ['sort_by_metric'], + ], + }, + { + label: t('Chart Options'), + expanded: true, + controlSetRows: [['color_scheme']], + }, + ], +}; + +export default config; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Sankey/images/example1.png b/superset-frontend/plugins/plugin-chart-echarts/src/Sankey/images/example1.png new file mode 100644 index 0000000000000..ff5ca93400974 Binary files /dev/null and b/superset-frontend/plugins/plugin-chart-echarts/src/Sankey/images/example1.png differ diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Sankey/images/example2.png b/superset-frontend/plugins/plugin-chart-echarts/src/Sankey/images/example2.png new file mode 100644 index 0000000000000..9ae0754bb42dd Binary files /dev/null and b/superset-frontend/plugins/plugin-chart-echarts/src/Sankey/images/example2.png differ diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Sankey/images/thumbnail.png b/superset-frontend/plugins/plugin-chart-echarts/src/Sankey/images/thumbnail.png new file mode 100644 index 0000000000000..6f18b45f08fe3 Binary files /dev/null and b/superset-frontend/plugins/plugin-chart-echarts/src/Sankey/images/thumbnail.png differ diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Sankey/index.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Sankey/index.ts new file mode 100644 index 0000000000000..77348bdab7176 --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Sankey/index.ts @@ -0,0 +1,66 @@ +/** + * 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 + * regardin + * g 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 { Behavior, ChartMetadata, ChartPlugin, t } from '@superset-ui/core'; +import buildQuery from './buildQuery'; +import controlPanel from './controlPanel'; +import transformProps from './transformProps'; +import thumbnail from './images/thumbnail.png'; +import example1 from './images/example1.png'; +import example2 from './images/example2.png'; +import { SankeyChartProps, SankeyFormData } from './types'; + +export default class EchartsSankeyChartPlugin extends ChartPlugin< + SankeyFormData, + SankeyChartProps +> { + /** + * The constructor is used to pass relevant metadata and callbacks that get + * registered in respective registries that are used throughout the library + * and application. A more thorough description of each property is given in + * the respective imported file. + * + * It is worth noting that `buildQuery` and is optional, and only needed for + * advanced visualizations that require either post processing operations + * (pivoting, rolling aggregations, sorting etc) or submitting multiple queries. + */ + constructor() { + super({ + buildQuery, + controlPanel, + loadChart: () => import('./Sankey'), + metadata: new ChartMetadata({ + behaviors: [Behavior.InteractiveChart], + credits: ['https://echarts.apache.org'], + category: t('Flow'), + description: t( + `The Sankey chart visually tracks the movement and transformation of values across + system stages. Nodes represent stages, connected by links depicting value flow. Node + height corresponds to the visualized metric, providing a clear representation of + value distribution and transformation.`, + ), + exampleGallery: [{ url: example1 }, { url: example2 }], + name: t('Sankey Chart'), + tags: [t('Directional'), t('Distribution'), t('Flow')], + thumbnail, + }), + transformProps, + }); + } +} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Sankey/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Sankey/transformProps.ts new file mode 100644 index 0000000000000..96be18c8988ba --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Sankey/transformProps.ts @@ -0,0 +1,125 @@ +/** + * 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 { EChartsOption, SankeySeriesOption } from 'echarts'; +import { CallbackDataParams } from 'echarts/types/src/util/types'; +import { + CategoricalColorNamespace, + NumberFormats, + getColumnLabel, + getMetricLabel, + getNumberFormatter, + tooltipHtml, +} from '@superset-ui/core'; +import { SankeyChartProps, SankeyTransformedProps } from './types'; +import { Refs } from '../types'; +import { getDefaultTooltip } from '../utils/tooltip'; +import { getPercentFormatter } from '../utils/formatters'; + +type Link = { source: string; target: string; value: number }; + +export default function transformProps( + chartProps: SankeyChartProps, +): SankeyTransformedProps { + const refs: Refs = {}; + const { formData, height, hooks, queriesData, width } = chartProps; + const { onLegendStateChanged } = hooks; + const { colorScheme, metric, source, target } = formData; + const { data } = queriesData[0]; + const colorFn = CategoricalColorNamespace.getScale(colorScheme); + const metricLabel = getMetricLabel(metric); + const valueFormatter = getNumberFormatter(NumberFormats.FLOAT_2_POINT); + const percentFormatter = getPercentFormatter(NumberFormats.PERCENT_2_POINT); + + const links: Link[] = []; + const set = new Set(); + data.forEach(datum => { + const sourceName = String(datum[getColumnLabel(source)]); + const targetName = String(datum[getColumnLabel(target)]); + const value = datum[metricLabel] as number; + set.add(sourceName); + set.add(targetName); + links.push({ + source: sourceName, + target: targetName, + value, + }); + }); + + const seriesData: NonNullable = Array.from( + set, + ).map(name => ({ + name, + itemStyle: { + color: colorFn(name), + }, + })); + + // stores a map with the total values for each node considering the links + const nodeValues = new Map(); + links.forEach(link => { + const { source, target, value } = link; + const sourceValue = nodeValues.get(source) || 0; + const targetValue = nodeValues.get(target) || 0; + nodeValues.set(source, sourceValue + value); + nodeValues.set(target, targetValue + value); + }); + + const tooltipFormatter = (params: CallbackDataParams) => { + const { name, data } = params; + const value = params.value as number; + const rows = [[metricLabel, valueFormatter.format(value)]]; + const { source, target } = data as Link; + if (source && target) { + rows.push([ + `% (${source})`, + percentFormatter.format(value / nodeValues.get(source)!), + ]); + rows.push([ + `% (${target})`, + percentFormatter.format(value / nodeValues.get(target)!), + ]); + } + return tooltipHtml(rows, name); + }; + + const echartOptions: EChartsOption = { + series: { + animation: false, + data: seriesData, + lineStyle: { + color: 'source', + }, + links, + type: 'sankey', + }, + tooltip: { + ...getDefaultTooltip(refs), + formatter: tooltipFormatter, + }, + }; + + return { + refs, + formData, + width, + height, + echartOptions, + onLegendStateChanged, + }; +} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Sankey/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Sankey/types.ts new file mode 100644 index 0000000000000..323bd1612ca80 --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Sankey/types.ts @@ -0,0 +1,37 @@ +/** + * 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 { + QueryFormColumn, + QueryFormData, + QueryFormMetric, +} from '@superset-ui/core'; +import { BaseChartProps, BaseTransformedProps } from '../types'; + +export type SankeyFormData = QueryFormData & { + colorScheme: string; + metric: QueryFormMetric; + source: QueryFormColumn; + target: QueryFormColumn; +}; + +export interface SankeyChartProps extends BaseChartProps { + formData: SankeyFormData; +} + +export type SankeyTransformedProps = BaseTransformedProps & {}; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/index.ts b/superset-frontend/plugins/plugin-chart-echarts/src/index.ts index 36290ec450300..5c3c3e4c7f1f7 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/index.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/index.ts @@ -41,6 +41,7 @@ export { } from './BigNumber'; export { default as EchartsSunburstChartPlugin } from './Sunburst'; export { default as EchartsBubbleChartPlugin } from './Bubble'; +export { default as EchartsSankeyChartPlugin } from './Sankey'; export { default as EchartsWaterfallChartPlugin } from './Waterfall'; export { default as BoxPlotTransformProps } from './BoxPlot/transformProps'; @@ -58,6 +59,7 @@ export { default as SunburstTransformProps } from './Sunburst/transformProps'; export { default as BubbleTransformProps } from './Bubble/transformProps'; export { default as WaterfallTransformProps } from './Waterfall/transformProps'; export { default as HistogramTransformProps } from './Histogram/transformProps'; +export { default as SankeyTransformProps } from './Sankey/transformProps'; export { DEFAULT_FORM_DATA as TimeseriesDefaultFormData } from './Timeseries/constants'; diff --git a/superset-frontend/src/visualizations/presets/MainPreset.js b/superset-frontend/src/visualizations/presets/MainPreset.js index 196f18b8d6e84..72dc9151c3b8c 100644 --- a/superset-frontend/src/visualizations/presets/MainPreset.js +++ b/superset-frontend/src/visualizations/presets/MainPreset.js @@ -61,6 +61,7 @@ import { EchartsHistogramChartPlugin, EchartsRadarChartPlugin, EchartsFunnelChartPlugin, + EchartsSankeyChartPlugin, EchartsTreemapChartPlugin, EchartsMixedTimeseriesChartPlugin, EchartsTreeChartPlugin, @@ -112,6 +113,7 @@ export default class MainPreset extends Preset { new DistBarChartPlugin().configure({ key: 'dist_bar' }), new EventFlowChartPlugin().configure({ key: 'event_flow' }), new EchartsFunnelChartPlugin().configure({ key: 'funnel' }), + new EchartsSankeyChartPlugin().configure({ key: 'sankey_v2' }), new EchartsTreemapChartPlugin().configure({ key: 'treemap_v2' }), new EchartsGaugeChartPlugin().configure({ key: 'gauge_chart' }), new EchartsGraphChartPlugin().configure({ key: 'graph_chart' }), diff --git a/superset/cli/viz_migrations.py b/superset/cli/viz_migrations.py index 9714969073099..6b6ba1a7046d9 100644 --- a/superset/cli/viz_migrations.py +++ b/superset/cli/viz_migrations.py @@ -33,6 +33,7 @@ class VizType(str, Enum): HISTOGRAM = "histogram" LINE = "line" PIVOT_TABLE = "pivot_table" + SANKEY = "sankey" SUNBURST = "sunburst" TREEMAP = "treemap" @@ -89,6 +90,7 @@ def migrate(viz_type: VizType, is_downgrade: bool = False) -> None: MigrateHistogramChart, MigrateLineChart, MigratePivotTable, + MigrateSankey, MigrateSunburst, MigrateTreeMap, ) @@ -103,6 +105,7 @@ def migrate(viz_type: VizType, is_downgrade: bool = False) -> None: VizType.HISTOGRAM: MigrateHistogramChart, VizType.LINE: MigrateLineChart, VizType.PIVOT_TABLE: MigratePivotTable, + VizType.SANKEY: MigrateSankey, VizType.SUNBURST: MigrateSunburst, VizType.TREEMAP: MigrateTreeMap, } diff --git a/superset/examples/configs/charts/Featured Charts/Sankey.yaml b/superset/examples/configs/charts/Featured Charts/Sankey.yaml new file mode 100644 index 0000000000000..8540f5f6499ee --- /dev/null +++ b/superset/examples/configs/charts/Featured Charts/Sankey.yaml @@ -0,0 +1,41 @@ +# 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. +slice_name: Sankey +description: null +certified_by: null +certification_details: null +viz_type: sankey_v2 +params: + datasource: 22__table + viz_type: sankey_v2 + source: product_line + target: deal_size + metric: count + adhoc_filters: + - clause: WHERE + subject: order_date + operator: TEMPORAL_RANGE + comparator: No filter + expressionType: SIMPLE + row_limit: 10000 + color_scheme: supersetColors + extra_form_data: {} + dashboards: [] +cache_timeout: null +uuid: fead4d46-ecb9-4fd9-8b9c-420629269c02 +version: 1.0.0 +dataset_uuid: e8623bb9-5e00-f531-506a-19607f5f8005 diff --git a/superset/examples/configs/dashboards/Featured_Charts.yaml b/superset/examples/configs/dashboards/Featured_Charts.yaml index 2d0e0c4f1b296..598db88ad6185 100644 --- a/superset/examples/configs/dashboards/Featured_Charts.yaml +++ b/superset/examples/configs/dashboards/Featured_Charts.yaml @@ -49,7 +49,7 @@ position: parents: - ROOT_ID - GRID_ID - - ROW-Jq9auQfs6- + - ROW-3XARWMYOfz type: CHART CHART-3tEC_8e-uS: children: [] @@ -147,7 +147,7 @@ position: parents: - ROOT_ID - GRID_ID - - ROW-3XARWMYOfz + - ROW-ux6j1ePT8I type: CHART CHART-EpsTnvUMuW: children: [] @@ -273,7 +273,7 @@ position: parents: - ROOT_ID - GRID_ID - - ROW-ux6j1ePT8I + - ROW-we3i1eOT75 type: CHART CHART-gfrGP3BD76: children: [] @@ -289,6 +289,20 @@ position: - GRID_ID - ROW-W7YILGiS0- type: CHART + CHART-Yi0u5d9otw: + children: [] + id: CHART-Yi0u5d9otw + meta: + chartId: 359 + height: 50 + sliceName: Sankey + uuid: fead4d46-ecb9-4fd9-8b9c-420629269c02 + width: 4 + parents: + - ROOT_ID + - GRID_ID + - ROW-Jq9auQfs6- + type: CHART CHART-j2o9aZo4HY: children: [] id: CHART-j2o9aZo4HY @@ -370,6 +384,7 @@ position: - ROW-Jq9auQfs6- - ROW-3XARWMYOfz - ROW-ux6j1ePT8I + - ROW-we3i1eOT75 id: GRID_ID parents: - ROOT_ID @@ -386,9 +401,9 @@ position: type: ROOT ROW-3XARWMYOfz: children: + - CHART-33vjmwrGX1 - CHART-3tEC_8e-uS - CHART-A4qrvR24Ne - - CHART-DqaJJ8Fse6 id: ROW-3XARWMYOfz meta: background: BACKGROUND_TRANSPARENT @@ -400,7 +415,7 @@ position: children: - CHART-qZh51tuuRH - CHART-j2o9aZo4HY - - CHART-33vjmwrGX1 + - CHART-Yi0u5d9otw id: ROW-Jq9auQfs6- meta: background: BACKGROUND_TRANSPARENT @@ -470,9 +485,9 @@ position: type: ROW ROW-ux6j1ePT8I: children: + - CHART-DqaJJ8Fse6 - CHART-XwFZukVv8E - CHART-KE7lk61Tbt - - CHART-aAhaxRYu_t id: ROW-ux6j1ePT8I meta: background: BACKGROUND_TRANSPARENT @@ -480,6 +495,16 @@ position: - ROOT_ID - GRID_ID type: ROW + ROW-we3i1eOT75: + children: + - CHART-aAhaxRYu_t + id: ROW-we3i1eOT75 + meta: + background: BACKGROUND_TRANSPARENT + parents: + - ROOT_ID + - GRID_ID + type: ROW metadata: color_scheme: supersetAndPresetColors refresh_frequency: 0 diff --git a/superset/migrations/shared/migrate_viz/processors.py b/superset/migrations/shared/migrate_viz/processors.py index ef5a1c6bd6094..99c545d98fa98 100644 --- a/superset/migrations/shared/migrate_viz/processors.py +++ b/superset/migrations/shared/migrate_viz/processors.py @@ -303,3 +303,15 @@ def _pre_action(self) -> None: groupby = self.data.get("groupby") if not groupby: self.data["groupby"] = [] + + +class MigrateSankey(MigrateViz): + source_viz_type = "sankey" + target_viz_type = "sankey_v2" + remove_keys = {"groupby"} + + def _pre_action(self) -> None: + groupby = self.data.get("groupby") + if groupby and len(groupby) > 1: + self.data["source"] = groupby[0] + self.data["target"] = groupby[1]