mirror of
https://github.com/apache/superset.git
synced 2026-06-09 17:49:26 +00:00
feat(plugin-chart-echarts): add x-filtering to treemap (#1115)
* fix(plugin-chart-echarts): add x-filtering to treemap * fix(plugin-chart-echarts): add behavior * fix(plugin-chart-echarts): one series at a time * fix(plugin-chart-echarts): type * fix(plugin-chart-echarts): color constant
This commit is contained in:
committed by
Yongjie Zhao
parent
b912b3fda1
commit
3d3c873f3c
@@ -16,10 +16,84 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { EchartsProps } from '../types';
|
||||
import React, { useCallback } from 'react';
|
||||
import Echart from '../components/Echart';
|
||||
import { EventHandlers } from '../types';
|
||||
import { extractTreePathInfo } from './constants';
|
||||
import { TreemapTransformedProps } from './types';
|
||||
|
||||
export default function EchartsTreemap({ height, width, echartOptions }: EchartsProps) {
|
||||
return <Echart height={height} width={width} echartOptions={echartOptions} forceClear />;
|
||||
export default function EchartsTreemap({
|
||||
height,
|
||||
width,
|
||||
echartOptions,
|
||||
setDataMask,
|
||||
labelMap,
|
||||
groupby,
|
||||
selectedValues,
|
||||
formData,
|
||||
}: TreemapTransformedProps) {
|
||||
const handleChange = useCallback(
|
||||
(values: string[]) => {
|
||||
if (!formData.emitFilter) {
|
||||
return;
|
||||
}
|
||||
|
||||
const groupbyValues = values.map(value => labelMap[value]);
|
||||
|
||||
setDataMask({
|
||||
extraFormData: {
|
||||
filters:
|
||||
values.length === 0
|
||||
? []
|
||||
: groupby.map((col, idx) => {
|
||||
const val = groupbyValues.map(v => v[idx]);
|
||||
if (val === null || val === undefined)
|
||||
return {
|
||||
col,
|
||||
op: 'IS NULL',
|
||||
};
|
||||
return {
|
||||
col,
|
||||
op: 'IN',
|
||||
val: val as (string | number | boolean)[],
|
||||
};
|
||||
}),
|
||||
},
|
||||
filterState: {
|
||||
value: groupbyValues.length ? groupbyValues : null,
|
||||
selectedValues: values.length ? values : null,
|
||||
},
|
||||
});
|
||||
},
|
||||
[groupby, labelMap, setDataMask, selectedValues],
|
||||
);
|
||||
|
||||
const eventHandlers: EventHandlers = {
|
||||
click: props => {
|
||||
const { data, treePathInfo } = props;
|
||||
// do noting when clicking the parent node
|
||||
if (data?.children) {
|
||||
return;
|
||||
}
|
||||
const { treePath } = extractTreePathInfo(treePathInfo);
|
||||
const name = treePath.join(',');
|
||||
const values = Object.values(selectedValues);
|
||||
if (values.includes(name)) {
|
||||
handleChange(values.filter(v => v !== name));
|
||||
} else {
|
||||
handleChange([name]);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<Echart
|
||||
height={height}
|
||||
width={width}
|
||||
echartOptions={echartOptions}
|
||||
eventHandlers={eventHandlers}
|
||||
selectedValues={selectedValues}
|
||||
forceClear
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 { TreePathInfo } from './types';
|
||||
|
||||
export const COLOR_SATURATION = [0.4, 0.7];
|
||||
export const LABEL_FONTSIZE = 11;
|
||||
export const BORDER_WIDTH = 2;
|
||||
export const GAP_WIDTH = 2;
|
||||
export const COLOR_ALPHA = 0.3;
|
||||
export const BORDER_COLOR = '#fff';
|
||||
|
||||
export const extractTreePathInfo = (treePathInfo: TreePathInfo[] | undefined) => {
|
||||
const treePath = (treePathInfo ?? [])
|
||||
.map(pathInfo => pathInfo?.name || '')
|
||||
.filter(path => path !== '');
|
||||
|
||||
// the 1st tree path is metric label
|
||||
const metricLabel = treePath.shift() || '';
|
||||
return { metricLabel, treePath };
|
||||
};
|
||||
@@ -17,7 +17,7 @@
|
||||
* under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { t } from '@superset-ui/core';
|
||||
import { FeatureFlag, isFeatureEnabled, t } from '@superset-ui/core';
|
||||
import {
|
||||
ControlPanelConfig,
|
||||
D3_FORMAT_DOCS,
|
||||
@@ -27,7 +27,14 @@ import {
|
||||
} from '@superset-ui/chart-controls';
|
||||
import { DEFAULT_FORM_DATA } from './types';
|
||||
|
||||
const { labelType, numberFormat, showLabels, showUpperLabels, dateFormat } = DEFAULT_FORM_DATA;
|
||||
const {
|
||||
labelType,
|
||||
numberFormat,
|
||||
showLabels,
|
||||
showUpperLabels,
|
||||
dateFormat,
|
||||
emitFilter,
|
||||
} = DEFAULT_FORM_DATA;
|
||||
|
||||
const config: ControlPanelConfig = {
|
||||
controlPanelSections: [
|
||||
@@ -57,6 +64,20 @@ const config: ControlPanelConfig = {
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
['color_scheme'],
|
||||
isFeatureEnabled(FeatureFlag.DASHBOARD_CROSS_FILTERS)
|
||||
? [
|
||||
{
|
||||
name: 'emit_filter',
|
||||
config: {
|
||||
type: 'CheckboxControl',
|
||||
label: t('Enable emitting filters'),
|
||||
default: emitFilter,
|
||||
renderTrigger: true,
|
||||
description: t('Enable emmiting filters.'),
|
||||
},
|
||||
},
|
||||
]
|
||||
: [],
|
||||
[<h1 className="section-header">{t('Labels')}</h1>],
|
||||
[
|
||||
{
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { ChartMetadata, ChartPlugin, t } from '@superset-ui/core';
|
||||
import { Behavior, ChartMetadata, ChartPlugin, t } from '@superset-ui/core';
|
||||
import buildQuery from './buildQuery';
|
||||
import controlPanel from './controlPanel';
|
||||
import transformProps from './transformProps';
|
||||
@@ -44,6 +44,7 @@ export default class EchartsTreemapChartPlugin extends ChartPlugin<
|
||||
controlPanel,
|
||||
loadChart: () => import('./EchartsTreemap'),
|
||||
metadata: new ChartMetadata({
|
||||
behaviors: [Behavior.INTERACTIVE_CHART],
|
||||
credits: ['https://echarts.apache.org'],
|
||||
description: 'Treemap (Apache ECharts)',
|
||||
name: t('Treemap v2'),
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
import {
|
||||
CategoricalColorNamespace,
|
||||
DataRecord,
|
||||
DataRecordValue,
|
||||
getMetricLabel,
|
||||
getNumberFormatter,
|
||||
getTimeFormatter,
|
||||
@@ -34,10 +35,19 @@ import {
|
||||
EchartsTreemapFormData,
|
||||
EchartsTreemapLabelType,
|
||||
TreemapSeriesCallbackDataParams,
|
||||
TreemapTransformedProps,
|
||||
} from './types';
|
||||
import { EchartsProps } from '../types';
|
||||
import { formatSeriesName, getColtypesMapping } from '../utils/series';
|
||||
import { defaultTooltip } from '../defaults';
|
||||
import {
|
||||
COLOR_ALPHA,
|
||||
COLOR_SATURATION,
|
||||
BORDER_WIDTH,
|
||||
GAP_WIDTH,
|
||||
LABEL_FONTSIZE,
|
||||
extractTreePathInfo,
|
||||
BORDER_COLOR,
|
||||
} from './constants';
|
||||
|
||||
export function formatLabel({
|
||||
params,
|
||||
@@ -72,6 +82,7 @@ export function formatTooltip({
|
||||
}): string {
|
||||
const { value, treePathInfo = [] } = params;
|
||||
const formattedValue = numberFormatter(value as number);
|
||||
const { metricLabel, treePath } = extractTreePathInfo(treePathInfo);
|
||||
const percentFormatter = getNumberFormatter(NumberFormats.PERCENT_2_POINT);
|
||||
|
||||
let formattedPercent = '';
|
||||
@@ -85,12 +96,6 @@ export function formatTooltip({
|
||||
formattedPercent = percentFormatter(percent);
|
||||
}
|
||||
|
||||
const treePath = (treePathInfo ?? [])
|
||||
.map(pathInfo => pathInfo?.name || '')
|
||||
.filter(path => path !== '');
|
||||
// the 1st tree path is metric label
|
||||
const metricLabel = treePath.shift() || '';
|
||||
|
||||
// groupby1/groupby2/...
|
||||
// metric: value (percent of parent)
|
||||
return [
|
||||
@@ -100,9 +105,12 @@ export function formatTooltip({
|
||||
].join('');
|
||||
}
|
||||
|
||||
export default function transformProps(chartProps: EchartsTreemapChartProps): EchartsProps {
|
||||
const { formData, height, queriesData, width } = chartProps;
|
||||
export default function transformProps(
|
||||
chartProps: EchartsTreemapChartProps,
|
||||
): TreemapTransformedProps {
|
||||
const { formData, height, queriesData, width, hooks, filterState } = chartProps;
|
||||
const { data = [] } = queriesData[0];
|
||||
const { setDataMask = () => {} } = hooks;
|
||||
const coltypeMapping = getColtypesMapping(queriesData[0]);
|
||||
|
||||
const {
|
||||
@@ -116,6 +124,7 @@ export default function transformProps(chartProps: EchartsTreemapChartProps): Ec
|
||||
showLabels,
|
||||
showUpperLabels,
|
||||
dashboardId,
|
||||
emitFilter,
|
||||
}: EchartsTreemapFormData = {
|
||||
...DEFAULT_TREEMAP_FORM_DATA,
|
||||
...formData,
|
||||
@@ -130,11 +139,14 @@ export default function transformProps(chartProps: EchartsTreemapChartProps): Ec
|
||||
labelType,
|
||||
});
|
||||
|
||||
const columnsLabelMap = new Map<string, DataRecordValue[]>();
|
||||
|
||||
const transformer = (
|
||||
data: DataRecord[],
|
||||
groupbyData: string[],
|
||||
metric: string,
|
||||
depth: number,
|
||||
path: string[],
|
||||
): TreemapSeriesNodeItemOption[] => {
|
||||
const [currGroupby, ...restGroupby] = groupbyData;
|
||||
const currGrouping = groupBy(data, currGroupby);
|
||||
@@ -148,10 +160,22 @@ export default function transformProps(chartProps: EchartsTreemapChartProps): Ec
|
||||
timeFormatter: getTimeFormatter(dateFormat),
|
||||
...(coltypeMapping[currGroupby] && { coltype: coltypeMapping[currGroupby] }),
|
||||
});
|
||||
result.push({
|
||||
const item: TreemapSeriesNodeItemOption = {
|
||||
name,
|
||||
value: isNumber(datum[metric]) ? (datum[metric] as number) : 0,
|
||||
});
|
||||
};
|
||||
const joinedName = path.concat(name).join(',');
|
||||
// map(joined_name: [columnLabel_1, columnLabel_2, ...])
|
||||
columnsLabelMap.set(joinedName, path.concat(name));
|
||||
if (filterState.selectedValues && !filterState.selectedValues.includes(joinedName)) {
|
||||
item.itemStyle = {
|
||||
colorAlpha: COLOR_ALPHA,
|
||||
};
|
||||
item.label = {
|
||||
color: `rgba(0, 0, 0, ${COLOR_ALPHA})`,
|
||||
};
|
||||
}
|
||||
result.push(item);
|
||||
});
|
||||
},
|
||||
[] as TreemapSeriesNodeItemOption[],
|
||||
@@ -165,7 +189,7 @@ export default function transformProps(chartProps: EchartsTreemapChartProps): Ec
|
||||
timeFormatter: getTimeFormatter(dateFormat),
|
||||
...(coltypeMapping[currGroupby] && { coltype: coltypeMapping[currGroupby] }),
|
||||
});
|
||||
const children = transformer(value, restGroupby, metric, depth + 1);
|
||||
const children = transformer(value, restGroupby, metric, depth + 1, path.concat(name));
|
||||
result.push({
|
||||
name,
|
||||
children,
|
||||
@@ -178,12 +202,12 @@ export default function transformProps(chartProps: EchartsTreemapChartProps): Ec
|
||||
// sort according to the area and then take the color value in order
|
||||
return sortedData.map(child => ({
|
||||
...child,
|
||||
colorSaturation: [0.4, 0.7],
|
||||
colorSaturation: COLOR_SATURATION,
|
||||
itemStyle: {
|
||||
borderColor: '#fff',
|
||||
borderColor: BORDER_COLOR,
|
||||
color: colorFn(`${child.name}_${depth}`),
|
||||
borderWidth: 2,
|
||||
gapWidth: 2,
|
||||
borderWidth: BORDER_WIDTH,
|
||||
gapWidth: GAP_WIDTH,
|
||||
},
|
||||
}));
|
||||
};
|
||||
@@ -193,16 +217,16 @@ export default function transformProps(chartProps: EchartsTreemapChartProps): Ec
|
||||
const transformedData: TreemapSeriesNodeItemOption[] = [
|
||||
{
|
||||
name: metricLabel,
|
||||
colorSaturation: [0.4, 0.7],
|
||||
colorSaturation: COLOR_SATURATION,
|
||||
itemStyle: {
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2,
|
||||
gapWidth: 2,
|
||||
borderColor: BORDER_COLOR,
|
||||
borderWidth: BORDER_WIDTH,
|
||||
gapWidth: GAP_WIDTH,
|
||||
},
|
||||
upperLabel: {
|
||||
show: false,
|
||||
},
|
||||
children: transformer(data, groupby, metricLabel, initialDepth),
|
||||
children: transformer(data, groupby, metricLabel, initialDepth, []),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -233,7 +257,6 @@ export default function transformProps(chartProps: EchartsTreemapChartProps): Ec
|
||||
show: false,
|
||||
emptyItemWidth: 25,
|
||||
},
|
||||
squareRatio: 0.5 * (1 + Math.sqrt(5)), // golden ratio
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
@@ -245,13 +268,13 @@ export default function transformProps(chartProps: EchartsTreemapChartProps): Ec
|
||||
position: labelPosition,
|
||||
formatter,
|
||||
color: '#000',
|
||||
fontSize: 11,
|
||||
fontSize: LABEL_FONTSIZE,
|
||||
},
|
||||
upperLabel: {
|
||||
show: showUpperLabels,
|
||||
formatter,
|
||||
textBorderColor: 'transparent',
|
||||
fontSize: 11,
|
||||
fontSize: LABEL_FONTSIZE,
|
||||
},
|
||||
data: transformedData,
|
||||
},
|
||||
@@ -271,8 +294,14 @@ export default function transformProps(chartProps: EchartsTreemapChartProps): Ec
|
||||
};
|
||||
|
||||
return {
|
||||
formData,
|
||||
width,
|
||||
height,
|
||||
echartOptions,
|
||||
setDataMask,
|
||||
emitFilter,
|
||||
labelMap: Object.fromEntries(columnsLabelMap),
|
||||
groupby,
|
||||
selectedValues: filterState.selectedValues || [],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -19,9 +19,12 @@
|
||||
import {
|
||||
ChartDataResponseResult,
|
||||
ChartProps,
|
||||
DataRecordValue,
|
||||
QueryFormData,
|
||||
QueryFormMetric,
|
||||
SetDataMaskHook,
|
||||
} from '@superset-ui/core';
|
||||
import { EChartsOption } from 'echarts';
|
||||
import { CallbackDataParams } from 'echarts/types/src/util/types';
|
||||
import { LabelPositionEnum } from '../types';
|
||||
|
||||
@@ -36,6 +39,7 @@ export type EchartsTreemapFormData = QueryFormData & {
|
||||
numberFormat: string;
|
||||
dateFormat: string;
|
||||
dashboardId?: number;
|
||||
emitFilter: boolean;
|
||||
};
|
||||
|
||||
export enum EchartsTreemapLabelType {
|
||||
@@ -57,9 +61,10 @@ export const DEFAULT_FORM_DATA: Partial<EchartsTreemapFormData> = {
|
||||
showLabels: true,
|
||||
showUpperLabels: true,
|
||||
dateFormat: 'smart_date',
|
||||
emitFilter: false,
|
||||
};
|
||||
|
||||
interface TreePathInfo {
|
||||
export interface TreePathInfo {
|
||||
name: string;
|
||||
dataIndex: number;
|
||||
value: number | number[];
|
||||
@@ -67,3 +72,15 @@ interface TreePathInfo {
|
||||
export interface TreemapSeriesCallbackDataParams extends CallbackDataParams {
|
||||
treePathInfo?: TreePathInfo[];
|
||||
}
|
||||
|
||||
export interface TreemapTransformedProps {
|
||||
formData: EchartsTreemapFormData;
|
||||
height: number;
|
||||
width: number;
|
||||
echartOptions: EChartsOption;
|
||||
emitFilter: boolean;
|
||||
setDataMask: SetDataMaskHook;
|
||||
labelMap: Record<string, DataRecordValue[]>;
|
||||
groupby: string[];
|
||||
selectedValues: Record<number, string>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user