feat: Adds drill to detail context menu for ECharts visualizations (#20891)

* feat: Adds drill to detail context menu for ECharts visualizations

* Rebases and adds time grain

* Fixes selected gauge values

* Fixes Treemap edge click

* Adds right click to big number trendline

* Address some comments
This commit is contained in:
Michael S. Molina 2022-08-09 17:02:31 -03:00 committed by GitHub
parent 0042ade66f
commit 3df8335f87
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 734 additions and 249 deletions

View File

@ -50,6 +50,8 @@ type Hooks = {
* also handles "change" and "remove".
*/
onAddFilter?: (newFilters: DataRecordFilters, merge?: boolean) => void;
/** handle right click */
onContextMenu?: HandlerFunction;
/** handle errors */
onError?: HandlerFunction;
/** use the vis as control to update state */
@ -136,6 +138,8 @@ export default class ChartProps<FormData extends RawFormData = RawFormData> {
inputRef?: RefObject<any>;
inContextMenu?: boolean;
theme: SupersetTheme;
constructor(config: ChartPropsConfig & { formData?: FormData } = {}) {
@ -154,6 +158,7 @@ export default class ChartProps<FormData extends RawFormData = RawFormData> {
appSection,
isRefreshing,
inputRef,
inContextMenu = false,
theme,
} = config;
this.width = width;
@ -172,6 +177,7 @@ export default class ChartProps<FormData extends RawFormData = RawFormData> {
this.appSection = appSection;
this.isRefreshing = isRefreshing;
this.inputRef = inputRef;
this.inContextMenu = inContextMenu;
this.theme = theme;
}
}
@ -193,6 +199,7 @@ ChartProps.createSelector = function create(): ChartPropsSelector {
input => input.appSection,
input => input.isRefreshing,
input => input.inputRef,
input => input.inContextMenu,
input => input.theme,
(
annotationData,
@ -209,6 +216,7 @@ ChartProps.createSelector = function create(): ChartPropsSelector {
appSection,
isRefreshing,
inputRef,
inContextMenu,
theme,
) =>
new ChartProps({
@ -226,6 +234,7 @@ ChartProps.createSelector = function create(): ChartPropsSelector {
appSection,
isRefreshing,
inputRef,
inContextMenu,
theme,
}),
);

View File

@ -40,13 +40,16 @@ export type QueryObjectFilterClause = {
| {
op: BinaryOperator;
val: string | number | boolean;
formattedVal?: string;
}
| {
op: SetOperator;
val: (string | number | boolean)[];
formattedVal?: string[];
}
| {
op: UnaryOperator;
formattedVal?: string;
}
);

View File

@ -57,6 +57,7 @@ export enum FeatureFlag {
USE_ANALAGOUS_COLORS = 'USE_ANALAGOUS_COLORS',
DASHBOARD_EDIT_CHART_IN_NEW_TAB = 'DASHBOARD_EDIT_CHART_IN_NEW_TAB',
EMBEDDABLE_CHARTS = 'EMBEDDABLE_CHARTS',
DRILL_TO_DETAIL = 'DRILL_TO_DETAIL',
}
export type ScheduleQueriesProps = {
JSONSCHEMA: {

View File

@ -27,7 +27,8 @@ import { BigNumberTotalChartProps } from '../types';
import { getDateFormatter, parseMetricValue } from '../utils';
export default function transformProps(chartProps: BigNumberTotalChartProps) {
const { width, height, queriesData, formData, rawFormData } = chartProps;
const { width, height, queriesData, formData, rawFormData, hooks } =
chartProps;
const {
headerFontSize,
metric = 'value',
@ -64,6 +65,8 @@ export default function transformProps(chartProps: BigNumberTotalChartProps) {
? formatTime
: getNumberFormatter(yAxisFormat ?? metricEntry?.d3format ?? undefined);
const { onContextMenu } = hooks;
return {
width,
height,
@ -72,5 +75,6 @@ export default function transformProps(chartProps: BigNumberTotalChartProps) {
headerFontSize,
subheaderFontSize,
subheader: formattedSubheader,
onContextMenu,
};
}

View File

@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import React, { MouseEvent } from 'react';
import {
t,
getNumberFormatter,
@ -26,10 +26,12 @@ import {
computeMaxFontSize,
BRAND_COLOR,
styled,
QueryObjectFilterClause,
} from '@superset-ui/core';
import { EChartsCoreOption } from 'echarts';
import Echart from '../components/Echart';
import { TimeSeriesDatum } from './types';
import { BigNumberWithTrendlineFormData, TimeSeriesDatum } from './types';
import { EventHandlers } from '../types';
const defaultNumberFormatter = getNumberFormatter();
@ -62,6 +64,13 @@ type BigNumberVisProps = {
trendLineData?: TimeSeriesDatum[];
mainColor: string;
echartOptions: EChartsCoreOption;
onContextMenu?: (
filters: QueryObjectFilterClause[],
offsetX: number,
offsetY: number,
) => void;
xValueFormatter?: TimeFormatter;
formData?: BigNumberWithTrendlineFormData;
};
class BigNumberVis extends React.PureComponent<BigNumberVisProps> {
@ -159,6 +168,17 @@ class BigNumberVis extends React.PureComponent<BigNumberVisProps> {
});
container.remove();
const onContextMenu = (e: MouseEvent<HTMLDivElement>) => {
if (this.props.onContextMenu) {
e.preventDefault();
this.props.onContextMenu(
[],
e.nativeEvent.offsetX,
e.nativeEvent.offsetY,
);
}
};
return (
<div
className="header-line"
@ -166,6 +186,7 @@ class BigNumberVis extends React.PureComponent<BigNumberVisProps> {
fontSize,
height: maxHeight,
}}
onContextMenu={onContextMenu}
>
{text}
</div>
@ -213,7 +234,7 @@ class BigNumberVis extends React.PureComponent<BigNumberVisProps> {
return null;
}
renderTrendline(maxHeight: number) {
renderTrendline(maxHeight: number, chartHeight: number) {
const { width, trendLineData, echartOptions } = this.props;
// if can't find any non-null values, no point rendering the trendline
@ -221,11 +242,37 @@ class BigNumberVis extends React.PureComponent<BigNumberVisProps> {
return null;
}
const eventHandlers: EventHandlers = {
contextmenu: eventParams => {
if (this.props.onContextMenu) {
eventParams.event.stop();
const { data } = eventParams;
if (data) {
const pointerEvent = eventParams.event.event;
const filters: QueryObjectFilterClause[] = [];
filters.push({
col: this.props.formData?.granularitySqla,
grain: this.props.formData?.timeGrainSqla,
op: '==',
val: data[0],
formattedVal: this.props.xValueFormatter?.(data[0]),
});
this.props.onContextMenu(
filters,
pointerEvent.offsetX,
chartHeight - 100,
);
}
}
},
};
return (
<Echart
width={Math.floor(width)}
height={maxHeight}
echartOptions={echartOptions}
eventHandlers={eventHandlers}
/>
);
}
@ -260,7 +307,7 @@ class BigNumberVis extends React.PureComponent<BigNumberVisProps> {
),
)}
</div>
{this.renderTrendline(chartHeight)}
{this.renderTrendline(chartHeight, height)}
</div>
);
}
@ -283,6 +330,7 @@ export default styled(BigNumberVis)`
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-start;
&.no-trendline .subheader-line {
padding-bottom: 0.3em;

View File

@ -62,8 +62,16 @@ const formatPercentChange = getNumberFormatter(
export default function transformProps(
chartProps: BigNumberWithTrendlineChartProps,
) {
const { width, height, queriesData, formData, rawFormData, theme } =
chartProps;
const {
width,
height,
queriesData,
formData,
rawFormData,
theme,
hooks,
inContextMenu,
} = chartProps;
const {
colorPicker,
compareLag: compareLag_,
@ -221,7 +229,7 @@ export default function transformProps(
},
tooltip: {
appendToBody: true,
show: true,
show: !inContextMenu,
trigger: 'axis',
confine: true,
formatter: renderTooltipFactory(formatTime, headerFormatter),
@ -234,6 +242,9 @@ export default function transformProps(
},
}
: {};
const { onContextMenu } = hooks;
return {
width,
height,
@ -242,6 +253,7 @@ export default function transformProps(
className,
headerFormatter,
formatTime,
formData,
headerFontSize,
subheaderFontSize,
mainColor,
@ -252,5 +264,7 @@ export default function transformProps(
timestamp,
trendLineData,
echartOptions,
onContextMenu,
xValueFormatter: formatTime,
};
}

View File

@ -18,19 +18,20 @@
*/
import React, { useCallback } from 'react';
import Echart from '../components/Echart';
import { EventHandlers } from '../types';
import { allEventHandlers } from '../utils/eventHandlers';
import { BoxPlotChartTransformedProps } from './types';
export default function EchartsBoxPlot({
height,
width,
echartOptions,
setDataMask,
labelMap,
groupby,
selectedValues,
formData,
}: BoxPlotChartTransformedProps) {
export default function EchartsBoxPlot(props: BoxPlotChartTransformedProps) {
const {
height,
width,
echartOptions,
setDataMask,
labelMap,
groupby,
selectedValues,
formData,
} = props;
const handleChange = useCallback(
(values: string[]) => {
if (!formData.emitFilter) {
@ -67,17 +68,7 @@ export default function EchartsBoxPlot({
[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([name]);
}
},
};
const eventHandlers = allEventHandlers(props, handleChange);
return (
<Echart

View File

@ -44,10 +44,17 @@ import { OpacityEnum } from '../constants';
export default function transformProps(
chartProps: EchartsBoxPlotChartProps,
): BoxPlotChartTransformedProps {
const { width, height, formData, hooks, filterState, queriesData } =
chartProps;
const {
width,
height,
formData,
hooks,
filterState,
queriesData,
inContextMenu,
} = chartProps;
const { data = [] } = queriesData[0];
const { setDataMask = () => {} } = hooks;
const { setDataMask = () => {}, onContextMenu } = hooks;
const coltypeMapping = getColtypesMapping(queriesData[0]);
const {
colorScheme,
@ -268,6 +275,7 @@ export default function transformProps(
},
tooltip: {
...defaultTooltip,
show: !inContextMenu,
trigger: 'item',
axisPointer: {
type: 'shadow',
@ -286,5 +294,6 @@ export default function transformProps(
labelMap,
groupby,
selectedValues,
onContextMenu,
};
}

View File

@ -19,13 +19,9 @@
import {
ChartDataResponseResult,
ChartProps,
DataRecordValue,
QueryFormColumn,
QueryFormData,
SetDataMaskHook,
} from '@superset-ui/core';
import { EChartsCoreOption } from 'echarts';
import { EchartsTitleFormData } from '../types';
import { EchartsTitleFormData, EChartTransformedProps } from '../types';
import { DEFAULT_TITLE_FORM_DATA } from '../constants';
export type BoxPlotQueryFormData = QueryFormData & {
@ -60,14 +56,5 @@ export interface EchartsBoxPlotChartProps
queriesData: ChartDataResponseResult[];
}
export interface BoxPlotChartTransformedProps {
formData: BoxPlotQueryFormData;
height: number;
width: number;
echartOptions: EChartsCoreOption;
emitFilter: boolean;
setDataMask: SetDataMaskHook;
labelMap: Record<string, DataRecordValue[]>;
groupby: QueryFormColumn[];
selectedValues: Record<number, string>;
}
export type BoxPlotChartTransformedProps =
EChartTransformedProps<BoxPlotQueryFormData>;

View File

@ -19,18 +19,19 @@
import React, { useCallback } from 'react';
import { FunnelChartTransformedProps } from './types';
import Echart from '../components/Echart';
import { EventHandlers } from '../types';
import { allEventHandlers } from '../utils/eventHandlers';
export default function EchartsFunnel({
height,
width,
echartOptions,
setDataMask,
labelMap,
groupby,
selectedValues,
formData,
}: FunnelChartTransformedProps) {
export default function EchartsFunnel(props: FunnelChartTransformedProps) {
const {
height,
width,
echartOptions,
setDataMask,
labelMap,
groupby,
selectedValues,
formData,
} = props;
const handleChange = useCallback(
(values: string[]) => {
if (!formData.emitFilter) {
@ -67,17 +68,7 @@ export default function EchartsFunnel({
[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([name]);
}
},
};
const eventHandlers = allEventHandlers(props, handleChange);
return (
<Echart

View File

@ -82,8 +82,16 @@ export function formatFunnelLabel({
export default function transformProps(
chartProps: EchartsFunnelChartProps,
): FunnelChartTransformedProps {
const { formData, height, hooks, filterState, queriesData, width, theme } =
chartProps;
const {
formData,
height,
hooks,
filterState,
queriesData,
width,
theme,
inContextMenu,
} = chartProps;
const data: DataRecord[] = queriesData[0].data || [];
const {
@ -128,7 +136,7 @@ export default function transformProps(
{},
);
const { setDataMask = () => {} } = hooks;
const { setDataMask = () => {}, onContextMenu } = hooks;
const colorFn = CategoricalColorNamespace.getScale(colorScheme as string);
const numberFormatter = getNumberFormatter(numberFormat);
@ -209,6 +217,7 @@ export default function transformProps(
},
tooltip: {
...defaultTooltip,
show: !inContextMenu,
trigger: 'item',
formatter: (params: any) =>
formatFunnelLabel({
@ -234,5 +243,6 @@ export default function transformProps(
labelMap,
groupby,
selectedValues,
onContextMenu,
};
}

View File

@ -16,16 +16,17 @@
* specific language governing permissions and limitations
* under the License.
*/
import { EChartsCoreOption } from 'echarts';
import {
ChartDataResponseResult,
ChartProps,
DataRecordValue,
QueryFormColumn,
QueryFormData,
SetDataMaskHook,
} from '@superset-ui/core';
import { EchartsLegendFormData, LegendOrientation, LegendType } from '../types';
import {
EchartsLegendFormData,
EChartTransformedProps,
LegendOrientation,
LegendType,
} from '../types';
import { DEFAULT_LEGEND_FORM_DATA } from '../constants';
export type EchartsFunnelFormData = QueryFormData &
@ -74,14 +75,5 @@ export const DEFAULT_FORM_DATA: EchartsFunnelFormData = {
emitFilter: false,
};
export interface FunnelChartTransformedProps {
formData: EchartsFunnelFormData;
height: number;
width: number;
echartOptions: EChartsCoreOption;
emitFilter: boolean;
setDataMask: SetDataMaskHook;
labelMap: Record<string, DataRecordValue[]>;
groupby: QueryFormColumn[];
selectedValues: Record<number, string>;
}
export type FunnelChartTransformedProps =
EChartTransformedProps<EchartsFunnelFormData>;

View File

@ -17,9 +17,10 @@
* under the License.
*/
import React, { useCallback } from 'react';
import { QueryObjectFilterClause } from '@superset-ui/core';
import { GaugeChartTransformedProps } from './types';
import Echart from '../components/Echart';
import { EventHandlers } from '../types';
import { Event, clickEventHandler } from '../utils/eventHandlers';
export default function EchartsGauge({
height,
@ -30,6 +31,7 @@ export default function EchartsGauge({
groupby,
selectedValues,
formData: { emitFilter },
onContextMenu,
}: GaugeChartTransformedProps) {
const handleChange = useCallback(
(values: string[]) => {
@ -67,14 +69,25 @@ export default function EchartsGauge({
[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([name]);
const eventHandlers = {
click: clickEventHandler(selectedValues, handleChange),
contextmenu: (e: Event) => {
if (onContextMenu) {
e.event.stop();
const pointerEvent = e.event.event;
const filters: QueryObjectFilterClause[] = [];
if (groupby.length > 0) {
const values = e.name.split(',');
groupby.forEach((dimension, i) =>
filters.push({
col: dimension,
op: '==',
val: values[i].split(': ')[1],
formattedVal: values[i].split(': ')[1],
}),
);
}
onContextMenu(filters, pointerEvent.offsetX, pointerEvent.offsetY);
}
},
};

View File

@ -194,7 +194,7 @@ export default function transformProps(
},
);
const { setDataMask = () => {} } = hooks;
const { setDataMask = () => {}, onContextMenu } = hooks;
const progress = {
show: showProgress,
@ -298,5 +298,6 @@ export default function transformProps(
labelMap: Object.fromEntries(columnsLabelMap),
groupby,
selectedValues: filterState.selectedValues || [],
onContextMenu,
};
}

View File

@ -17,13 +17,61 @@
* under the License.
*/
import React from 'react';
import { EchartsProps } from '../types';
import { QueryObjectFilterClause } from '@superset-ui/core';
import { EventHandlers } from '../types';
import Echart from '../components/Echart';
import { GraphChartTransformedProps } from './types';
type Event = {
name: string;
event: { stop: () => void; event: PointerEvent };
data: { source: string; target: string };
};
export default function EchartsGraph({
height,
width,
echartOptions,
}: EchartsProps) {
return <Echart height={height} width={width} echartOptions={echartOptions} />;
formData,
onContextMenu,
}: GraphChartTransformedProps) {
const eventHandlers: EventHandlers = {
contextmenu: (e: Event) => {
if (onContextMenu) {
e.event.stop();
const pointerEvent = e.event.event;
const data = (echartOptions as any).series[0].data as {
id: string;
name: string;
}[];
const sourceValue = data.find(item => item.id === e.data.source)?.name;
const targetValue = data.find(item => item.id === e.data.target)?.name;
if (sourceValue && targetValue) {
const filters: QueryObjectFilterClause[] = [
{
col: formData.source,
op: '==',
val: sourceValue,
formattedVal: sourceValue,
},
{
col: formData.target,
op: '==',
val: targetValue,
formattedVal: targetValue,
},
];
onContextMenu(filters, pointerEvent.offsetX, pointerEvent.offsetY);
}
}
},
};
return (
<Echart
height={height}
width={width}
echartOptions={echartOptions}
eventHandlers={eventHandlers}
/>
);
}

View File

@ -31,9 +31,9 @@ import {
EChartGraphNode,
DEFAULT_FORM_DATA as DEFAULT_GRAPH_FORM_DATA,
EdgeSymbol,
GraphChartTransformedProps,
} from './types';
import { DEFAULT_GRAPH_SERIES_OPTION } from './constants';
import { EchartsProps } from '../types';
import { getChartPadding, getLegendProps, sanitizeHtml } from '../utils/series';
type EdgeWithStyles = GraphEdgeItemOption & {
@ -157,8 +157,11 @@ function getCategoryName(columnName: string, name?: DataRecordValue) {
return String(name);
}
export default function transformProps(chartProps: ChartProps): EchartsProps {
const { width, height, formData, queriesData } = chartProps;
export default function transformProps(
chartProps: ChartProps,
): GraphChartTransformedProps {
const { width, height, formData, queriesData, hooks, inContextMenu } =
chartProps;
const data: DataRecord[] = queriesData[0].data || [];
const {
@ -295,6 +298,7 @@ export default function transformProps(chartProps: ChartProps): EchartsProps {
animationDuration: DEFAULT_GRAPH_SERIES_OPTION.animationDuration,
animationEasing: DEFAULT_GRAPH_SERIES_OPTION.animationEasing,
tooltip: {
show: !inContextMenu,
formatter: (params: any): string =>
edgeFormatter(
params.data.source,
@ -309,9 +313,14 @@ export default function transformProps(chartProps: ChartProps): EchartsProps {
},
series,
};
const { onContextMenu } = hooks;
return {
width,
height,
formData,
echartOptions,
onContextMenu,
};
}

View File

@ -16,10 +16,19 @@
* specific language governing permissions and limitations
* under the License.
*/
import { QueryFormData } from '@superset-ui/core';
import {
PlainObject,
QueryFormData,
QueryObjectFilterClause,
} from '@superset-ui/core';
import { GraphNodeItemOption } from 'echarts/types/src/chart/graph/GraphSeries';
import { SeriesTooltipOption } from 'echarts/types/src/util/types';
import { EchartsLegendFormData, LegendOrientation, LegendType } from '../types';
import {
EchartsLegendFormData,
EchartsProps,
LegendOrientation,
LegendType,
} from '../types';
import { DEFAULT_LEGEND_FORM_DATA } from '../constants';
export type EdgeSymbol = 'none' | 'circle' | 'arrow';
@ -75,3 +84,12 @@ export const DEFAULT_FORM_DATA: EchartsGraphFormData = {
export type tooltipFormatParams = {
data: { [name: string]: string };
};
export type GraphChartTransformedProps = EchartsProps & {
formData: PlainObject;
onContextMenu?: (
filters: QueryObjectFilterClause[],
offsetX: number,
offsetY: number,
) => void;
};

View File

@ -17,6 +17,7 @@
* under the License.
*/
import React, { useCallback } from 'react';
import { DataRecordValue, QueryObjectFilterClause } from '@superset-ui/core';
import { EchartsMixedTimeseriesChartTransformedProps } from './types';
import Echart from '../components/Echart';
import { EventHandlers } from '../types';
@ -34,6 +35,8 @@ export default function EchartsMixedTimeseries({
selectedValues,
formData,
seriesBreakdown,
onContextMenu,
xValueFormatter,
}: EchartsMixedTimeseriesChartTransformedProps) {
const isFirstQuery = useCallback(
(seriesIndex: number) => seriesIndex < seriesBreakdown,
@ -63,7 +66,9 @@ export default function EchartsMixedTimeseries({
? []
: [
...currentGroupBy.map((col, idx) => {
const val = groupbyValues.map(v => v[idx]);
const val: DataRecordValue[] = groupbyValues.map(
v => v[idx],
);
if (val === null || val === undefined)
return {
col,
@ -105,6 +110,35 @@ export default function EchartsMixedTimeseries({
mouseover: params => {
currentSeries.name = params.seriesName;
},
contextmenu: eventParams => {
if (onContextMenu) {
eventParams.event.stop();
const { data, seriesIndex } = eventParams;
if (data) {
const pointerEvent = eventParams.event.event;
const values = eventParams.seriesName.split(',');
const { queryIndex } = (echartOptions.series as any)[seriesIndex];
const groupby = queryIndex > 0 ? formData.groupbyB : formData.groupby;
const filters: QueryObjectFilterClause[] = [];
filters.push({
col: formData.granularitySqla,
grain: formData.timeGrainSqla,
op: '==',
val: data[0],
formattedVal: xValueFormatter(data[0]),
});
groupby.forEach((dimension, i) =>
filters.push({
col: dimension,
op: '==',
val: values[i],
formattedVal: values[i],
}),
);
onContextMenu(filters, pointerEvent.offsetX, pointerEvent.offsetY);
}
}
},
};
return (

View File

@ -84,6 +84,7 @@ export default function transformProps(
filterState,
datasource,
theme,
inContextMenu,
} = chartProps;
const { verboseMap = {} } = datasource;
const data1 = (queriesData[0].data || []) as TimeseriesDataRecord[];
@ -198,6 +199,7 @@ export default function transformProps(
filterState,
seriesKey: entry.name,
sliceId,
queryIndex: 0,
});
if (transformedSeries) series.push(transformedSeries);
});
@ -217,6 +219,7 @@ export default function transformProps(
? `${entry.name} (1)`
: entry.name,
sliceId,
queryIndex: 1,
});
if (transformedSeries) series.push(transformedSeries);
});
@ -319,7 +322,7 @@ export default function transformProps(
};
}, {}) as Record<string, DataRecordValue[]>;
const { setDataMask = () => {} } = hooks;
const { setDataMask = () => {}, onContextMenu } = hooks;
const alignTicks = yAxisIndex !== yAxisIndexB;
const echartOptions: EChartsCoreOption = {
@ -373,6 +376,7 @@ export default function transformProps(
],
tooltip: {
...defaultTooltip,
show: !inContextMenu,
appendToBody: true,
trigger: richTooltip ? 'axis' : 'item',
formatter: (params: any) => {
@ -459,5 +463,7 @@ export default function transformProps(
groupbyB,
seriesBreakdown: rawSeriesA.length,
selectedValues: filterState.selectedValues || [],
onContextMenu,
xValueFormatter: tooltipFormatter,
};
}

View File

@ -16,23 +16,23 @@
* specific language governing permissions and limitations
* under the License.
*/
import { EChartsCoreOption } from 'echarts';
import {
AnnotationLayer,
TimeGranularity,
DataRecordValue,
SetDataMaskHook,
QueryFormData,
ChartProps,
ChartDataResponseResult,
QueryFormColumn,
ContributionType,
TimeFormatter,
} from '@superset-ui/core';
import {
EchartsLegendFormData,
EchartsTitleFormData,
StackType,
EchartsTimeseriesSeriesType,
EChartTransformedProps,
} from '../types';
import {
DEFAULT_LEGEND_FORM_DATA,
@ -138,18 +138,11 @@ export interface EchartsMixedTimeseriesProps extends ChartProps {
queriesData: ChartDataResponseResult[];
}
export type EchartsMixedTimeseriesChartTransformedProps = {
formData: EchartsMixedTimeseriesFormData;
height: number;
width: number;
echartOptions: EChartsCoreOption;
emitFilter: boolean;
emitFilterB: boolean;
setDataMask: SetDataMaskHook;
groupby: QueryFormColumn[];
groupbyB: QueryFormColumn[];
labelMap: Record<string, DataRecordValue[]>;
labelMapB: Record<string, DataRecordValue[]>;
selectedValues: Record<number, string>;
seriesBreakdown: number;
};
export type EchartsMixedTimeseriesChartTransformedProps =
EChartTransformedProps<EchartsMixedTimeseriesFormData> & {
emitFilterB: boolean;
groupbyB: QueryFormColumn[];
labelMapB: Record<string, DataRecordValue[]>;
seriesBreakdown: number;
xValueFormatter: TimeFormatter | StringConstructor;
};

View File

@ -19,18 +19,19 @@
import React, { useCallback } from 'react';
import { PieChartTransformedProps } from './types';
import Echart from '../components/Echart';
import { EventHandlers } from '../types';
import { allEventHandlers } from '../utils/eventHandlers';
export default function EchartsPie({
height,
width,
echartOptions,
setDataMask,
labelMap,
groupby,
selectedValues,
formData,
}: PieChartTransformedProps) {
export default function EchartsPie(props: PieChartTransformedProps) {
const {
height,
width,
echartOptions,
setDataMask,
labelMap,
groupby,
selectedValues,
formData,
} = props;
const handleChange = useCallback(
(values: string[]) => {
if (!formData.emitFilter) {
@ -67,17 +68,7 @@ export default function EchartsPie({
[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([name]);
}
},
};
const eventHandlers = allEventHandlers(props, handleChange);
return (
<Echart

View File

@ -134,8 +134,16 @@ function getTotalValuePadding({
export default function transformProps(
chartProps: EchartsPieChartProps,
): PieChartTransformedProps {
const { formData, height, hooks, filterState, queriesData, width, theme } =
chartProps;
const {
formData,
height,
hooks,
filterState,
queriesData,
width,
theme,
inContextMenu,
} = chartProps;
const { data = [] } = queriesData[0];
const coltypeMapping = getColtypesMapping(queriesData[0]);
@ -193,7 +201,7 @@ export default function transformProps(
{},
);
const { setDataMask = () => {} } = hooks;
const { setDataMask = () => {}, onContextMenu } = hooks;
const colorFn = CategoricalColorNamespace.getScale(colorScheme as string);
const numberFormatter = getNumberFormatter(numberFormat);
@ -296,6 +304,7 @@ export default function transformProps(
...defaultGrid,
},
tooltip: {
show: !inContextMenu,
...defaultTooltip,
trigger: 'item',
formatter: (params: any) =>
@ -335,5 +344,6 @@ export default function transformProps(
labelMap,
groupby,
selectedValues,
onContextMenu,
};
}

View File

@ -16,16 +16,18 @@
* specific language governing permissions and limitations
* under the License.
*/
import { EChartsCoreOption } from 'echarts';
import {
ChartDataResponseResult,
ChartProps,
DataRecordValue,
QueryFormColumn,
QueryFormData,
SetDataMaskHook,
} from '@superset-ui/core';
import { EchartsLegendFormData, LegendOrientation, LegendType } from '../types';
import {
EchartsLegendFormData,
EChartTransformedProps,
LegendOrientation,
LegendType,
} from '../types';
import { DEFAULT_LEGEND_FORM_DATA } from '../constants';
export type EchartsPieFormData = QueryFormData &
@ -81,14 +83,5 @@ export const DEFAULT_FORM_DATA: EchartsPieFormData = {
dateFormat: 'smart_date',
};
export interface PieChartTransformedProps {
formData: EchartsPieFormData;
height: number;
width: number;
echartOptions: EChartsCoreOption;
emitFilter: boolean;
setDataMask: SetDataMaskHook;
labelMap: Record<string, DataRecordValue[]>;
groupby: QueryFormColumn[];
selectedValues: Record<number, string>;
}
export type PieChartTransformedProps =
EChartTransformedProps<EchartsPieFormData>;

View File

@ -19,18 +19,19 @@
import React, { useCallback } from 'react';
import { RadarChartTransformedProps } from './types';
import Echart from '../components/Echart';
import { EventHandlers } from '../types';
import { allEventHandlers } from '../utils/eventHandlers';
export default function EchartsRadar({
height,
width,
echartOptions,
setDataMask,
labelMap,
groupby,
selectedValues,
formData,
}: RadarChartTransformedProps) {
export default function EchartsRadar(props: RadarChartTransformedProps) {
const {
height,
width,
echartOptions,
setDataMask,
labelMap,
groupby,
selectedValues,
formData,
} = props;
const handleChange = useCallback(
(values: string[]) => {
if (!formData.emitFilter) {
@ -67,17 +68,7 @@ export default function EchartsRadar({
[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([name]);
}
},
};
const eventHandlers = allEventHandlers(props, handleChange);
return (
<Echart

View File

@ -70,8 +70,16 @@ export function formatLabel({
export default function transformProps(
chartProps: EchartsRadarChartProps,
): RadarChartTransformedProps {
const { formData, height, hooks, filterState, queriesData, width, theme } =
chartProps;
const {
formData,
height,
hooks,
filterState,
queriesData,
width,
theme,
inContextMenu,
} = chartProps;
const { data = [] } = queriesData[0];
const coltypeMapping = getColtypesMapping(queriesData[0]);
@ -91,12 +99,13 @@ export default function transformProps(
isCircle,
columnConfig,
sliceId,
emitFilter,
}: EchartsRadarFormData = {
...DEFAULT_LEGEND_FORM_DATA,
...DEFAULT_RADAR_FORM_DATA,
...formData,
};
const { setDataMask = () => {} } = hooks;
const { setDataMask = () => {}, onContextMenu } = hooks;
const colorFn = CategoricalColorNamespace.getScale(colorScheme as string);
const numberFormatter = getNumberFormatter(numberFormat);
@ -222,6 +231,7 @@ export default function transformProps(
},
tooltip: {
...defaultTooltip,
show: !inContextMenu,
trigger: 'item',
},
legend: {
@ -240,9 +250,11 @@ export default function transformProps(
width,
height,
echartOptions,
emitFilter,
setDataMask,
labelMap: Object.fromEntries(columnsLabelMap),
groupby,
selectedValues,
onContextMenu,
};
}

View File

@ -16,18 +16,16 @@
* specific language governing permissions and limitations
* under the License.
*/
import { EChartsCoreOption } from 'echarts';
import {
ChartDataResponseResult,
ChartProps,
DataRecordValue,
QueryFormColumn,
QueryFormData,
QueryFormMetric,
SetDataMaskHook,
} from '@superset-ui/core';
import {
EchartsLegendFormData,
EChartTransformedProps,
LabelPositionEnum,
LegendOrientation,
LegendType,
@ -79,13 +77,5 @@ export const DEFAULT_FORM_DATA: EchartsRadarFormData = {
isCircle: false,
};
export interface RadarChartTransformedProps {
formData: EchartsRadarFormData;
height: number;
width: number;
echartOptions: EChartsCoreOption;
setDataMask: SetDataMaskHook;
labelMap: Record<string, DataRecordValue[]>;
groupby: QueryFormColumn[];
selectedValues: Record<number, string>;
}
export type RadarChartTransformedProps =
EChartTransformedProps<EchartsRadarFormData>;

View File

@ -17,6 +17,7 @@
* under the License.
*/
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { QueryObjectFilterClause } from '@superset-ui/core';
import { ViewRootGroup } from 'echarts/types/src/util/types';
import GlobalModel from 'echarts/types/src/model/Global';
import ComponentModel from 'echarts/types/src/model/Component';
@ -40,6 +41,8 @@ export default function EchartsTimeseries({
setDataMask,
setControlValue,
legendData = [],
onContextMenu,
xValueFormatter,
}: TimeseriesChartTransformedProps) {
const { emitFilter, stack } = formData;
const echartRef = useRef<EchartsHandler | null>(null);
@ -173,6 +176,33 @@ export default function EchartsTimeseries({
handleDoubleClickChange();
}
},
contextmenu: eventParams => {
if (onContextMenu) {
eventParams.event.stop();
const { data } = eventParams;
if (data) {
const pointerEvent = eventParams.event.event;
const values = eventParams.seriesName.split(',');
const filters: QueryObjectFilterClause[] = [];
filters.push({
col: formData.granularitySqla,
grain: formData.timeGrainSqla,
op: '==',
val: data[0],
formattedVal: xValueFormatter(data[0]),
});
formData.groupby.forEach((dimension, i) =>
filters.push({
col: dimension,
op: '==',
val: values[i],
formattedVal: values[i],
}),
);
onContextMenu(filters, pointerEvent.offsetX, pointerEvent.offsetY);
}
}
},
};
const zrEventHandlers: EventHandlers = {

View File

@ -97,6 +97,7 @@ export default function transformProps(
queriesData,
datasource,
theme,
inContextMenu,
} = chartProps;
const { verboseMap = {} } = datasource;
const [queryData] = queriesData;
@ -302,6 +303,7 @@ export default function transformProps(
const {
setDataMask = () => {},
setControlValue = (...args: unknown[]) => {},
onContextMenu,
} = hooks;
const addYAxisLabelOffset = !!yAxisTitle;
@ -380,6 +382,7 @@ export default function transformProps(
xAxis,
yAxis,
tooltip: {
show: !inContextMenu,
...defaultTooltip,
appendToBody: true,
trigger: richTooltip ? 'axis' : 'item',
@ -457,5 +460,7 @@ export default function transformProps(
setControlValue,
width,
legendData,
onContextMenu,
xValueFormatter: tooltipFormatter,
};
}

View File

@ -97,6 +97,7 @@ export function transformSeries(
sliceId?: number;
isHorizontal?: boolean;
lineStyle?: LineStyleOption;
queryIndex?: number;
},
): SeriesOption | undefined {
const { name } = series;
@ -120,6 +121,7 @@ export function transformSeries(
seriesKey,
sliceId,
isHorizontal = false,
queryIndex = 0,
} = opts;
const contexts = seriesContexts[name || ''] || [];
const hasForecast =
@ -197,6 +199,7 @@ export function transformSeries(
: { ...opts.lineStyle, opacity };
return {
...series,
queryIndex,
yAxisIndex,
name: forecastSeries.name,
itemStyle,

View File

@ -24,6 +24,7 @@ import {
QueryFormData,
TimeGranularity,
ContributionType,
TimeFormatter,
} from '@superset-ui/core';
import {
EchartsLegendFormData,
@ -93,7 +94,9 @@ export interface EchartsTimeseriesChartProps
}
export type TimeseriesChartTransformedProps =
EChartTransformedProps<EchartsTimeseriesFormData>;
EChartTransformedProps<EchartsTimeseriesFormData> & {
xValueFormatter: TimeFormatter | StringConstructor;
};
export enum AxisType {
category = 'category',

View File

@ -16,6 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { QueryObjectFilterClause } from '@superset-ui/core';
import React, { useCallback } from 'react';
import Echart from '../components/Echart';
import { EventHandlers } from '../types';
@ -31,6 +32,7 @@ export default function EchartsTreemap({
groupby,
selectedValues,
formData,
onContextMenu,
}: TreemapTransformedProps) {
const handleChange = useCallback(
(values: string[]) => {
@ -71,7 +73,7 @@ export default function EchartsTreemap({
const eventHandlers: EventHandlers = {
click: props => {
const { data, treePathInfo } = props;
// do noting when clicking the parent node
// do nothing when clicking on the parent node
if (data?.children) {
return;
}
@ -84,6 +86,25 @@ export default function EchartsTreemap({
handleChange([name]);
}
},
contextmenu: eventParams => {
if (onContextMenu) {
eventParams.event.stop();
const { treePath } = extractTreePathInfo(eventParams.treePathInfo);
if (treePath.length > 0) {
const pointerEvent = eventParams.event.event;
const filters: QueryObjectFilterClause[] = [];
treePath.forEach((path, i) =>
filters.push({
col: groupby[i],
op: '==',
val: path,
formattedVal: path,
}),
);
onContextMenu(filters, pointerEvent.offsetX, pointerEvent.offsetY);
}
}
},
};
return (

View File

@ -109,10 +109,18 @@ export function formatTooltip({
export default function transformProps(
chartProps: EchartsTreemapChartProps,
): TreemapTransformedProps {
const { formData, height, queriesData, width, hooks, filterState, theme } =
chartProps;
const {
formData,
height,
queriesData,
width,
hooks,
filterState,
theme,
inContextMenu,
} = chartProps;
const { data = [] } = queriesData[0];
const { setDataMask = () => {} } = hooks;
const { setDataMask = () => {}, onContextMenu } = hooks;
const coltypeMapping = getColtypesMapping(queriesData[0]);
const {
@ -303,6 +311,7 @@ export default function transformProps(
const echartOptions: EChartsCoreOption = {
tooltip: {
...defaultTooltip,
show: !inContextMenu,
trigger: 'item',
formatter: (params: any) =>
formatTooltip({
@ -323,5 +332,6 @@ export default function transformProps(
labelMap: Object.fromEntries(columnsLabelMap),
groupby,
selectedValues: filterState.selectedValues || [],
onContextMenu,
};
}

View File

@ -19,15 +19,12 @@
import {
ChartDataResponseResult,
ChartProps,
DataRecordValue,
QueryFormColumn,
QueryFormData,
QueryFormMetric,
SetDataMaskHook,
} from '@superset-ui/core';
import { EChartsCoreOption } from 'echarts';
import { CallbackDataParams } from 'echarts/types/src/util/types';
import { LabelPositionEnum } from '../types';
import { EChartTransformedProps, LabelPositionEnum } from '../types';
export type EchartsTreemapFormData = QueryFormData & {
colorScheme?: string;
@ -75,14 +72,5 @@ export interface TreemapSeriesCallbackDataParams extends CallbackDataParams {
treePathInfo?: TreePathInfo[];
}
export interface TreemapTransformedProps {
formData: EchartsTreemapFormData;
height: number;
width: number;
echartOptions: EChartsCoreOption;
emitFilter: boolean;
setDataMask: SetDataMaskHook;
labelMap: Record<string, DataRecordValue[]>;
groupby: QueryFormColumn[];
selectedValues: Record<number, string>;
}
export type TreemapTransformedProps =
EChartTransformedProps<EchartsTreemapFormData>;

View File

@ -20,6 +20,7 @@ import {
DataRecordValue,
HandlerFunction,
QueryFormColumn,
QueryObjectFilterClause,
SetDataMaskHook,
} from '@superset-ui/core';
import { EChartsCoreOption, ECharts } from 'echarts';
@ -115,6 +116,11 @@ export interface EChartTransformedProps<F> {
groupby: QueryFormColumn[];
selectedValues: Record<number, string>;
legendData?: OptionName[];
onContextMenu?: (
filters: QueryObjectFilterClause[],
offsetX: number,
offsetY: number,
) => void;
}
export interface EchartsTitleFormData {

View File

@ -0,0 +1,76 @@
/**
* 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 { QueryObjectFilterClause } from '@superset-ui/core';
import { EChartTransformedProps, EventHandlers } from '../types';
export type Event = {
name: string;
event: { stop: () => void; event: PointerEvent };
};
export const clickEventHandler =
(
selectedValues: Record<number, string>,
handleChange: (values: string[]) => void,
) =>
({ name }: { name: string }) => {
const values = Object.values(selectedValues);
if (values.includes(name)) {
handleChange(values.filter(v => v !== name));
} else {
handleChange([name]);
}
};
export const contextMenuEventHandler =
(
groupby: EChartTransformedProps<any>['groupby'],
onContextMenu: EChartTransformedProps<any>['onContextMenu'],
) =>
(e: Event) => {
if (onContextMenu) {
e.event.stop();
const pointerEvent = e.event.event;
const filters: QueryObjectFilterClause[] = [];
if (groupby.length > 0) {
const values = e.name.split(',');
groupby.forEach((dimension, i) =>
filters.push({
col: dimension,
op: '==',
val: values[i],
formattedVal: values[i],
}),
);
}
onContextMenu(filters, pointerEvent.offsetX, pointerEvent.offsetY);
}
};
export const allEventHandlers = (
transformedProps: EChartTransformedProps<any>,
handleChange: (values: string[]) => void,
) => {
const { groupby, selectedValues, onContextMenu } = transformedProps;
const eventHandlers: EventHandlers = {
click: clickEventHandler(selectedValues, handleChange),
contextmenu: contextMenuEventHandler(groupby, onContextMenu),
};
return eventHandlers;
};

View File

@ -0,0 +1,123 @@
/**
* 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 React, {
forwardRef,
RefObject,
useCallback,
useImperativeHandle,
useState,
} from 'react';
import { QueryObjectFilterClause, t, styled } from '@superset-ui/core';
import { Menu } from 'src/components/Menu';
import { AntdDropdown as Dropdown } from 'src/components';
export interface ChartContextMenuProps {
id: string;
onSelection: (filters: QueryObjectFilterClause[]) => void;
onClose: () => void;
}
export interface Ref {
open: (
filters: QueryObjectFilterClause[],
offsetX: number,
offsetY: number,
) => void;
}
const Filter = styled.span`
${({ theme }) => `
font-weight: ${theme.typography.weights.bold};
color: ${theme.colors.primary.base};
`}
`;
const ChartContextMenu = (
{ id, onSelection, onClose }: ChartContextMenuProps,
ref: RefObject<Ref>,
) => {
const [state, setState] = useState<{
filters: QueryObjectFilterClause[];
offsetX: number;
offsetY: number;
}>({ filters: [], offsetX: 0, offsetY: 0 });
const menu = (
<Menu>
{state.filters.map((filter, i) => (
<Menu.Item key={i} onClick={() => onSelection([filter])}>
{`${t('Drill to detail by')} `}
<Filter>{filter.formattedVal}</Filter>
</Menu.Item>
))}
{state.filters.length === 0 && (
<Menu.Item key="none" onClick={() => onSelection([])}>
{t('Drill to detail')}
</Menu.Item>
)}
{state.filters.length > 1 && (
<Menu.Item key="all" onClick={() => onSelection(state.filters)}>
{`${t('Drill to detail by')} `}
<Filter>{t('all')}</Filter>
</Menu.Item>
)}
</Menu>
);
const open = useCallback(
(filters: QueryObjectFilterClause[], offsetX: number, offsetY: number) => {
setState({ filters, offsetX, offsetY });
// Since Ant Design's Dropdown does not offer an imperative API
// and we can't attach event triggers to charts SVG elements, we
// use a hidden span that gets clicked on when receiving click events
// from the charts.
document.getElementById(`hidden-span-${id}`)?.click();
},
[id],
);
useImperativeHandle(
ref,
() => ({
open,
}),
[open],
);
return (
<Dropdown
overlay={menu}
trigger={['click']}
onVisibleChange={value => !value && onClose()}
>
<span
id={`hidden-span-${id}`}
css={{
visibility: 'hidden',
position: 'absolute',
top: state.offsetY,
left: state.offsetX,
}}
/>
</Dropdown>
);
};
export default forwardRef(ChartContextMenu);

View File

@ -19,9 +19,17 @@
import { snakeCase, isEqual } from 'lodash';
import PropTypes from 'prop-types';
import React from 'react';
import { SuperChart, logging, Behavior, t } from '@superset-ui/core';
import {
SuperChart,
logging,
Behavior,
t,
isFeatureEnabled,
FeatureFlag,
} from '@superset-ui/core';
import { Logger, LOG_ACTIONS_RENDER_CHART } from 'src/logger/LogUtils';
import { EmptyStateBig, EmptyStateSmall } from 'src/components/EmptyState';
import ChartContextMenu from './ChartContextMenu';
const propTypes = {
annotationData: PropTypes.object,
@ -73,15 +81,28 @@ const defaultProps = {
class ChartRenderer extends React.Component {
constructor(props) {
super(props);
this.state = {
inContextMenu: false,
};
this.hasQueryResponseChange = false;
this.contextMenuRef = React.createRef();
this.handleAddFilter = this.handleAddFilter.bind(this);
this.handleRenderSuccess = this.handleRenderSuccess.bind(this);
this.handleRenderFailure = this.handleRenderFailure.bind(this);
this.handleSetControlValue = this.handleSetControlValue.bind(this);
this.handleOnContextMenu = this.handleOnContextMenu.bind(this);
this.handleContextMenuSelected = this.handleContextMenuSelected.bind(this);
this.handleContextMenuClosed = this.handleContextMenuClosed.bind(this);
const showContextMenu =
props.source === 'dashboard' &&
isFeatureEnabled(FeatureFlag.DRILL_TO_DETAIL);
this.hooks = {
onAddFilter: this.handleAddFilter,
onContextMenu: showContextMenu ? this.handleOnContextMenu : undefined,
onError: this.handleRenderFailure,
setControlValue: this.handleSetControlValue,
onFilterMenuOpen: this.props.onFilterMenuOpen,
@ -92,13 +113,16 @@ class ChartRenderer extends React.Component {
};
}
shouldComponentUpdate(nextProps) {
shouldComponentUpdate(nextProps, nextState) {
const resultsReady =
nextProps.queriesResponse &&
['success', 'rendered'].indexOf(nextProps.chartStatus) > -1 &&
!nextProps.queriesResponse?.[0]?.error;
if (resultsReady) {
if (!isEqual(this.state, nextState)) {
return true;
}
this.hasQueryResponseChange =
nextProps.queriesResponse !== this.props.queriesResponse;
return (
@ -172,6 +196,22 @@ class ChartRenderer extends React.Component {
}
}
handleOnContextMenu(filters, offsetX, offsetY) {
this.contextMenuRef.current.open(filters, offsetX, offsetY);
this.setState({ inContextMenu: true });
}
handleContextMenuSelected(filters) {
const extraFilters = this.props.formData.extra_form_data?.filters || [];
// eslint-disable-next-line no-alert
alert(JSON.stringify(filters.concat(extraFilters)));
this.setState({ inContextMenu: false });
}
handleContextMenuClosed() {
this.setState({ inContextMenu: false });
}
render() {
const { chartAlert, chartStatus, chartId } = this.props;
@ -247,28 +287,39 @@ class ChartRenderer extends React.Component {
}
return (
<SuperChart
disableErrorBoundary
key={`${chartId}${webpackHash}`}
id={`chart-id-${chartId}`}
className={chartClassName}
chartType={vizType}
width={width}
height={height}
annotationData={annotationData}
datasource={datasource}
initialValues={initialValues}
formData={currentFormData}
ownState={ownState}
filterState={filterState}
hooks={this.hooks}
behaviors={behaviors}
queriesData={queriesResponse}
onRenderSuccess={this.handleRenderSuccess}
onRenderFailure={this.handleRenderFailure}
noResults={noResultsComponent}
postTransformProps={postTransformProps}
/>
<div>
{this.props.source === 'dashboard' && (
<ChartContextMenu
ref={this.contextMenuRef}
id={chartId}
onSelection={this.handleContextMenuSelected}
onClose={this.handleContextMenuClosed}
/>
)}
<SuperChart
disableErrorBoundary
key={`${chartId}${webpackHash}`}
id={`chart-id-${chartId}`}
className={chartClassName}
chartType={vizType}
width={width}
height={height}
annotationData={annotationData}
datasource={datasource}
initialValues={initialValues}
formData={currentFormData}
ownState={ownState}
filterState={filterState}
hooks={this.hooks}
behaviors={behaviors}
queriesData={queriesResponse}
onRenderSuccess={this.handleRenderSuccess}
onRenderFailure={this.handleRenderFailure}
noResults={noResultsComponent}
postTransformProps={postTransformProps}
inContextMenu={this.state.inContextMenu}
/>
</div>
);
}
}

View File

@ -438,6 +438,7 @@ DEFAULT_FEATURE_FLAGS: Dict[str, bool] = {
"CACHE_IMPERSONATION": False,
# Enable sharing charts with embedding
"EMBEDDABLE_CHARTS": True,
"DRILL_TO_DETAIL": False,
}
# Feature flags may also be set via 'SUPERSET_FEATURE_' prefixed environment vars.