Skip to content

Commit

Permalink
feat(plugin-chart-echarts): add x-filtering to treemap (#1115)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
stephenLYZ authored and zhaoyongjie committed Nov 26, 2021
1 parent b912b3f commit 3d3c873
Show file tree
Hide file tree
Showing 6 changed files with 211 additions and 32 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
/>
);
}
Original file line number Diff line number Diff line change
@@ -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 };
};
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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: [
Expand Down Expand Up @@ -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>],
[
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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'),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import {
CategoricalColorNamespace,
DataRecord,
DataRecordValue,
getMetricLabel,
getNumberFormatter,
getTimeFormatter,
Expand All @@ -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,
Expand Down Expand Up @@ -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 = '';
Expand All @@ -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 [
Expand All @@ -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 {
Expand All @@ -116,6 +124,7 @@ export default function transformProps(chartProps: EchartsTreemapChartProps): Ec
showLabels,
showUpperLabels,
dashboardId,
emitFilter,
}: EchartsTreemapFormData = {
...DEFAULT_TREEMAP_FORM_DATA,
...formData,
Expand All @@ -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);
Expand All @@ -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[],
Expand All @@ -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,
Expand All @@ -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,
},
}));
};
Expand All @@ -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, []),
},
];

Expand Down Expand Up @@ -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,
Expand All @@ -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,
},
Expand All @@ -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 || [],
};
}

0 comments on commit 3d3c873

Please sign in to comment.