feat: Adds the ECharts Sankey chart

This commit is contained in:
Michael S. Molina 2024-06-21 09:55:45 -03:00
parent fe3ba12801
commit d14da22e75
13 changed files with 368 additions and 1 deletions

View File

@ -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'),

View File

@ -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 (
<Echart
refs={refs}
height={height}
width={width}
echartOptions={echartOptions}
/>
);
}

View File

@ -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]] }),
},
]);
}

View File

@ -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;

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

View File

@ -0,0 +1,61 @@
/**
* 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('TODO'),
description: t(`TODO`),
exampleGallery: [{ url: example1 }, { url: example2 }],
name: t('Sankey Chart'),
tags: [],
thumbnail,
}),
transformProps,
});
}
}

View File

@ -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<string>();
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<SankeySeriesOption['data']> = 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<string, number>();
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,
};
}

View File

@ -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<SankeyFormData> {
formData: SankeyFormData;
}
export type SankeyTransformedProps = BaseTransformedProps<SankeyFormData> & {};

View File

@ -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';

View File

@ -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' }),