From 3bc0865d9071cdf32d268ee8fee4c4ad93680429 Mon Sep 17 00:00:00 2001 From: "Michael S. Molina" <70410625+michael-s-molina@users.noreply.github.com> Date: Wed, 23 Nov 2022 14:50:06 -0500 Subject: [PATCH] fix: Drill to detail blocked by tooltip (#22082) Co-authored-by: Ville Brofeldt --- .../BigNumberTotal/transformProps.ts | 9 ++- .../src/BigNumber/BigNumberViz.tsx | 70 ++++++----------- .../BigNumberWithTrendline/transformProps.ts | 13 +++- .../src/BigNumber/types.ts | 60 ++++++++++++--- .../src/BoxPlot/EchartsBoxPlot.tsx | 2 + .../src/BoxPlot/transformProps.ts | 8 +- .../plugin-chart-echarts/src/BoxPlot/types.ts | 21 ++--- .../src/Funnel/EchartsFunnel.tsx | 2 + .../src/Funnel/transformProps.ts | 8 +- .../plugin-chart-echarts/src/Funnel/types.ts | 22 +++--- .../src/Gauge/EchartsGauge.tsx | 2 + .../src/Gauge/transformProps.ts | 6 ++ .../plugin-chart-echarts/src/Gauge/types.ts | 19 ++--- .../src/Graph/EchartsGraph.tsx | 2 + .../src/Graph/transformProps.ts | 9 ++- .../plugin-chart-echarts/src/Graph/types.ts | 29 ++++--- .../EchartsMixedTimeseries.tsx | 6 +- .../src/MixedTimeseries/transformProps.ts | 13 +++- .../src/MixedTimeseries/types.ts | 43 ++++++----- .../src/Pie/EchartsPie.tsx | 2 + .../src/Pie/transformProps.ts | 8 +- .../plugin-chart-echarts/src/Pie/types.ts | 24 +++--- .../src/Radar/EchartsRadar.tsx | 2 + .../src/Radar/transformProps.ts | 8 +- .../plugin-chart-echarts/src/Radar/types.ts | 19 +++-- .../src/Timeseries/EchartsTimeseries.tsx | 5 +- .../src/Timeseries/transformProps.ts | 9 ++- .../src/Timeseries/types.ts | 40 +++++----- .../src/Tree/EchartsTree.tsx | 14 +++- .../src/Tree/constants.ts | 16 ++++ .../src/Tree/controlPanel.tsx | 2 +- .../src/Tree/transformProps.ts | 18 +++-- .../plugin-chart-echarts/src/Tree/types.ts | 34 ++++----- .../src/Treemap/EchartsTreemap.tsx | 14 ++-- .../src/Treemap/transformProps.ts | 8 +- .../plugin-chart-echarts/src/Treemap/types.ts | 11 ++- .../src/components/Echart.tsx | 6 ++ .../plugin-chart-echarts/src/constants.ts | 15 +++- .../plugin-chart-echarts/src/defaults.ts | 56 +++++++++++++- .../plugins/plugin-chart-echarts/src/types.ts | 51 +++++++++---- .../src/utils/eventHandlers.ts | 13 +++- .../plugin-chart-echarts/src/utils/tooltip.ts | 76 +++++++++++++++++++ .../test/BigNumber/transformProps.test.ts | 8 +- .../test/Graph/transformProps.test.ts | 13 ++-- .../test/Tree/transformProps.test.ts | 11 +-- 45 files changed, 572 insertions(+), 255 deletions(-) create mode 100644 superset-frontend/plugins/plugin-chart-echarts/src/utils/tooltip.ts diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/transformProps.ts index 3438654831..e690b1ef52 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/transformProps.ts @@ -23,10 +23,13 @@ import { extractTimegrain, QueryFormData, } from '@superset-ui/core'; -import { BigNumberTotalChartProps } from '../types'; +import { BigNumberTotalChartProps, BigNumberVizProps } from '../types'; import { getDateFormatter, parseMetricValue } from '../utils'; +import { Refs } from '../../types'; -export default function transformProps(chartProps: BigNumberTotalChartProps) { +export default function transformProps( + chartProps: BigNumberTotalChartProps, +): BigNumberVizProps { const { width, height, queriesData, formData, rawFormData, hooks } = chartProps; const { @@ -38,6 +41,7 @@ export default function transformProps(chartProps: BigNumberTotalChartProps) { timeFormat, yAxisFormat, } = formData; + const refs: Refs = {}; const { data = [], coltypes = [] } = queriesData[0]; const granularity = extractTimegrain(rawFormData as QueryFormData); const metricName = getMetricLabel(metric); @@ -76,5 +80,6 @@ export default function transformProps(chartProps: BigNumberTotalChartProps) { subheaderFontSize, subheader: formattedSubheader, onContextMenu, + refs, }; } diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberViz.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberViz.tsx index b7516561cd..669926d58b 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberViz.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberViz.tsx @@ -20,17 +20,14 @@ import React, { MouseEvent } from 'react'; import { t, getNumberFormatter, - NumberFormatter, smartDateVerboseFormatter, - TimeFormatter, computeMaxFontSize, BRAND_COLOR, styled, BinaryQueryObjectFilterClause, } from '@superset-ui/core'; -import { EChartsCoreOption } from 'echarts'; import Echart from '../components/Echart'; -import { BigNumberWithTrendlineFormData, TimeSeriesDatum } from './types'; +import { BigNumberVizProps } from './types'; import { EventHandlers } from '../types'; const defaultNumberFormatter = getNumberFormatter(); @@ -44,36 +41,7 @@ const PROPORTION = { TRENDLINE: 0.3, }; -type BigNumberVisProps = { - className?: string; - width: number; - height: number; - bigNumber?: number | null; - bigNumberFallback?: TimeSeriesDatum; - headerFormatter: NumberFormatter | TimeFormatter; - formatTime: TimeFormatter; - headerFontSize: number; - kickerFontSize: number; - subheader: string; - subheaderFontSize: number; - showTimestamp?: boolean; - showTrendLine?: boolean; - startYAxisAtZero?: boolean; - timeRangeFixed?: boolean; - timestamp?: number; - trendLineData?: TimeSeriesDatum[]; - mainColor: string; - echartOptions: EChartsCoreOption; - onContextMenu?: ( - clientX: number, - clientY: number, - filters?: BinaryQueryObjectFilterClause[], - ) => void; - xValueFormatter?: TimeFormatter; - formData?: BigNumberWithTrendlineFormData; -}; - -class BigNumberVis extends React.PureComponent { +class BigNumberVis extends React.PureComponent { static defaultProps = { className: '', headerFormatter: defaultNumberFormatter, @@ -108,7 +76,7 @@ class BigNumberVis extends React.PureComponent { renderFallbackWarning() { const { bigNumberFallback, formatTime, showTimestamp } = this.props; - if (!bigNumberFallback || showTimestamp) return null; + if (!formatTime || !bigNumberFallback || showTimestamp) return null; return ( { renderKicker(maxHeight: number) { const { timestamp, showTimestamp, formatTime, width } = this.props; - if (!showTimestamp) return null; + if ( + !formatTime || + !showTimestamp || + typeof timestamp === 'string' || + typeof timestamp === 'boolean' + ) + return null; const text = timestamp === null ? '' : formatTime(timestamp); @@ -155,6 +129,7 @@ class BigNumberVis extends React.PureComponent { renderHeader(maxHeight: number) { const { bigNumber, headerFormatter, width } = this.props; + // @ts-ignore const text = bigNumber === null ? t('No data') : headerFormatter(bigNumber); const container = this.createTemporaryContainer(); @@ -231,7 +206,7 @@ class BigNumberVis extends React.PureComponent { } renderTrendline(maxHeight: number) { - const { width, trendLineData, echartOptions } = this.props; + const { width, trendLineData, echartOptions, refs } = this.props; // if can't find any non-null values, no point rendering the trendline if (!trendLineData?.some(d => d[1] !== null)) { @@ -264,12 +239,15 @@ class BigNumberVis extends React.PureComponent { }; return ( - + echartOptions && ( + + ) ); } @@ -292,7 +270,9 @@ class BigNumberVis extends React.PureComponent {
{this.renderFallbackWarning()} {this.renderKicker( - Math.ceil(kickerFontSize * (1 - PROPORTION.TRENDLINE) * height), + Math.ceil( + (kickerFontSize || 0) * (1 - PROPORTION.TRENDLINE) * height, + ), )} {this.renderHeader( Math.ceil(headerFontSize * (1 - PROPORTION.TRENDLINE) * height), @@ -311,7 +291,7 @@ class BigNumberVis extends React.PureComponent { return (
{this.renderFallbackWarning()} - {this.renderKicker(kickerFontSize * height)} + {this.renderKicker((kickerFontSize || 0) * height)} {this.renderHeader(Math.ceil(headerFontSize * height))} {this.renderSubheader(Math.ceil(subheaderFontSize * height))}
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/transformProps.ts index 2d8cc129c0..e668a75fbe 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/transformProps.ts @@ -30,11 +30,14 @@ import { } from '@superset-ui/core'; import { EChartsCoreOption, graphic } from 'echarts'; import { + BigNumberVizProps, BigNumberDatum, BigNumberWithTrendlineChartProps, TimeSeriesDatum, } from '../types'; import { getDateFormatter, parseMetricValue } from '../utils'; +import { getDefaultPosition } from '../../utils/tooltip'; +import { Refs } from '../../types'; const defaultNumberFormatter = getNumberFormatter(); export function renderTooltipFactory( @@ -60,7 +63,7 @@ const formatPercentChange = getNumberFormatter( export default function transformProps( chartProps: BigNumberWithTrendlineChartProps, -) { +): BigNumberVizProps { const { width, height, @@ -95,6 +98,7 @@ export default function transformProps( from_dttm: fromDatetime, to_dttm: toDatetime, } = queriesData[0]; + const refs: Refs = {}; const metricName = getMetricLabel(metric); const compareLag = Number(compareLag_) || 0; let formattedSubheader = subheader; @@ -103,7 +107,7 @@ export default function transformProps( const mainColor = `rgb(${r}, ${g}, ${b})`; const xAxisLabel = getXAxisLabel(rawFormData) as string; - let trendLineData; + let trendLineData: TimeSeriesDatum[] | undefined; let percentChange = 0; let bigNumber = data.length === 0 ? null : data[0][metricName]; let timestamp = data.length === 0 ? null : data[0][xAxisLabel]; @@ -144,6 +148,7 @@ export default function transformProps( } } sortedData.reverse(); + // @ts-ignore trendLineData = showTrendLine ? sortedData : undefined; } @@ -229,10 +234,10 @@ export default function transformProps( bottom: 0, }, tooltip: { + position: getDefaultPosition(refs), appendToBody: true, show: !inContextMenu, trigger: 'axis', - confine: true, formatter: renderTooltipFactory(formatTime, headerFormatter), }, aria: { @@ -250,6 +255,7 @@ export default function transformProps( width, height, bigNumber, + // @ts-ignore bigNumberFallback, className, headerFormatter, @@ -267,5 +273,6 @@ export default function transformProps( echartOptions, onContextMenu, xValueFormatter: formatTime, + refs, }; } diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/types.ts index 60c43770e3..90b852b01e 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/types.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/types.ts @@ -17,12 +17,17 @@ * under the License. */ +import { EChartsCoreOption } from 'echarts'; import { + BinaryQueryObjectFilterClause, ChartDataResponseResult, - ChartProps, + DataRecordValue, + NumberFormatter, QueryFormData, QueryFormMetric, + TimeFormatter, } from '@superset-ui/core'; +import { BaseChartProps, Refs } from '../types'; export interface BigNumberDatum { [key: string]: number | null; @@ -43,15 +48,50 @@ export type BigNumberWithTrendlineFormData = BigNumberTotalFormData & { compareLag?: string | number; }; -export type BigNumberTotalChartProps = ChartProps & { - formData: BigNumberTotalFormData; - queriesData: (ChartDataResponseResult & { - data?: BigNumberDatum[]; - })[]; -}; +export interface BigNumberTotalChartDataResponseResult + extends ChartDataResponseResult { + data: BigNumberDatum[]; +} -export type BigNumberWithTrendlineChartProps = BigNumberTotalChartProps & { - formData: BigNumberWithTrendlineFormData; -}; +export type BigNumberTotalChartProps = + BaseChartProps & { + formData: BigNumberTotalFormData; + queriesData: BigNumberTotalChartDataResponseResult[]; + }; + +export type BigNumberWithTrendlineChartProps = + BaseChartProps & { + formData: BigNumberWithTrendlineFormData; + }; export type TimeSeriesDatum = [number, number | null]; + +export type BigNumberVizProps = { + className?: string; + width: number; + height: number; + bigNumber?: DataRecordValue; + bigNumberFallback?: TimeSeriesDatum; + headerFormatter: NumberFormatter | TimeFormatter; + formatTime?: TimeFormatter; + headerFontSize: number; + kickerFontSize?: number; + subheader: string; + subheaderFontSize: number; + showTimestamp?: boolean; + showTrendLine?: boolean; + startYAxisAtZero?: boolean; + timeRangeFixed?: boolean; + timestamp?: DataRecordValue; + trendLineData?: TimeSeriesDatum[]; + mainColor?: string; + echartOptions?: EChartsCoreOption; + onContextMenu?: ( + clientX: number, + clientY: number, + filters?: BinaryQueryObjectFilterClause[], + ) => void; + xValueFormatter?: TimeFormatter; + formData?: BigNumberWithTrendlineFormData; + refs: Refs; +}; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/EchartsBoxPlot.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/EchartsBoxPlot.tsx index 29c4e2a6e6..135d31317e 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/EchartsBoxPlot.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/EchartsBoxPlot.tsx @@ -31,6 +31,7 @@ export default function EchartsBoxPlot(props: BoxPlotChartTransformedProps) { groupby, selectedValues, formData, + refs, } = props; const handleChange = useCallback( (values: string[]) => { @@ -72,6 +73,7 @@ export default function EchartsBoxPlot(props: BoxPlotChartTransformedProps) { return ( { + extends BaseChartProps { formData: BoxPlotQueryFormData; - queriesData: ChartDataResponseResult[]; } export type BoxPlotChartTransformedProps = - EChartTransformedProps; + BaseTransformedProps & + CrossFilterTransformedProps & + ContextMenuTransformedProps; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/EchartsFunnel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/EchartsFunnel.tsx index 52de923659..c492500e8e 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/EchartsFunnel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/EchartsFunnel.tsx @@ -31,6 +31,7 @@ export default function EchartsFunnel(props: FunnelChartTransformedProps) { groupby, selectedValues, formData, + refs, } = props; const handleChange = useCallback( (values: string[]) => { @@ -72,6 +73,7 @@ export default function EchartsFunnel(props: FunnelChartTransformedProps) { return ( @@ -212,7 +215,7 @@ export default function transformProps( ...defaultGrid, }, tooltip: { - ...defaultTooltip, + position: getDefaultPosition(refs), show: !inContextMenu, trigger: 'item', formatter: (params: any) => @@ -240,5 +243,6 @@ export default function transformProps( groupby, selectedValues, onContextMenu, + refs, }; } diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/types.ts index 0d1f3caf5e..841722ce4b 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/types.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/types.ts @@ -16,21 +16,20 @@ * specific language governing permissions and limitations * under the License. */ +import { QueryFormData } from '@superset-ui/core'; import { - ChartDataResponseResult, - ChartProps, - QueryFormData, -} from '@superset-ui/core'; -import { - EchartsLegendFormData, - EChartTransformedProps, + BaseChartProps, + BaseTransformedProps, + ContextMenuTransformedProps, + CrossFilterTransformedProps, + LegendFormData, LegendOrientation, LegendType, } from '../types'; import { DEFAULT_LEGEND_FORM_DATA } from '../constants'; export type EchartsFunnelFormData = QueryFormData & - EchartsLegendFormData & { + LegendFormData & { colorScheme?: string; groupby: QueryFormData[]; labelLine: boolean; @@ -54,9 +53,8 @@ export enum EchartsFunnelLabelTypeType { } export interface EchartsFunnelChartProps - extends ChartProps { + extends BaseChartProps { formData: EchartsFunnelFormData; - queriesData: ChartDataResponseResult[]; } // @ts-ignore @@ -76,4 +74,6 @@ export const DEFAULT_FORM_DATA: EchartsFunnelFormData = { }; export type FunnelChartTransformedProps = - EChartTransformedProps; + BaseTransformedProps & + CrossFilterTransformedProps & + ContextMenuTransformedProps; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/EchartsGauge.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/EchartsGauge.tsx index 118783b47e..7ffb571b79 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/EchartsGauge.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/EchartsGauge.tsx @@ -31,6 +31,7 @@ export default function EchartsGauge(props: GaugeChartTransformedProps) { groupby, selectedValues, formData: { emitFilter }, + refs, } = props; const handleChange = useCallback( (values: string[]) => { @@ -72,6 +73,7 @@ export default function EchartsGauge(props: GaugeChartTransformedProps) { return ( { const { name, value } = params; return `${name} : ${formatValue(value as number)}`; @@ -300,6 +304,7 @@ export default function transformProps( axisTick, pointer, detail, + // @ts-ignore tooltip, radius: Math.min(width, height) / 2 - axisLabelDistance - axisTickDistance, @@ -327,5 +332,6 @@ export default function transformProps( groupby, selectedValues: filterState.selectedValues || [], onContextMenu, + refs, }; } diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/types.ts index 4824d579c4..9f2c08fd5f 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/types.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/types.ts @@ -16,13 +16,13 @@ * specific language governing permissions and limitations * under the License. */ +import { QueryFormColumn, QueryFormData } from '@superset-ui/core'; import { - ChartDataResponseResult, - ChartProps, - QueryFormColumn, - QueryFormData, -} from '@superset-ui/core'; -import { EChartTransformedProps } from '../types'; + BaseChartProps, + BaseTransformedProps, + ContextMenuTransformedProps, + CrossFilterTransformedProps, +} from '../types'; import { DEFAULT_LEGEND_FORM_DATA } from '../constants'; export type AxisTickLineStyle = { @@ -80,10 +80,11 @@ export const DEFAULT_FORM_DATA: Partial = { }; export interface EchartsGaugeChartProps - extends ChartProps { + extends BaseChartProps { formData: EchartsGaugeFormData; - queriesData: ChartDataResponseResult[]; } export type GaugeChartTransformedProps = - EChartTransformedProps; + BaseTransformedProps & + ContextMenuTransformedProps & + CrossFilterTransformedProps; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Graph/EchartsGraph.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Graph/EchartsGraph.tsx index 0f09fe2386..4f83d1bcaf 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Graph/EchartsGraph.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Graph/EchartsGraph.tsx @@ -34,6 +34,7 @@ export default function EchartsGraph({ echartOptions, formData, onContextMenu, + refs, }: GraphChartTransformedProps) { const eventHandlers: EventHandlers = { contextmenu: (e: Event) => { @@ -68,6 +69,7 @@ export default function EchartsGraph({ }; return ( ; @@ -158,7 +160,7 @@ function getCategoryName(columnName: string, name?: DataRecordValue) { } export default function transformProps( - chartProps: ChartProps, + chartProps: EchartsGraphChartProps, ): GraphChartTransformedProps { const { width, height, formData, queriesData, hooks, inContextMenu } = chartProps; @@ -294,11 +296,13 @@ export default function transformProps( }, ]; + const refs: Refs = {}; const echartOptions: EChartsCoreOption = { animationDuration: DEFAULT_GRAPH_SERIES_OPTION.animationDuration, animationEasing: DEFAULT_GRAPH_SERIES_OPTION.animationEasing, tooltip: { show: !inContextMenu, + position: getDefaultPosition(refs), formatter: (params: any): string => edgeFormatter( params.data.source, @@ -322,5 +326,6 @@ export default function transformProps( formData, echartOptions, onContextMenu, + refs, }; } diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Graph/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Graph/types.ts index 70a068c977..95dc386eb2 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Graph/types.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Graph/types.ts @@ -16,16 +16,14 @@ * specific language governing permissions and limitations * under the License. */ -import { - PlainObject, - QueryFormData, - BinaryQueryObjectFilterClause, -} from '@superset-ui/core'; +import { QueryFormData } from '@superset-ui/core'; import { GraphNodeItemOption } from 'echarts/types/src/chart/graph/GraphSeries'; import { SeriesTooltipOption } from 'echarts/types/src/util/types'; import { - EchartsLegendFormData, - EchartsProps, + BaseChartProps, + BaseTransformedProps, + ContextMenuTransformedProps, + LegendFormData, LegendOrientation, LegendType, } from '../types'; @@ -34,7 +32,7 @@ import { DEFAULT_LEGEND_FORM_DATA } from '../constants'; export type EdgeSymbol = 'none' | 'circle' | 'arrow'; export type EchartsGraphFormData = QueryFormData & - EchartsLegendFormData & { + LegendFormData & { source: string; target: string; sourceCategory?: string; @@ -85,11 +83,10 @@ export type tooltipFormatParams = { data: { [name: string]: string }; }; -export type GraphChartTransformedProps = EchartsProps & { - formData: PlainObject; - onContextMenu?: ( - clientX: number, - clientY: number, - filters?: BinaryQueryObjectFilterClause[], - ) => void; -}; +export interface EchartsGraphChartProps + extends BaseChartProps { + formData: EchartsGraphFormData; +} + +export type GraphChartTransformedProps = + BaseTransformedProps & ContextMenuTransformedProps; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/EchartsMixedTimeseries.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/EchartsMixedTimeseries.tsx index 8a5421d217..1d1cd65477 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/EchartsMixedTimeseries.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/EchartsMixedTimeseries.tsx @@ -43,6 +43,7 @@ export default function EchartsMixedTimeseries({ onContextMenu, xValueFormatter, xAxis, + refs, }: EchartsMixedTimeseriesChartTransformedProps) { const isFirstQuery = useCallback( (seriesIndex: number) => seriesIndex < seriesBreakdown, @@ -61,7 +62,7 @@ export default function EchartsMixedTimeseries({ const currentGroupBy = isFirstQuery(seriesIndex) ? groupby : groupbyB; const currentLabelMap = isFirstQuery(seriesIndex) ? labelMap : labelMapB; const groupbyValues = values - .map(value => currentLabelMap[value]) + .map(value => currentLabelMap?.[value]) .filter(value => !!value); setDataMask({ @@ -100,7 +101,7 @@ export default function EchartsMixedTimeseries({ const eventHandlers: EventHandlers = { click: props => { const { seriesName, seriesIndex } = props; - const values: string[] = Object.values(selectedValues); + const values: string[] = Object.values(selectedValues || {}); if (values.includes(seriesName)) { handleChange( values.filter(v => v !== seriesName), @@ -162,6 +163,7 @@ export default function EchartsMixedTimeseries({ return ( { formData: EchartsMixedTimeseriesFormData; - queriesData: ChartDataResponseResult[]; } export type EchartsMixedTimeseriesChartTransformedProps = - EChartTransformedProps & { - emitFilterB: boolean; - groupbyB: QueryFormColumn[]; - labelMapB: Record; - seriesBreakdown: number; - xValueFormatter: TimeFormatter | StringConstructor; - xAxis: { - label: string; - type: AxisType; + BaseTransformedProps & + ContextMenuTransformedProps & + CrossFilterTransformedProps & { + emitFilterB: boolean; + groupbyB: QueryFormColumn[]; + labelMapB: Record; + seriesBreakdown: number; + xValueFormatter: TimeFormatter | StringConstructor; + xAxis: { + label: string; + type: AxisType; + }; }; - }; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Pie/EchartsPie.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Pie/EchartsPie.tsx index 37606ac6f8..6de4c8423d 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Pie/EchartsPie.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Pie/EchartsPie.tsx @@ -31,6 +31,7 @@ export default function EchartsPie(props: PieChartTransformedProps) { groupby, selectedValues, formData, + refs, } = props; const handleChange = useCallback( (values: string[]) => { @@ -72,6 +73,7 @@ export default function EchartsPie(props: PieChartTransformedProps) { return ( formatPieLabel({ @@ -341,5 +344,6 @@ export default function transformProps( groupby, selectedValues, onContextMenu, + refs, }; } diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Pie/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Pie/types.ts index e31127f7b3..4b45751a23 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Pie/types.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Pie/types.ts @@ -16,22 +16,20 @@ * specific language governing permissions and limitations * under the License. */ +import { QueryFormColumn, QueryFormData } from '@superset-ui/core'; import { - ChartDataResponseResult, - ChartProps, - QueryFormColumn, - QueryFormData, -} from '@superset-ui/core'; -import { - EchartsLegendFormData, - EChartTransformedProps, + BaseChartProps, + BaseTransformedProps, + ContextMenuTransformedProps, + CrossFilterTransformedProps, + LegendFormData, LegendOrientation, LegendType, } from '../types'; import { DEFAULT_LEGEND_FORM_DATA } from '../constants'; export type EchartsPieFormData = QueryFormData & - EchartsLegendFormData & { + LegendFormData & { colorScheme?: string; currentOwnValue?: string[] | null; donut: boolean; @@ -59,9 +57,9 @@ export enum EchartsPieLabelType { KeyValuePercent = 'key_value_percent', } -export interface EchartsPieChartProps extends ChartProps { +export interface EchartsPieChartProps + extends BaseChartProps { formData: EchartsPieFormData; - queriesData: ChartDataResponseResult[]; } // @ts-ignore @@ -84,4 +82,6 @@ export const DEFAULT_FORM_DATA: EchartsPieFormData = { }; export type PieChartTransformedProps = - EChartTransformedProps; + BaseTransformedProps & + ContextMenuTransformedProps & + CrossFilterTransformedProps; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Radar/EchartsRadar.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Radar/EchartsRadar.tsx index bcba60f5cb..1291b69c12 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Radar/EchartsRadar.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Radar/EchartsRadar.tsx @@ -31,6 +31,7 @@ export default function EchartsRadar(props: RadarChartTransformedProps) { groupby, selectedValues, formData, + refs, } = props; const handleChange = useCallback( (values: string[]) => { @@ -72,6 +73,7 @@ export default function EchartsRadar(props: RadarChartTransformedProps) { return ( ; export type EchartsRadarFormData = QueryFormData & - EchartsLegendFormData & { + LegendFormData & { colorScheme?: string; columnConfig?: RadarColumnConfig; currentOwnValue?: string[] | null; @@ -57,9 +58,9 @@ export enum EchartsRadarLabelType { KeyValue = 'key_value', } -export interface EchartsRadarChartProps extends ChartProps { +export interface EchartsRadarChartProps + extends BaseChartProps { formData: EchartsRadarFormData; - queriesData: ChartDataResponseResult[]; } // @ts-ignore @@ -78,4 +79,6 @@ export const DEFAULT_FORM_DATA: EchartsRadarFormData = { }; export type RadarChartTransformedProps = - EChartTransformedProps; + BaseTransformedProps & + ContextMenuTransformedProps & + CrossFilterTransformedProps; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/EchartsTimeseries.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/EchartsTimeseries.tsx index a178e5d05d..2ffdb0e87b 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/EchartsTimeseries.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/EchartsTimeseries.tsx @@ -48,9 +48,12 @@ export default function EchartsTimeseries({ onContextMenu, xValueFormatter, xAxis, + refs, }: TimeseriesChartTransformedProps) { const { emitFilter, stack } = formData; const echartRef = useRef(null); + // eslint-disable-next-line no-param-reassign + refs.echartRef = echartRef; const lastTimeRef = useRef(Date.now()); const lastSelectedLegend = useRef(''); const clickTimer = useRef>(); @@ -256,7 +259,7 @@ export default function EchartsTimeseries({
{ @@ -463,5 +465,6 @@ export default function transformProps( label: xAxisLabel, type: xAxisType, }, + refs, }; } diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/types.ts index f8545307ef..82a204e38d 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/types.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/types.ts @@ -16,22 +16,24 @@ * specific language governing permissions and limitations * under the License. */ +import { OptionName } from 'echarts/types/src/util/types'; import { AnnotationLayer, - ChartDataResponseResult, - ChartProps, + AxisType, + ContributionType, QueryFormColumn, QueryFormData, - TimeGranularity, - ContributionType, TimeFormatter, - AxisType, + TimeGranularity, } from '@superset-ui/core'; import { - EchartsLegendFormData, - EChartTransformedProps, - EchartsTitleFormData, + BaseChartProps, + BaseTransformedProps, + ContextMenuTransformedProps, + CrossFilterTransformedProps, + LegendFormData, StackType, + TitleFormData, } from '../types'; export enum OrientationType { @@ -85,20 +87,22 @@ export type EchartsTimeseriesFormData = QueryFormData & { showExtraControls: boolean; percentageThreshold: number; orientation?: OrientationType; -} & EchartsLegendFormData & - EchartsTitleFormData; +} & LegendFormData & + TitleFormData; export interface EchartsTimeseriesChartProps - extends ChartProps { + extends BaseChartProps { formData: EchartsTimeseriesFormData; - queriesData: ChartDataResponseResult[]; } export type TimeseriesChartTransformedProps = - EChartTransformedProps & { - xValueFormatter: TimeFormatter | StringConstructor; - xAxis: { - label: string; - type: AxisType; + BaseTransformedProps & + ContextMenuTransformedProps & + CrossFilterTransformedProps & { + legendData?: OptionName[]; + xValueFormatter: TimeFormatter | StringConstructor; + xAxis: { + label: string; + type: AxisType; + }; }; - }; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Tree/EchartsTree.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Tree/EchartsTree.tsx index 9b42c6e553..b1b3f8e896 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Tree/EchartsTree.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Tree/EchartsTree.tsx @@ -21,9 +21,17 @@ import { EchartsProps } from '../types'; import Echart from '../components/Echart'; export default function EchartsGraph({ - height, - width, echartOptions, + height, + refs, + width, }: EchartsProps) { - return ; + return ( + + ); } diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Tree/constants.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Tree/constants.ts index 463835966c..35567c3fc5 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Tree/constants.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Tree/constants.ts @@ -17,6 +17,7 @@ * under the License. */ import { TreeSeriesOption } from 'echarts'; +import { EchartsTreeFormData } from './types'; export const DEFAULT_TREE_SERIES_OPTION: TreeSeriesOption = { label: { @@ -28,3 +29,18 @@ export const DEFAULT_TREE_SERIES_OPTION: TreeSeriesOption = { animationEasing: 'cubicOut', lineStyle: { color: 'source', width: 1.5 }, }; + +export const DEFAULT_FORM_DATA: Partial = { + id: '', + parent: '', + name: '', + rootNodeId: '', + layout: 'orthogonal', + orient: 'LR', + symbol: 'emptyCircle', + symbolSize: 7, + roam: true, + nodeLabelPosition: 'left', + childLabelPosition: 'bottom', + emphasis: 'descendant', +}; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Tree/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Tree/controlPanel.tsx index 79e0639277..f8035203fe 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Tree/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Tree/controlPanel.tsx @@ -24,7 +24,7 @@ import { sections, sharedControls, } from '@superset-ui/chart-controls'; -import { DEFAULT_FORM_DATA } from './types'; +import { DEFAULT_FORM_DATA } from './constants'; const requiredEntity = { ...sharedControls.entity, diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Tree/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Tree/transformProps.ts index c89fbe8b37..2e9037be80 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Tree/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Tree/transformProps.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { ChartProps, getMetricLabel, DataRecordValue } from '@superset-ui/core'; +import { getMetricLabel, DataRecordValue } from '@superset-ui/core'; import { EChartsCoreOption, TreeSeriesOption } from 'echarts'; import { TreeSeriesCallbackDataParams, @@ -24,12 +24,13 @@ import { } from 'echarts/types/src/chart/tree/TreeSeries'; import { OptionName } from 'echarts/types/src/util/types'; import { + EchartsTreeChartProps, EchartsTreeFormData, - DEFAULT_FORM_DATA as DEFAULT_GRAPH_FORM_DATA, TreeDataRecord, + TreeTransformedProps, } from './types'; -import { DEFAULT_TREE_SERIES_OPTION } from './constants'; -import { EchartsProps } from '../types'; +import { DEFAULT_FORM_DATA, DEFAULT_TREE_SERIES_OPTION } from './constants'; +import { Refs } from '../types'; export function formatTooltip({ params, @@ -49,8 +50,11 @@ export function formatTooltip({ ].join(''); } -export default function transformProps(chartProps: ChartProps): EchartsProps { +export default function transformProps( + chartProps: EchartsTreeChartProps, +): TreeTransformedProps { const { width, height, formData, queriesData } = chartProps; + const refs: Refs = {}; const data: TreeDataRecord[] = queriesData[0].data || []; const { @@ -67,7 +71,7 @@ export default function transformProps(chartProps: ChartProps): EchartsProps { nodeLabelPosition, childLabelPosition, emphasis, - }: EchartsTreeFormData = { ...DEFAULT_GRAPH_FORM_DATA, ...formData }; + }: EchartsTreeFormData = { ...DEFAULT_FORM_DATA, ...formData }; const metricLabel = getMetricLabel(metric); const nameColumn = name || id; @@ -212,8 +216,10 @@ export default function transformProps(chartProps: ChartProps): EchartsProps { }; return { + formData, width, height, echartOptions, + refs, }; } diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Tree/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Tree/types.ts index 81db2d5950..0fde0cde2a 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Tree/types.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Tree/types.ts @@ -16,9 +16,12 @@ * specific language governing permissions and limitations * under the License. */ +import { OptionName } from 'echarts/types/src/util/types'; +import { ChartDataResponseResult, QueryFormData } from '@superset-ui/core'; import { TreeSeriesNodeItemOption } from 'echarts/types/src/chart/tree/TreeSeries'; +import { BaseChartProps, BaseTransformedProps } from '../types'; -export type EchartsTreeFormData = { +export type EchartsTreeFormData = QueryFormData & { id: string; parent: string; name: string; @@ -35,21 +38,18 @@ export type EchartsTreeFormData = { emphasis: 'none' | 'ancestor' | 'descendant'; }; -export const DEFAULT_FORM_DATA: EchartsTreeFormData = { - id: '', - parent: '', - name: '', - rootNodeId: '', - layout: 'orthogonal', - orient: 'LR', - symbol: 'emptyCircle', - symbolSize: 7, - roam: true, - nodeLabelPosition: 'left', - childLabelPosition: 'bottom', - emphasis: 'descendant', +export interface TreeChartDataResponseResult extends ChartDataResponseResult { + data: TreeDataRecord[]; +} + +export interface EchartsTreeChartProps + extends BaseChartProps { + formData: EchartsTreeFormData; + queriesData: TreeChartDataResponseResult[]; +} + +export type TreeDataRecord = Record & { + children?: TreeSeriesNodeItemOption[]; }; -export type TreeDataRecord = Record & { - children: TreeSeriesNodeItemOption[]; -}; +export type TreeTransformedProps = BaseTransformedProps; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/EchartsTreemap.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/EchartsTreemap.tsx index 1ff112cedd..8559688939 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/EchartsTreemap.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/EchartsTreemap.tsx @@ -28,15 +28,16 @@ import { extractTreePathInfo } from './constants'; import { TreemapTransformedProps } from './types'; export default function EchartsTreemap({ - height, - width, echartOptions, - setDataMask, - labelMap, - groupby, - selectedValues, formData, + groupby, + height, + labelMap, onContextMenu, + refs, + setDataMask, + selectedValues, + width, }: TreemapTransformedProps) { const handleChange = useCallback( (values: string[]) => { @@ -113,6 +114,7 @@ export default function EchartsTreemap({ return ( @@ -309,7 +310,7 @@ export default function transformProps( const echartOptions: EChartsCoreOption = { tooltip: { - ...defaultTooltip, + position: getDefaultPosition(refs), show: !inContextMenu, trigger: 'item', formatter: (params: any) => @@ -332,5 +333,6 @@ export default function transformProps( groupby, selectedValues: filterState.selectedValues || [], onContextMenu, + refs, }; } diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/types.ts index 9120fb72f7..c318b2ac2a 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/types.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/types.ts @@ -24,7 +24,12 @@ import { QueryFormMetric, } from '@superset-ui/core'; import { CallbackDataParams } from 'echarts/types/src/util/types'; -import { EChartTransformedProps, LabelPositionEnum } from '../types'; +import { + BaseTransformedProps, + ContextMenuTransformedProps, + CrossFilterTransformedProps, + LabelPositionEnum, +} from '../types'; export type EchartsTreemapFormData = QueryFormData & { colorScheme?: string; @@ -73,4 +78,6 @@ export interface TreemapSeriesCallbackDataParams extends CallbackDataParams { } export type TreemapTransformedProps = - EChartTransformedProps; + BaseTransformedProps & + ContextMenuTransformedProps & + CrossFilterTransformedProps; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/components/Echart.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/components/Echart.tsx index 0ef7704311..011d62abac 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/components/Echart.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/components/Echart.tsx @@ -42,10 +42,15 @@ function Echart( eventHandlers, zrEventHandlers, selectedValues = {}, + refs, }: EchartsProps, ref: React.Ref, ) { const divRef = useRef(null); + if (refs) { + // eslint-disable-next-line no-param-reassign + refs.divRef = divRef; + } const chartRef = useRef(); const currentSelection = useMemo( () => Object.keys(selectedValues) || [], @@ -106,6 +111,7 @@ function Echart( // did mount useEffect(() => { handleSizeChange({ width, height }); + return () => chartRef.current?.dispose(); }, []); useLayoutEffect(() => { diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/constants.ts b/superset-frontend/plugins/plugin-chart-echarts/src/constants.ts index ec956a9591..1c20128b67 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/constants.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/constants.ts @@ -20,8 +20,8 @@ import { JsonValue, t, TimeGranularity } from '@superset-ui/core'; import { ReactNode } from 'react'; import { - EchartsLegendFormData, - EchartsTitleFormData, + LegendFormData, + TitleFormData, LabelPositionEnum, LegendOrientation, LegendType, @@ -91,14 +91,14 @@ export const TIMEGRAIN_TO_TIMESTAMP = { [TimeGranularity.YEAR]: 3600 * 1000 * 24 * 31 * 12, }; -export const DEFAULT_LEGEND_FORM_DATA: EchartsLegendFormData = { +export const DEFAULT_LEGEND_FORM_DATA: LegendFormData = { legendMargin: null, legendOrientation: LegendOrientation.Top, legendType: LegendType.Scroll, showLegend: true, }; -export const DEFAULT_TITLE_FORM_DATA: EchartsTitleFormData = { +export const DEFAULT_TITLE_FORM_DATA: TitleFormData = { xAxisTitle: '', xAxisTitleMargin: 0, yAxisTitle: '', @@ -107,3 +107,10 @@ export const DEFAULT_TITLE_FORM_DATA: EchartsTitleFormData = { }; export { DEFAULT_FORM_DATA } from './Timeseries/constants'; + +// How far away from the mouse should the tooltip be +export const TOOLTIP_POINTER_MARGIN = 10; + +// If no satisfactory position can be found, how far away +// from the edge of the window should the tooltip be kept +export const TOOLTIP_OVERFLOW_MARGIN = 5; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/defaults.ts b/superset-frontend/plugins/plugin-chart-echarts/src/defaults.ts index c74e3d78a0..d76de5b53d 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/defaults.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/defaults.ts @@ -1,4 +1,6 @@ +import { CallbackDataParams } from 'echarts/types/src/util/types'; import { LegendOrientation } from './types'; +import { TOOLTIP_POINTER_MARGIN, TOOLTIP_OVERFLOW_MARGIN } from './constants'; /** * Licensed to the Apache Software Foundation (ASF) under one @@ -23,7 +25,59 @@ export const defaultGrid = { }; export const defaultTooltip = { - confine: true, + position: ( + canvasMousePos: [number, number], + params: CallbackDataParams, + tooltipDom: HTMLElement, + rect: any, + sizes: { contentSize: [number, number]; viewSize: [number, number] }, + ) => { + // algorithm copy-pasted from here: + // https://github.com/apache/echarts/issues/5004#issuecomment-559668309 + + // The chart canvas position + const canvasRect = tooltipDom.parentElement + ?.getElementsByTagName('canvas')[0] + .getBoundingClientRect(); + + // The mouse coordinates relative to the whole window + // The first parameter to the position function is the mouse position relative to the canvas + const mouseX = canvasMousePos[0] + (canvasRect?.x || 0); + const mouseY = canvasMousePos[1] + (canvasRect?.y || 0); + + // The width and height of the tooltip dom element + const tooltipWidth = sizes.contentSize[0]; + const tooltipHeight = sizes.contentSize[1]; + + // Start by placing the tooltip top and right relative to the mouse position + let xPos = mouseX + TOOLTIP_POINTER_MARGIN; + let yPos = mouseY - TOOLTIP_POINTER_MARGIN - tooltipHeight; + + // The tooltip is overflowing past the right edge of the window + if (xPos + tooltipWidth >= document.documentElement.clientWidth) { + // Attempt to place the tooltip to the left of the mouse position + xPos = mouseX - TOOLTIP_POINTER_MARGIN - tooltipWidth; + + // The tooltip is overflowing past the left edge of the window + if (xPos <= 0) + // Place the tooltip a fixed distance from the left edge of the window + xPos = TOOLTIP_OVERFLOW_MARGIN; + } + + // The tooltip is overflowing past the top edge of the window + if (yPos <= 0) { + // Attempt to place the tooltip to the bottom of the mouse position + yPos = mouseY + TOOLTIP_POINTER_MARGIN; + + // The tooltip is overflowing past the bottom edge of the window + if (yPos + tooltipHeight >= document.documentElement.clientHeight) + // Place the tooltip a fixed distance from the top edge of the window + yPos = TOOLTIP_OVERFLOW_MARGIN; + } + + // Return the position (converted back to a relative position on the canvas) + return [xPos - (canvasRect?.x || 0), yPos - (canvasRect?.y || 0)]; + }, }; export const defaultYAxis = { diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/types.ts index 8c20543e78..57ed645839 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/types.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/types.ts @@ -16,15 +16,17 @@ * specific language governing permissions and limitations * under the License. */ +import React, { RefObject } from 'react'; import { + BinaryQueryObjectFilterClause, + ChartDataResponseResult, + ChartProps, HandlerFunction, QueryFormColumn, - BinaryQueryObjectFilterClause, SetDataMaskHook, } from '@superset-ui/core'; import { EChartsCoreOption, ECharts } from 'echarts'; import { TooltipMarker } from 'echarts/types/src/util/format'; -import { OptionName } from 'echarts/types/src/util/types'; import { AreaChartExtraControlsValue } from './constants'; export type EchartsStylesProps = { @@ -32,6 +34,11 @@ export type EchartsStylesProps = { width: number; }; +export type Refs = { + echartRef?: React.Ref; + divRef?: RefObject; +}; + export interface EchartsProps { height: number; width: number; @@ -40,6 +47,7 @@ export interface EchartsProps { zrEventHandlers?: EventHandlers; selectedValues?: Record; forceClear?: boolean; + refs: Refs; } export interface EchartsHandler { @@ -78,7 +86,7 @@ export type ForecastValue = { forecastUpper?: number; }; -export type EchartsLegendFormData = { +export type LegendFormData = { legendMargin: number | null | string; legendOrientation: LegendOrientation; legendType: LegendType; @@ -103,26 +111,41 @@ export enum LabelPositionEnum { InsideBottomRight = 'insideBottomRight', } -export interface EChartTransformedProps { +export interface BaseChartProps extends ChartProps { + queriesData: ChartDataResponseResult[]; +} + +export interface BaseTransformedProps { + echartOptions: EChartsCoreOption; formData: F; height: number; - width: number; - echartOptions: EChartsCoreOption; - emitFilter: boolean; - setDataMask: SetDataMaskHook; - setControlValue?: HandlerFunction; - labelMap: Record; - groupby: QueryFormColumn[]; - selectedValues: Record; - legendData?: OptionName[]; onContextMenu?: ( clientX: number, clientY: number, filters?: BinaryQueryObjectFilterClause[], ) => void; + refs: Refs; + width: number; } -export interface EchartsTitleFormData { +export type CrossFilterTransformedProps = { + emitFilter: boolean; + groupby: QueryFormColumn[]; + labelMap: Record; + setControlValue?: HandlerFunction; + setDataMask: SetDataMaskHook; + selectedValues: Record; +}; + +export type ContextMenuTransformedProps = { + onContextMenu?: ( + clientX: number, + clientY: number, + filters?: BinaryQueryObjectFilterClause[], + ) => void; +}; + +export interface TitleFormData { xAxisTitle: string; xAxisTitleMargin: number; yAxisTitle: string; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/utils/eventHandlers.ts b/superset-frontend/plugins/plugin-chart-echarts/src/utils/eventHandlers.ts index d7c552edfc..0d26b92d08 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/utils/eventHandlers.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/utils/eventHandlers.ts @@ -17,7 +17,11 @@ * under the License. */ import { BinaryQueryObjectFilterClause } from '@superset-ui/core'; -import { EChartTransformedProps, EventHandlers } from '../types'; +import { + BaseTransformedProps, + CrossFilterTransformedProps, + EventHandlers, +} from '../types'; export type Event = { name: string; @@ -40,8 +44,9 @@ export const clickEventHandler = export const contextMenuEventHandler = ( - groupby: EChartTransformedProps['groupby'], - onContextMenu: EChartTransformedProps['onContextMenu'], + groupby: (BaseTransformedProps & + CrossFilterTransformedProps)['groupby'], + onContextMenu: BaseTransformedProps['onContextMenu'], labelMap: Record, ) => (e: Event) => { @@ -65,7 +70,7 @@ export const contextMenuEventHandler = }; export const allEventHandlers = ( - transformedProps: EChartTransformedProps, + transformedProps: BaseTransformedProps & CrossFilterTransformedProps, handleChange: (values: string[]) => void, ) => { const { groupby, selectedValues, onContextMenu, labelMap } = transformedProps; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/utils/tooltip.ts b/superset-frontend/plugins/plugin-chart-echarts/src/utils/tooltip.ts new file mode 100644 index 0000000000..d2f6890430 --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-echarts/src/utils/tooltip.ts @@ -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 { CallbackDataParams } from 'echarts/types/src/util/types'; +import { TOOLTIP_OVERFLOW_MARGIN, TOOLTIP_POINTER_MARGIN } from '../constants'; +import { Refs } from '../types'; + +export function getDefaultPosition(refs: Refs) { + return ( + canvasMousePos: [number, number], + params: CallbackDataParams, + tooltipDom: HTMLDivElement | null, + rect: any, + sizes: { contentSize: [number, number]; viewSize: [number, number] }, + ) => { + // algorithm partially based on this snippet: + // https://github.com/apache/echarts/issues/5004#issuecomment-559668309 + + // The chart canvas position + const divRect = refs.divRef?.current?.getBoundingClientRect(); + + // The mouse coordinates relative to the whole window + // The first parameter to the position function is the mouse position relative to the canvas + const mouseX = canvasMousePos[0] + (divRect?.x || 0); + const mouseY = canvasMousePos[1] + (divRect?.y || 0); + + // The width and height of the tooltip dom element + const tooltipWidth = sizes.contentSize[0]; + const tooltipHeight = sizes.contentSize[1]; + + // Start by placing the tooltip top and right relative to the mouse position + let xPos = mouseX + TOOLTIP_POINTER_MARGIN; + let yPos = mouseY - TOOLTIP_POINTER_MARGIN - tooltipHeight; + + // The tooltip is overflowing past the right edge of the window + if (xPos + tooltipWidth >= document.documentElement.clientWidth) { + // Attempt to place the tooltip to the left of the mouse position + xPos = mouseX - TOOLTIP_POINTER_MARGIN - tooltipWidth; + + // The tooltip is overflowing past the left edge of the window + if (xPos <= 0) + // Place the tooltip a fixed distance from the left edge of the window + xPos = TOOLTIP_OVERFLOW_MARGIN; + } + + // The tooltip is overflowing past the top edge of the window + if (yPos <= 0) { + // Attempt to place the tooltip to the bottom of the mouse position + yPos = mouseY + TOOLTIP_POINTER_MARGIN; + + // The tooltip is overflowing past the bottom edge of the window + if (yPos + tooltipHeight >= document.documentElement.clientHeight) + // Place the tooltip a fixed distance from the top edge of the window + yPos = TOOLTIP_OVERFLOW_MARGIN; + } + + // Return the position (converted back to a relative position on the canvas) + return [xPos - (divRect?.x || 0), yPos - (divRect?.y || 0)]; + }; +} diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/BigNumber/transformProps.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/BigNumber/transformProps.test.ts index f138765987..ce00bb7190 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/test/BigNumber/transformProps.test.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/test/BigNumber/transformProps.test.ts @@ -25,6 +25,7 @@ import transformProps from '../../src/BigNumber/BigNumberWithTrendline/transform import { BigNumberDatum, BigNumberWithTrendlineChartProps, + BigNumberWithTrendlineFormData, } from '../../src/BigNumber/types'; const formData = { @@ -44,7 +45,8 @@ const formData = { datasource: 'test_datasource', }; -const rawFormData = { +const rawFormData: BigNumberWithTrendlineFormData = { + colorPicker: { b: 0, g: 0, r: 0 }, datasource: '1__table', metric: 'value', color_picker: { @@ -129,7 +131,8 @@ describe('BigNumberWithTrendline', () => { expect(transformed.bigNumber).toStrictEqual(1.2345); expect(transformed.bigNumberFallback).not.toBeNull(); - // should successfully formatTime by ganularity + // should successfully formatTime by granularity + // @ts-ignore expect(transformed.formatTime(new Date('2020-01-01'))).toStrictEqual( '2020-01-01 00:00:00', ); @@ -150,6 +153,7 @@ describe('BigNumberWithTrendline', () => { }, }; const transformed = transformProps(propsWithDatasource); + // @ts-ignore expect(transformed.headerFormatter(transformed.bigNumber)).toStrictEqual( '1.23', ); diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/Graph/transformProps.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/Graph/transformProps.test.ts index 480f4828f3..2401206c28 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/test/Graph/transformProps.test.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/test/Graph/transformProps.test.ts @@ -16,13 +16,14 @@ * specific language governing permissions and limitations * under the License. */ -import { ChartProps, supersetTheme } from '@superset-ui/core'; +import { ChartProps, SqlaFormData, supersetTheme } from '@superset-ui/core'; import transformProps from '../../src/Graph/transformProps'; import { DEFAULT_GRAPH_SERIES_OPTION } from '../../src/Graph/constants'; +import { EchartsGraphChartProps } from '../../src/Graph/types'; describe('EchartsGraph transformProps', () => { it('should transform chart props for viz without category', () => { - const formData = { + const formData: SqlaFormData = { colorScheme: 'bnbColors', datasource: '3__table', granularity_sqla: 'ds', @@ -30,6 +31,7 @@ describe('EchartsGraph transformProps', () => { source: 'source_column', target: 'target_column', category: null, + viz_type: 'graph', }; const queriesData = [ { @@ -57,7 +59,7 @@ describe('EchartsGraph transformProps', () => { }; const chartProps = new ChartProps(chartPropsConfig); - expect(transformProps(chartProps)).toEqual( + expect(transformProps(chartProps as EchartsGraphChartProps)).toEqual( expect.objectContaining({ width: 800, height: 600, @@ -151,7 +153,7 @@ describe('EchartsGraph transformProps', () => { }); it('should transform chart props for viz with category and falsey normalization', () => { - const formData = { + const formData: SqlaFormData = { colorScheme: 'bnbColors', datasource: '3__table', granularity_sqla: 'ds', @@ -160,6 +162,7 @@ describe('EchartsGraph transformProps', () => { target: 'target_column', sourceCategory: 'source_category_column', targetCategory: 'target_category_column', + viz_type: 'graph', }; const queriesData = [ { @@ -197,7 +200,7 @@ describe('EchartsGraph transformProps', () => { }; const chartProps = new ChartProps(chartPropsConfig); - expect(transformProps(chartProps)).toEqual( + expect(transformProps(chartProps as EchartsGraphChartProps)).toEqual( expect.objectContaining({ width: 800, height: 600, diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/Tree/transformProps.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/Tree/transformProps.test.ts index ad06455cb1..6a56c997c2 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/test/Tree/transformProps.test.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/test/Tree/transformProps.test.ts @@ -18,6 +18,7 @@ */ import { ChartProps, supersetTheme } from '@superset-ui/core'; import transformProps from '../../src/Tree/transformProps'; +import { EchartsTreeChartProps } from '../../src/Tree/types'; describe('EchartsTree transformProps', () => { const formData = { @@ -70,7 +71,7 @@ describe('EchartsTree transformProps', () => { ]; const chartProps = new ChartProps({ ...chartPropsConfig, queriesData }); - expect(transformProps(chartProps)).toEqual( + expect(transformProps(chartProps as EchartsTreeChartProps)).toEqual( expect.objectContaining({ width: 800, height: 600, @@ -137,7 +138,7 @@ describe('EchartsTree transformProps', () => { ]; const chartProps = new ChartProps({ ...chartPropsConfig, queriesData }); - expect(transformProps(chartProps)).toEqual( + expect(transformProps(chartProps as EchartsTreeChartProps)).toEqual( expect.objectContaining({ width: 800, height: 600, @@ -223,7 +224,7 @@ describe('EchartsTree transformProps', () => { ]; const chartProps = new ChartProps({ ...chartPropsConfig, queriesData }); - expect(transformProps(chartProps)).toEqual( + expect(transformProps(chartProps as EchartsTreeChartProps)).toEqual( expect.objectContaining({ width: 800, height: 600, @@ -299,7 +300,7 @@ describe('EchartsTree transformProps', () => { ]; const chartProps = new ChartProps({ ...chartPropsConfig, queriesData }); - expect(transformProps(chartProps)).toEqual( + expect(transformProps(chartProps as EchartsTreeChartProps)).toEqual( expect.objectContaining({ width: 800, height: 600, @@ -385,7 +386,7 @@ describe('EchartsTree transformProps', () => { ]; const chartProps = new ChartProps({ ...chartPropsConfig, queriesData }); - expect(transformProps(chartProps)).toEqual( + expect(transformProps(chartProps as EchartsTreeChartProps)).toEqual( expect.objectContaining({ width: 800, height: 600,