Skip to content

Commit

Permalink
feat(plugin-chart-echarts): Emit cross filters for pie and boxplot (#…
Browse files Browse the repository at this point in the history
…1010)

* feat(plugin-chart-echarts): emit filter events when clicking on slice

* feat(plugin-chart-echarts): enable emitting filter

* clean up filter state

* check eventHandlers by emitFilter flag

* feat(plugin-chart-echarts): multiple choices for filter

* WIP

* feat(plugin-chart-echarts): select multiple values

* add conditional control item for emitting filter

* add cross filter for box plot chart

* refactor: combine values and indexes single object

* fix: eslint

* Add default emitFilter for BoxPlot. Check emit filter before handle crossFilter

Co-authored-by: Ville Brofeldt <ville.v.brofeldt@gmail.com>
  • Loading branch information
2 people authored and zhaoyongjie committed Nov 26, 2021
1 parent 5da7a6c commit acd23f3
Show file tree
Hide file tree
Showing 12 changed files with 355 additions and 24 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 { BoxPlotChartTransformedProps } from './types';

export default function EchartsBoxPlot({ height, width, echartOptions }: EchartsProps) {
return <Echart height={height} width={width} echartOptions={echartOptions} />;
export default function EchartsBoxPlot({
height,
width,
echartOptions,
setDataMask,
labelMap,
groupby,
selectedValues,
formData,
}: BoxPlotChartTransformedProps) {
const handleChange = useCallback(
(values: string[]) => {
if (!formData.emitFilter) {
return;
}

const groupbyValues = values.map(value => labelMap[value]);

setDataMask({
crossFilters: {
extraFormData: {
append_form_data: {
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)[],
};
}),
},
},
currentState: {
value: groupbyValues.length ? groupbyValues : null,
},
},
ownFilters: {
currentState: {
selectedValues: values.length ? values : null,
},
},
});
},
[groupby, labelMap, setDataMask, selectedValues],
);

const eventHandlers: EventHandlers = {
click: props => {
const { name } = props;
const values = Object.values(selectedValues);
if (values.includes(name)) {
handleChange(values.filter(v => v !== name));
} else {
handleChange([...values, name]);
}
},
};

return (
<Echart
height={height}
width={width}
echartOptions={echartOptions}
eventHandlers={eventHandlers}
selectedValues={selectedValues}
/>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,17 @@
* specific language governing permissions and limitations
* under the License.
*/
import { t } from '@superset-ui/core';
import { FeatureFlag, isFeatureEnabled, t } from '@superset-ui/core';
import {
D3_FORMAT_DOCS,
D3_FORMAT_OPTIONS,
D3_TIME_FORMAT_OPTIONS,
formatSelectOptions,
sections,
} from '@superset-ui/chart-controls';
import { DEFAULT_FORM_DATA } from '../Pie/types';

const { emitFilter } = DEFAULT_FORM_DATA;

export default {
controlPanelSections: [
Expand Down Expand Up @@ -62,6 +65,20 @@ export default {
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.'),
},
},
]
: [],
[
{
name: 'x_ticks_layout',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { t, ChartMetadata, ChartPlugin } from '@superset-ui/core';
import { t, ChartMetadata, ChartPlugin, Behavior } from '@superset-ui/core';
import buildQuery from './buildQuery';
import controlPanel from './controlPanel';
import transformProps from './transformProps';
Expand All @@ -43,6 +43,7 @@ export default class EchartsBoxPlotChartPlugin extends ChartPlugin<
controlPanel,
loadChart: () => import('./EchartsBoxPlot'),
metadata: new ChartMetadata({
behaviors: [Behavior.CROSS_FILTER],
credits: ['https://echarts.apache.org'],
description: 'Box Plot (Apache ECharts)',
name: t('Box Plot'),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,27 @@
*/
import {
CategoricalColorNamespace,
DataRecordValue,
getMetricLabel,
getNumberFormatter,
getTimeFormatter,
} from '@superset-ui/core';
import { EChartsOption, BoxplotSeriesOption } from 'echarts';
import { CallbackDataParams } from 'echarts/types/src/util/types';
import { BoxPlotQueryFormData, EchartsBoxPlotChartProps } from './types';
import { EchartsProps } from '../types';
import {
BoxPlotChartTransformedProps,
BoxPlotQueryFormData,
EchartsBoxPlotChartProps,
} from './types';
import { extractGroupbyLabel, getColtypesMapping } from '../utils/series';
import { defaultGrid, defaultTooltip, defaultYAxis } from '../defaults';

export default function transformProps(chartProps: EchartsBoxPlotChartProps): EchartsProps {
const { width, height, formData, queriesData } = chartProps;
export default function transformProps(
chartProps: EchartsBoxPlotChartProps,
): BoxPlotChartTransformedProps {
const { width, height, formData, hooks, ownCurrentState, queriesData } = chartProps;
const { data = [] } = queriesData[0];
const { setDataMask = () => {} } = hooks;
const coltypeMapping = getColtypesMapping(queriesData[0]);
const {
colorScheme,
Expand All @@ -40,6 +47,7 @@ export default function transformProps(chartProps: EchartsBoxPlotChartProps): Ec
numberFormat,
dateFormat,
xTicksLayout,
emitFilter,
} = formData as BoxPlotQueryFormData;
const colorFn = CategoricalColorNamespace.getScale(colorScheme as string);
const numberFormatter = getNumberFormatter(numberFormat);
Expand Down Expand Up @@ -106,6 +114,31 @@ export default function transformProps(chartProps: EchartsBoxPlotChartProps): Ec
}),
)
.flat(2);

const labelMap = data.reduce((acc: Record<string, DataRecordValue[]>, datum) => {
const label = extractGroupbyLabel({
datum,
groupby,
coltypeMapping,
timeFormatter: getTimeFormatter(dateFormat),
});
return {
...acc,
[label]: groupby.map(col => datum[col]),
};
}, {});

const selectedValues = (ownCurrentState.selectedValues || []).reduce(
(acc: Record<string, number>, selectedValue: string) => {
const index = transformedData.findIndex(({ name }) => name === selectedValue);
return {
...acc,
[index]: selectedValue,
};
},
{},
);

let axisLabel;
if (xTicksLayout === '45°') axisLabel = { rotate: -45 };
else if (xTicksLayout === '90°') axisLabel = { rotate: -90 };
Expand Down Expand Up @@ -178,8 +211,14 @@ export default function transformProps(chartProps: EchartsBoxPlotChartProps): Ec
};

return {
formData,
width,
height,
echartOptions,
setDataMask,
emitFilter,
labelMap,
groupby,
selectedValues,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,21 @@
* specific language governing permissions and limitations
* under the License.
*/
import { ChartDataResponseResult, ChartProps, QueryFormData } from '@superset-ui/core';
import {
ChartDataResponseResult,
ChartProps,
DataRecordValue,
QueryFormData,
SetDataMaskHook,
} from '@superset-ui/core';
import { PostProcessingBoxplot } from '@superset-ui/core/lib/query/types/PostProcessing';
import { EChartsOption } from 'echarts';

export type BoxPlotQueryFormData = QueryFormData & {
numberFormat?: string;
whiskerOptions?: BoxPlotFormDataWhiskerOptions;
xTickLayout?: BoxPlotFormXTickLayout;
emitFilter: boolean;
};

export type BoxPlotFormDataWhiskerOptions =
Expand All @@ -33,9 +41,26 @@ export type BoxPlotFormDataWhiskerOptions =

export type BoxPlotFormXTickLayout = '45°' | '90°' | 'auto' | 'flat' | 'staggered';

// @ts-ignore
export const DEFAULT_FORM_DATA: BoxPlotQueryFormData = {
emitFilter: false,
};

export interface EchartsBoxPlotChartProps extends ChartProps {
formData: BoxPlotQueryFormData;
queriesData: ChartDataResponseResult[];
}

export type BoxPlotQueryObjectWhiskerType = PostProcessingBoxplot['options']['whisker_type'];

export interface BoxPlotChartTransformedProps {
formData: BoxPlotQueryFormData;
height: number;
width: number;
echartOptions: EChartsOption;
emitFilter: boolean;
setDataMask: SetDataMaskHook;
labelMap: Record<string, DataRecordValue[]>;
groupby: string[];
selectedValues: Record<number, string>;
}
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 { PieChartTransformedProps } from './types';
import Echart from '../components/Echart';
import { EventHandlers } from '../types';

export default function EchartsPie({ height, width, echartOptions }: EchartsProps) {
return <Echart height={height} width={width} echartOptions={echartOptions} />;
export default function EchartsPie({
height,
width,
echartOptions,
setDataMask,
labelMap,
groupby,
selectedValues,
formData,
}: PieChartTransformedProps) {
const handleChange = useCallback(
(values: string[]) => {
if (!formData.emitFilter) {
return;
}

const groupbyValues = values.map(value => labelMap[value]);

setDataMask({
crossFilters: {
extraFormData: {
append_form_data: {
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)[],
};
}),
},
},
currentState: {
value: groupbyValues.length ? groupbyValues : null,
},
},
ownFilters: {
currentState: {
selectedValues: values.length ? values : null,
},
},
});
},
[groupby, labelMap, setDataMask, selectedValues],
);

const eventHandlers: EventHandlers = {
click: props => {
const { name } = props;
const values = Object.values(selectedValues);
if (values.includes(name)) {
handleChange(values.filter(v => v !== name));
} else {
handleChange([...values, name]);
}
},
};

return (
<Echart
height={height}
width={width}
echartOptions={echartOptions}
eventHandlers={eventHandlers}
selectedValues={selectedValues}
/>
);
}

0 comments on commit acd23f3

Please sign in to comment.