From 3d3c873f3cfee2def4db08af55087b4c5d2df16e Mon Sep 17 00:00:00 2001 From: Yaozong Liu <750188453@qq.com> Date: Tue, 25 May 2021 23:58:57 +0800 Subject: [PATCH] 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 --- .../src/Treemap/EchartsTreemap.tsx | 82 ++++++++++++++++++- .../src/Treemap/constants.ts | 37 +++++++++ .../src/Treemap/controlPanel.tsx | 25 +++++- .../plugin-chart-echarts/src/Treemap/index.ts | 3 +- .../src/Treemap/transformProps.ts | 77 +++++++++++------ .../plugin-chart-echarts/src/Treemap/types.ts | 19 ++++- 6 files changed, 211 insertions(+), 32 deletions(-) create mode 100644 superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Treemap/constants.ts diff --git a/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Treemap/EchartsTreemap.tsx b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Treemap/EchartsTreemap.tsx index c6250b4ae29..9c8d466ef99 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Treemap/EchartsTreemap.tsx +++ b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Treemap/EchartsTreemap.tsx @@ -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 ; +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 ( + + ); } diff --git a/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Treemap/constants.ts b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Treemap/constants.ts new file mode 100644 index 00000000000..b31e9da7921 --- /dev/null +++ b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Treemap/constants.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 { 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 }; +}; diff --git a/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Treemap/controlPanel.tsx b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Treemap/controlPanel.tsx index d8e09df8328..b05845ec056 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Treemap/controlPanel.tsx +++ b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Treemap/controlPanel.tsx @@ -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.'), + }, + }, + ] + : [], [

{t('Labels')}

], [ { diff --git a/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Treemap/index.ts b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Treemap/index.ts index 2e3954853d1..947f48ba6f8 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Treemap/index.ts +++ b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Treemap/index.ts @@ -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'), diff --git a/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Treemap/transformProps.ts b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Treemap/transformProps.ts index ce0ba854e95..bbec3216f8f 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Treemap/transformProps.ts +++ b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Treemap/transformProps.ts @@ -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(); + 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 || [], }; } diff --git a/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Treemap/types.ts b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Treemap/types.ts index 8d7a74399a9..90494833410 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Treemap/types.ts +++ b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-echarts/src/Treemap/types.ts @@ -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 = { 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; + groupby: string[]; + selectedValues: Record; +}