mirror of
https://github.com/apache/superset.git
synced 2026-05-12 11:25:56 +00:00
fix(echarts): prevent plain legend clipping in dashboards (#38675)
(cherry picked from commit 12aca72074)
This commit is contained in:
committed by
Michael S. Molina
parent
9619fa2156
commit
1e7d781354
@@ -16,12 +16,62 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { ChartProps, SqlaFormData } from '@superset-ui/core';
|
||||
import {
|
||||
ChartDataResponseResult,
|
||||
ChartProps,
|
||||
DataRecord,
|
||||
SqlaFormData,
|
||||
} from '@superset-ui/core';
|
||||
import { GenericDataType } from '@apache-superset/core/common';
|
||||
import { supersetTheme } from '@apache-superset/core/theme';
|
||||
import { EchartsTimeseriesChartProps } from '../../../src/types';
|
||||
import type {
|
||||
GridComponentOption,
|
||||
LegendComponentOption,
|
||||
} from 'echarts/components';
|
||||
import {
|
||||
EchartsTimeseriesChartProps,
|
||||
LegendOrientation,
|
||||
LegendType,
|
||||
} from '../../../src/types';
|
||||
import transformProps from '../../../src/Timeseries/transformProps';
|
||||
import { DEFAULT_FORM_DATA } from '../../../src/Timeseries/constants';
|
||||
import { EchartsTimeseriesSeriesType } from '../../../src/Timeseries/types';
|
||||
import {
|
||||
EchartsTimeseriesFormData,
|
||||
OrientationType,
|
||||
EchartsTimeseriesSeriesType,
|
||||
} from '../../../src/Timeseries/types';
|
||||
import { getPadding } from '../../../src/Timeseries/transformers';
|
||||
import {
|
||||
getHorizontalLegendAvailableWidth,
|
||||
getLegendLayoutResult,
|
||||
} from '../../../src/utils/series';
|
||||
import { createEchartsTimeseriesTestChartProps } from '../../helpers';
|
||||
|
||||
function createTestQueryData(
|
||||
data: DataRecord[],
|
||||
overrides?: Partial<ChartDataResponseResult>,
|
||||
): ChartDataResponseResult {
|
||||
return {
|
||||
annotation_data: null,
|
||||
cache_key: null,
|
||||
cache_timeout: null,
|
||||
cached_dttm: null,
|
||||
queried_dttm: null,
|
||||
data,
|
||||
colnames: [],
|
||||
coltypes: [],
|
||||
error: null,
|
||||
is_cached: false,
|
||||
query: '',
|
||||
rowcount: data.length,
|
||||
sql_rowcount: data.length,
|
||||
stacktrace: null,
|
||||
status: 'success',
|
||||
from_dttm: null,
|
||||
to_dttm: null,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('Bar Chart X-axis Time Formatting', () => {
|
||||
const baseFormData: SqlaFormData = {
|
||||
@@ -570,6 +620,48 @@ describe('Bar Chart X-axis Time Formatting', () => {
|
||||
expect(legendData).toContain('C');
|
||||
});
|
||||
|
||||
test('should preserve source order for color-by-primary-axis legends when label sorting is enabled', () => {
|
||||
const unsortedCategoricalData = [
|
||||
{
|
||||
data: [
|
||||
{ category: 'Zulu', value: 100 },
|
||||
{ category: 'Alpha', value: 150 },
|
||||
{ category: 'Mike', value: 200 },
|
||||
],
|
||||
colnames: ['category', 'value'],
|
||||
coltypes: ['STRING', 'BIGINT'],
|
||||
},
|
||||
];
|
||||
|
||||
const formData = {
|
||||
...baseFormData,
|
||||
colorByPrimaryAxis: true,
|
||||
groupby: [],
|
||||
legendSort: 'asc',
|
||||
x_axis: 'category',
|
||||
metric: 'value',
|
||||
};
|
||||
|
||||
const chartProps = new ChartProps({
|
||||
...baseChartPropsConfig,
|
||||
queriesData: unsortedCategoricalData,
|
||||
formData,
|
||||
});
|
||||
|
||||
const transformedProps = transformProps(
|
||||
chartProps as unknown as EchartsTimeseriesChartProps,
|
||||
);
|
||||
|
||||
const legend = transformedProps.echartOptions.legend as {
|
||||
data: { name: string }[];
|
||||
};
|
||||
expect(legend.data.map(item => item.name)).toEqual([
|
||||
'Zulu',
|
||||
'Alpha',
|
||||
'Mike',
|
||||
]);
|
||||
});
|
||||
|
||||
test('should deduplicate legend entries when x-axis has repeated values', () => {
|
||||
const repeatedData = [
|
||||
{
|
||||
@@ -634,4 +726,180 @@ describe('Bar Chart X-axis Time Formatting', () => {
|
||||
expect(hiddenSeries.length).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Legend layout regressions', () => {
|
||||
const getBottomLegendLayout = (
|
||||
chartWidth: number,
|
||||
legendItems: string[],
|
||||
legendMargin?: string | number | null,
|
||||
) =>
|
||||
getLegendLayoutResult({
|
||||
availableWidth: getHorizontalLegendAvailableWidth({
|
||||
chartWidth,
|
||||
orientation: LegendOrientation.Bottom,
|
||||
padding: getPadding(
|
||||
true,
|
||||
LegendOrientation.Bottom,
|
||||
false,
|
||||
false,
|
||||
legendMargin,
|
||||
false,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
true,
|
||||
),
|
||||
}),
|
||||
chartHeight: baseChartPropsConfig.height,
|
||||
chartWidth,
|
||||
legendItems,
|
||||
legendMargin,
|
||||
orientation: LegendOrientation.Bottom,
|
||||
show: true,
|
||||
theme: supersetTheme,
|
||||
type: LegendType.Plain,
|
||||
});
|
||||
|
||||
test('should fall back to scroll for horizontal bottom legends after margin expansion reduces available width', () => {
|
||||
const legendLabels = [
|
||||
'This is a long sales legend',
|
||||
'This is a long marketing legend',
|
||||
'This is a long operations legend',
|
||||
];
|
||||
const longLegendData: ChartDataResponseResult[] = [
|
||||
createTestQueryData(
|
||||
[
|
||||
{
|
||||
[legendLabels[0]]: 100,
|
||||
[legendLabels[1]]: null,
|
||||
[legendLabels[2]]: null,
|
||||
__timestamp: 1609459200000,
|
||||
},
|
||||
{
|
||||
[legendLabels[0]]: null,
|
||||
[legendLabels[1]]: 150,
|
||||
[legendLabels[2]]: null,
|
||||
__timestamp: 1612137600000,
|
||||
},
|
||||
{
|
||||
[legendLabels[0]]: null,
|
||||
[legendLabels[1]]: null,
|
||||
[legendLabels[2]]: 200,
|
||||
__timestamp: 1614556800000,
|
||||
},
|
||||
],
|
||||
{
|
||||
colnames: [...legendLabels, '__timestamp'],
|
||||
coltypes: [
|
||||
GenericDataType.Numeric,
|
||||
GenericDataType.Numeric,
|
||||
GenericDataType.Numeric,
|
||||
GenericDataType.Temporal,
|
||||
],
|
||||
},
|
||||
),
|
||||
];
|
||||
const regressionFormData: EchartsTimeseriesFormData = {
|
||||
...(baseFormData as EchartsTimeseriesFormData),
|
||||
metric: legendLabels,
|
||||
orientation: OrientationType.Horizontal,
|
||||
legendOrientation: LegendOrientation.Bottom,
|
||||
legendType: LegendType.Plain,
|
||||
showLegend: true,
|
||||
};
|
||||
const baselineChartProps = createEchartsTimeseriesTestChartProps<
|
||||
EchartsTimeseriesFormData,
|
||||
EchartsTimeseriesChartProps
|
||||
>({
|
||||
defaultFormData: regressionFormData,
|
||||
defaultVizType: 'echarts_timeseries_bar',
|
||||
defaultQueriesData: longLegendData,
|
||||
width: baseChartPropsConfig.width,
|
||||
height: baseChartPropsConfig.height,
|
||||
});
|
||||
const baselineTransformed = transformProps(baselineChartProps);
|
||||
const legendItems = (
|
||||
(baselineTransformed.echartOptions.legend as LegendComponentOption)
|
||||
.data as Array<string | { name: string }>
|
||||
).map(item => (typeof item === 'string' ? item : item.name));
|
||||
let chartWidth: number | undefined;
|
||||
let expandedLegendMargin: number | null = null;
|
||||
|
||||
for (let width = 300; width <= 700; width += 1) {
|
||||
const initialLayout = getBottomLegendLayout(width, legendItems, null);
|
||||
|
||||
if (initialLayout.effectiveType !== LegendType.Plain) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const refinedLayout = getBottomLegendLayout(
|
||||
width,
|
||||
legendItems,
|
||||
initialLayout.effectiveMargin ?? null,
|
||||
);
|
||||
|
||||
if (refinedLayout.effectiveType === LegendType.Scroll) {
|
||||
chartWidth = width;
|
||||
expandedLegendMargin = initialLayout.effectiveMargin ?? null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
expect(chartWidth).toBeDefined();
|
||||
expect(expandedLegendMargin).not.toBeNull();
|
||||
const resolvedChartWidth = chartWidth ?? baseChartPropsConfig.width;
|
||||
|
||||
const chartProps = createEchartsTimeseriesTestChartProps<
|
||||
EchartsTimeseriesFormData,
|
||||
EchartsTimeseriesChartProps
|
||||
>({
|
||||
defaultFormData: regressionFormData,
|
||||
defaultVizType: 'echarts_timeseries_bar',
|
||||
defaultQueriesData: longLegendData,
|
||||
width: resolvedChartWidth,
|
||||
height: baseChartPropsConfig.height,
|
||||
});
|
||||
|
||||
const transformedProps = transformProps(chartProps);
|
||||
const legend = transformedProps.echartOptions
|
||||
.legend as LegendComponentOption;
|
||||
const grid = transformedProps.echartOptions.grid as GridComponentOption;
|
||||
const expectedPadding = getPadding(
|
||||
true,
|
||||
LegendOrientation.Bottom,
|
||||
false,
|
||||
false,
|
||||
null,
|
||||
false,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
true,
|
||||
);
|
||||
[expectedPadding.bottom, expectedPadding.left] = [
|
||||
expectedPadding.left,
|
||||
expectedPadding.bottom,
|
||||
];
|
||||
const expandedPadding = getPadding(
|
||||
true,
|
||||
LegendOrientation.Bottom,
|
||||
false,
|
||||
false,
|
||||
expandedLegendMargin,
|
||||
false,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
true,
|
||||
);
|
||||
[expandedPadding.bottom, expandedPadding.left] = [
|
||||
expandedPadding.left,
|
||||
expandedPadding.bottom,
|
||||
];
|
||||
|
||||
expect(legend.type).toBe(LegendType.Scroll);
|
||||
expect(grid.bottom).toBe(expectedPadding.bottom);
|
||||
expect(grid.bottom).not.toBe(expandedPadding.bottom);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user