mirror of
https://github.com/apache/superset.git
synced 2026-09-02 13:31:33 +00:00
196 lines
6.7 KiB
TypeScript
196 lines
6.7 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/Rose/transformProps';
|
|
import { EchartsRoseChartProps } from '../../src/Rose/types';
|
|
|
|
const T1 = 1609459200000; // 2021-01-01
|
|
const T2 = 1612137600000; // 2021-02-01
|
|
|
|
const formData: SqlaFormData = {
|
|
datasource: '1__table',
|
|
viz_type: VizType.Rose,
|
|
metrics: ['sum__num'],
|
|
groupby: ['region'],
|
|
numberFormat: 'SMART_NUMBER',
|
|
dateTimeFormat: 'smart_date',
|
|
richTooltip: true,
|
|
roseAreaProportion: false,
|
|
};
|
|
|
|
const records = [
|
|
{ __timestamp: T1, 'sum__num, East': 30, 'sum__num, West': 10 },
|
|
{ __timestamp: T2, 'sum__num, East': 10, 'sum__num, West': 10 },
|
|
];
|
|
|
|
const chartProps = (overrides: Partial<SqlaFormData> = {}) =>
|
|
new ChartProps({
|
|
width: 800,
|
|
height: 600,
|
|
formData: { ...formData, ...overrides },
|
|
theme: supersetTheme,
|
|
queriesData: [{ data: records }],
|
|
hooks: {},
|
|
}) as unknown as EchartsRoseChartProps;
|
|
|
|
test('builds one stacked polar bar series per group', () => {
|
|
const { echartOptions, periods, seriesNames } = transformProps(chartProps());
|
|
const { series, angleAxis } = echartOptions as any;
|
|
|
|
// series keys drop the metric for grouped single-metric data
|
|
expect(seriesNames).toEqual(['East', 'West']);
|
|
expect(series).toHaveLength(2);
|
|
series.forEach((s: any) => {
|
|
expect(s.type).toBe('bar');
|
|
expect(s.coordinateSystem).toBe('polar');
|
|
expect(s.stack).toBe('rose');
|
|
expect(s.universalTransition).toEqual({ enabled: true });
|
|
});
|
|
|
|
// one angular sector per time period, in chronological order
|
|
expect(periods.map((p: any) => p.time)).toEqual([T1, T2]);
|
|
expect(angleAxis.data).toHaveLength(2);
|
|
|
|
// radius mode stacks the raw values
|
|
expect(series[0].data.map((d: any) => d.value)).toEqual([30, 10]);
|
|
expect(series[1].data.map((d: any) => d.value)).toEqual([10, 10]);
|
|
});
|
|
|
|
test('area proportion plots sqrt-normalized increments on a unit axis', () => {
|
|
const { echartOptions, periods } = transformProps(
|
|
chartProps({ roseAreaProportion: true }),
|
|
);
|
|
const { series, radiusAxis } = echartOptions as any;
|
|
|
|
expect(radiusAxis.max).toBe(1);
|
|
// period 1: outer radii sqrt(30/40), sqrt(40/40); increments are their
|
|
// differences so the stacked outer edge encodes cumulative area
|
|
const sqrt30 = Math.sqrt(30 / 40);
|
|
expect(series[0].data[0].value).toBeCloseTo(sqrt30);
|
|
expect(series[1].data[0].value).toBeCloseTo(1 - sqrt30);
|
|
// the largest-sum period reaches the full unit radius
|
|
const outer = series[0].data[0].value + series[1].data[0].value;
|
|
expect(outer).toBeCloseTo(1);
|
|
expect(periods[0].entries.map((e: any) => e.value)).toEqual([30, 10]);
|
|
});
|
|
|
|
test('rich tooltip lists every series of the hovered period', () => {
|
|
const { echartOptions } = transformProps(chartProps());
|
|
const html = (echartOptions as any).tooltip.formatter({
|
|
dataIndex: 0,
|
|
seriesName: 'East',
|
|
});
|
|
expect(html).toContain('East');
|
|
expect(html).toContain('West');
|
|
expect(html).toContain('30');
|
|
expect(html).toContain('10');
|
|
});
|
|
|
|
test('plain tooltip shows only the hovered series', () => {
|
|
const { echartOptions } = transformProps(chartProps({ richTooltip: false }));
|
|
const html = (echartOptions as any).tooltip.formatter({
|
|
dataIndex: 0,
|
|
seriesName: 'West',
|
|
});
|
|
expect(html).toContain('West');
|
|
expect(html).not.toContain('East');
|
|
});
|
|
|
|
test('sanitizes series names in tooltips', () => {
|
|
const evil = [
|
|
{
|
|
__timestamp: T1,
|
|
'sum__num, <img src=x onerror=alert(1)>': 5,
|
|
},
|
|
];
|
|
const props = new ChartProps({
|
|
width: 800,
|
|
height: 600,
|
|
formData,
|
|
theme: supersetTheme,
|
|
queriesData: [{ data: evil }],
|
|
hooks: {},
|
|
}) as unknown as EchartsRoseChartProps;
|
|
const { echartOptions } = transformProps(props);
|
|
const html = (echartOptions as any).tooltip.formatter({
|
|
dataIndex: 0,
|
|
seriesName: '<img src=x onerror=alert(1)>',
|
|
});
|
|
expect(html).not.toContain('onerror');
|
|
});
|
|
|
|
test('handles empty results without crashing', () => {
|
|
const props = new ChartProps({
|
|
width: 800,
|
|
height: 600,
|
|
formData,
|
|
theme: supersetTheme,
|
|
queriesData: [{ data: [] }],
|
|
hooks: {},
|
|
}) as unknown as EchartsRoseChartProps;
|
|
const { echartOptions, periods } = transformProps(props);
|
|
expect(periods).toEqual([]);
|
|
expect((echartOptions as any).series).toEqual([]);
|
|
});
|
|
|
|
test('area proportion clamps negative cumulatives instead of yielding NaN', () => {
|
|
// difference-style comparisons can produce negative values
|
|
const negatives = [
|
|
{ __timestamp: T1, 'sum__num, East': -30, 'sum__num, West': 10 },
|
|
];
|
|
const props = new ChartProps({
|
|
width: 800,
|
|
height: 600,
|
|
formData: { ...formData, roseAreaProportion: true },
|
|
theme: supersetTheme,
|
|
queriesData: [{ data: negatives }],
|
|
hooks: {},
|
|
}) as unknown as EchartsRoseChartProps;
|
|
const { echartOptions } = transformProps(props);
|
|
const { series } = echartOptions as any;
|
|
series.forEach((s: any) =>
|
|
s.data.forEach((d: any) => expect(Number.isNaN(d.value)).toBe(false)),
|
|
);
|
|
});
|
|
|
|
test('renders when __timestamp is serialized as a non-epoch string', () => {
|
|
// some backends/date formats return ISO-ish timestamps rather than epoch
|
|
// millis; a parseInt round-trip on the key would strand every period
|
|
const isoRecords = [
|
|
{ __timestamp: '2021-01-01', 'sum__num, East': 30, 'sum__num, West': 10 },
|
|
{ __timestamp: '2021-02-01', 'sum__num, East': 10, 'sum__num, West': 10 },
|
|
];
|
|
const props = new ChartProps({
|
|
width: 800,
|
|
height: 600,
|
|
formData,
|
|
theme: supersetTheme,
|
|
queriesData: [{ data: isoRecords }],
|
|
hooks: {},
|
|
}) as unknown as EchartsRoseChartProps;
|
|
const { echartOptions, periods, seriesNames } = transformProps(props);
|
|
const { series } = echartOptions as any;
|
|
|
|
// both periods survive and both series get their values
|
|
expect(periods).toHaveLength(2);
|
|
expect(seriesNames).toEqual(['East', 'West']);
|
|
expect(series[0].data.map((d: any) => d.value)).toEqual([30, 10]);
|
|
});
|