mirror of https://github.com/apache/superset.git
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:
parent
0042ade66f
commit
3df8335f87
|
@ -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,
|
||||
}),
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -18,10 +18,11 @@
|
|||
*/
|
||||
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({
|
||||
export default function EchartsBoxPlot(props: BoxPlotChartTransformedProps) {
|
||||
const {
|
||||
height,
|
||||
width,
|
||||
echartOptions,
|
||||
|
@ -30,7 +31,7 @@ export default function EchartsBoxPlot({
|
|||
groupby,
|
||||
selectedValues,
|
||||
formData,
|
||||
}: BoxPlotChartTransformedProps) {
|
||||
} = 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
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -19,9 +19,10 @@
|
|||
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({
|
||||
export default function EchartsFunnel(props: FunnelChartTransformedProps) {
|
||||
const {
|
||||
height,
|
||||
width,
|
||||
echartOptions,
|
||||
|
@ -30,7 +31,7 @@ export default function EchartsFunnel({
|
|||
groupby,
|
||||
selectedValues,
|
||||
formData,
|
||||
}: FunnelChartTransformedProps) {
|
||||
} = 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
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
export type EchartsMixedTimeseriesChartTransformedProps =
|
||||
EChartTransformedProps<EchartsMixedTimeseriesFormData> & {
|
||||
emitFilterB: boolean;
|
||||
setDataMask: SetDataMaskHook;
|
||||
groupby: QueryFormColumn[];
|
||||
groupbyB: QueryFormColumn[];
|
||||
labelMap: Record<string, DataRecordValue[]>;
|
||||
labelMapB: Record<string, DataRecordValue[]>;
|
||||
selectedValues: Record<number, string>;
|
||||
seriesBreakdown: number;
|
||||
xValueFormatter: TimeFormatter | StringConstructor;
|
||||
};
|
||||
|
|
|
@ -19,9 +19,10 @@
|
|||
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({
|
||||
export default function EchartsPie(props: PieChartTransformedProps) {
|
||||
const {
|
||||
height,
|
||||
width,
|
||||
echartOptions,
|
||||
|
@ -30,7 +31,7 @@ export default function EchartsPie({
|
|||
groupby,
|
||||
selectedValues,
|
||||
formData,
|
||||
}: PieChartTransformedProps) {
|
||||
} = 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
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -19,9 +19,10 @@
|
|||
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({
|
||||
export default function EchartsRadar(props: RadarChartTransformedProps) {
|
||||
const {
|
||||
height,
|
||||
width,
|
||||
echartOptions,
|
||||
|
@ -30,7 +31,7 @@ export default function EchartsRadar({
|
|||
groupby,
|
||||
selectedValues,
|
||||
formData,
|
||||
}: RadarChartTransformedProps) {
|
||||
} = 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
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
};
|
|
@ -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);
|
|
@ -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,6 +287,15 @@ class ChartRenderer extends React.Component {
|
|||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{this.props.source === 'dashboard' && (
|
||||
<ChartContextMenu
|
||||
ref={this.contextMenuRef}
|
||||
id={chartId}
|
||||
onSelection={this.handleContextMenuSelected}
|
||||
onClose={this.handleContextMenuClosed}
|
||||
/>
|
||||
)}
|
||||
<SuperChart
|
||||
disableErrorBoundary
|
||||
key={`${chartId}${webpackHash}`}
|
||||
|
@ -268,7 +317,9 @@ class ChartRenderer extends React.Component {
|
|||
onRenderFailure={this.handleRenderFailure}
|
||||
noResults={noResultsComponent}
|
||||
postTransformProps={postTransformProps}
|
||||
inContextMenu={this.state.inContextMenu}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Reference in New Issue