Files
superset2/superset-frontend/plugins/plugin-chart-echarts/test/Bullet/transformProps.test.ts
2026-07-22 17:50:44 -07:00

187 lines
6.3 KiB
TypeScript

/**
* 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 { ChartProps, SqlaFormData, VizType } from '@superset-ui/core';
import { supersetTheme } from '@apache-superset/core/theme';
import transformProps from '../../src/Bullet/transformProps';
import { EchartsBulletChartProps } from '../../src/Bullet/types';
const formData: SqlaFormData = {
datasource: '5__table',
viz_type: VizType.Bullet,
metric: 'sum__num',
ranges: '100,200,300',
rangeLabels: 'low,mid,high',
markers: '150',
markerLabels: 'goal',
markerLines: '250',
markerLineLabels: 'stretch',
};
const chartProps = (overrides: Partial<SqlaFormData> = {}) =>
new ChartProps({
width: 800,
height: 200,
formData: { ...formData, ...overrides },
theme: supersetTheme,
queriesData: [{ data: [{ sum__num: 120 }] }],
hooks: {},
}) as unknown as EchartsBulletChartProps;
test('renders the measure bar with markers, marker lines and range bands', () => {
const { echartOptions } = transformProps(chartProps());
const { series } = echartOptions as any;
// measure bar
expect(series[0].type).toBe('bar');
expect(series[0].data).toEqual([120]);
// marker lines are their own named series so the legend can toggle them
const lineSeries = series.filter((x: any) => x.type === 'line');
expect(lineSeries).toHaveLength(1);
expect(lineSeries[0].markLine.data[0].xAxis).toBe(250);
// nested range bands, largest drawn first
const bandEnds = series[0].markArea.data.map((d: any) => d[1].xAxis);
expect(bandEnds).toEqual([300, 200, 100]);
// triangle markers, one named series each
const markerSeries = series.filter((x: any) => x.symbol === 'triangle');
expect(markerSeries).toHaveLength(1);
expect(markerSeries[0].data[0].value).toEqual([150, 0]);
// axis spans all values
expect((echartOptions as any).xAxis.max).toBe(300);
// markers sit below the bar (pixel offset); legend off unless enabled
const marker = series.find((x: any) => x.symbol === 'triangle');
expect(marker.symbolOffset[1]).toBeGreaterThan(0);
expect((echartOptions as any).legend.show).toBe(false);
// range thresholds get their own named hover-target series
const rangeSeries = series.filter((x: any) => x.symbol === 'rect');
const rangeTips = rangeSeries.map((x: any) => x.data[0].tooltip.formatter());
expect(rangeTips.join(' ')).toContain('low');
expect(rangeTips.join(' ')).toContain('high');
});
test('defaults the band to 110% of the measure when no ranges are set', () => {
const { echartOptions } = transformProps(
chartProps({
ranges: '',
rangeLabels: '',
markers: '',
markerLabels: '',
markerLines: '',
markerLineLabels: '',
}),
);
const { series } = echartOptions as any;
expect(series[0].data).toEqual([120]);
const bandEnds = series[0].markArea.data.map((d: any) => d[1].xAxis);
expect(bandEnds).toEqual([120 * 1.1]);
expect(series.filter((x: any) => x.symbol === 'triangle')).toHaveLength(0);
});
test('keeps distinct labels for duplicate marker and marker-line values', () => {
const { echartOptions } = transformProps(
chartProps({
markers: '150,150',
markerLabels: 'goal,forecast',
markerLines: '250,250',
markerLineLabels: 'stretch,ceiling',
}),
);
const { series } = echartOptions as any;
const markLineLabels = series
.filter((x: any) => x.type === 'line')
.map((x: any) => x.markLine.data[0].label.formatter());
expect(markLineLabels).toEqual(['stretch', 'ceiling']);
const markerTooltips = series
.filter((x: any) => x.symbol === 'triangle')
.map((x: any) => x.data[0].tooltip.formatter());
expect(markerTooltips[0]).toContain('goal');
expect(markerTooltips[1]).toContain('forecast');
});
test('sanitizes HTML in metric and marker labels before rendering tooltips', () => {
const { echartOptions } = transformProps(
chartProps({
markers: '150',
markerLabels: '<img src=x onerror=alert(1)>',
}),
);
const { series } = echartOptions as any;
expect(series[0].tooltip.formatter()).not.toContain('onerror');
expect(series[2].data[0].tooltip.formatter()).not.toContain('onerror');
});
test('handles an empty query result without crashing', () => {
const props = new ChartProps({
width: 800,
height: 200,
formData,
theme: supersetTheme,
queriesData: [{ data: [] }],
hooks: {},
}) as unknown as EchartsBulletChartProps;
const { echartOptions } = transformProps(props);
expect((echartOptions as any).series[0].data).toEqual([0]);
// the axis domain must not collapse to zero width
const { min, max } = (echartOptions as any).xAxis;
expect(max).toBeGreaterThan(min);
});
test('splits into one bullet row per group value with shared markers', () => {
const props = new ChartProps({
width: 800,
height: 400,
formData: { ...formData, groupby: ['state'] },
theme: supersetTheme,
queriesData: [
{
data: [
{ state: 'CA', sum__num: 120 },
{ state: 'NY', sum__num: 90 },
],
},
],
hooks: {},
}) as unknown as EchartsBulletChartProps;
const { echartOptions } = transformProps(props);
const { series, yAxis } = echartOptions as any;
// one category and one bar value per group
expect(yAxis.data).toEqual(['CA', 'NY']);
expect(yAxis.axisLabel.show).toBe(true);
expect(series[0].data).toEqual([120, 90]);
// the same marker repeats on every row
const marker = series.find((x: any) => x.symbol === 'triangle');
expect(marker.data.map((d: any) => d.value)).toEqual([
[150, 0],
[150, 1],
]);
// grouped bar tooltips name the row
expect(series[0].tooltip.formatter({ dataIndex: 1 })).toContain('NY');
});