fix: Timeseries annotation layers (#34709)

This commit is contained in:
Michael S. Molina
2025-08-15 12:59:30 -03:00
committed by GitHub
parent 3a007f6284
commit fc95c4fc89
11 changed files with 69 additions and 183 deletions

View File

@@ -158,31 +158,10 @@ export function isTableAnnotationLayer(
return layer.sourceType === AnnotationSourceType.Table;
}
export type RecordAnnotationResult = {
records: DataRecord[];
export type AnnotationResult = {
records?: DataRecord[];
};
export type TimeseriesAnnotationResult = {
key: string;
values: { x: string | number; y?: number }[];
}[];
export type AnnotationResult =
| RecordAnnotationResult
| TimeseriesAnnotationResult;
export function isTimeseriesAnnotationResult(
result: AnnotationResult,
): result is TimeseriesAnnotationResult {
return Array.isArray(result);
}
export function isRecordAnnotationResult(
result: any,
): result is RecordAnnotationResult {
return Array.isArray(result?.records);
}
export type AnnotationData = { [key: string]: AnnotationResult };
export type Annotation = {

View File

@@ -27,14 +27,10 @@ import {
isEventAnnotationLayer,
isFormulaAnnotationLayer,
isIntervalAnnotationLayer,
isRecordAnnotationResult,
isTableAnnotationLayer,
isTimeseriesAnnotationLayer,
isTimeseriesAnnotationResult,
RecordAnnotationResult,
TableAnnotationLayer,
TimeseriesAnnotationLayer,
TimeseriesAnnotationResult,
} from '@superset-ui/core';
describe('AnnotationLayer type guards', () => {
@@ -82,23 +78,6 @@ describe('AnnotationLayer type guards', () => {
show: true,
showLabel: false,
};
const timeseriesAnnotationResult: TimeseriesAnnotationResult = [
{
key: 'My Key',
values: [
{ x: -1000, y: 0 },
{ x: 0, y: 1000 },
{ x: 1000, y: 2000 },
],
},
];
const recordAnnotationResult: RecordAnnotationResult = {
records: [
{ a: 1, b: 2 },
{ a: 2, b: 3 },
],
};
describe('isFormulaAnnotationLayer', () => {
it('should return true when it is the correct type', () => {
expect(isFormulaAnnotationLayer(formulaAnnotationLayer)).toEqual(true);
@@ -161,28 +140,4 @@ describe('AnnotationLayer type guards', () => {
expect(isTableAnnotationLayer(formulaAnnotationLayer)).toEqual(false);
});
});
describe('isTimeseriesAnnotationResult', () => {
it('should return true when it is the correct type', () => {
expect(isTimeseriesAnnotationResult(timeseriesAnnotationResult)).toEqual(
true,
);
});
it('should return false otherwise', () => {
expect(isTimeseriesAnnotationResult(recordAnnotationResult)).toEqual(
false,
);
});
});
describe('isRecordAnnotationResult', () => {
it('should return true when it is the correct type', () => {
expect(isRecordAnnotationResult(recordAnnotationResult)).toEqual(true);
});
it('should return false otherwise', () => {
expect(isRecordAnnotationResult(timeseriesAnnotationResult)).toEqual(
false,
);
});
});
});

View File

@@ -718,7 +718,7 @@ export default function transformProps(
ForecastSeriesEnum.Observation,
)
.map(entry => entry.id || entry.name || '')
.concat(extractAnnotationLabels(annotationLayers, annotationData)),
.concat(extractAnnotationLabels(annotationLayers)),
},
series: dedupSeries(reorderForecastSeries(series) as SeriesOption[]),
toolbox: {

View File

@@ -60,6 +60,7 @@ export default class EchartsTimeseriesLineChartPlugin extends EchartsChartPlugin
'Line chart is used to visualize measurements taken over a given category. Line chart is a type of chart which displays information as a series of data points connected by straight line segments. It is a basic type of chart common in many fields.',
),
exampleGallery: [{ url: example1 }, { url: example2 }],
canBeAnnotationTypes: [AnnotationType.Timeseries],
supportedAnnotationTypes: [
AnnotationType.Event,
AnnotationType.Formula,

View File

@@ -493,7 +493,7 @@ export default function transformProps(
ForecastSeriesEnum.Observation,
)
.map(entry => entry.name || '')
.concat(extractAnnotationLabels(annotationLayers, annotationData));
.concat(extractAnnotationLabels(annotationLayers));
let xAxis: any = {
type: xAxisType,

View File

@@ -25,7 +25,6 @@ import {
FilterState,
FormulaAnnotationLayer,
IntervalAnnotationLayer,
isTimeseriesAnnotationResult,
LegendState,
SupersetTheme,
TimeseriesAnnotationLayer,
@@ -557,26 +556,30 @@ export function transformTimeseriesAnnotation(
const { hideLine, name, opacity, showMarkers, style, width, color } = layer;
const result = annotationData[name];
const isHorizontal = orientation === OrientationType.Horizontal;
if (isTimeseriesAnnotationResult(result)) {
result.forEach(annotation => {
const { key, values } = annotation;
series.push({
type: 'line',
id: key,
name: key,
data: values.map(({ x, y }) =>
isHorizontal
? ([y, x] as [number, OptionName])
: ([x, y] as [OptionName, number]),
),
symbolSize: showMarkers ? markerSize : 0,
lineStyle: {
opacity: parseAnnotationOpacity(opacity),
type: style as ZRLineType,
width: hideLine ? 0 : width,
color: color || colorScale(name, sliceId),
},
});
const { records } = result;
if (records) {
const data = records.map(record => {
const keys = Object.keys(record);
const x = keys.length > 0 ? record[keys[0]] : 0;
const y = keys.length > 1 ? record[keys[1]] : 0;
return isHorizontal
? ([y, x] as [number, OptionName])
: ([x, y] as [OptionName, number]);
});
const computedStyle = {
opacity: parseAnnotationOpacity(opacity),
type: style as ZRLineType,
width: hideLine ? 0 : width,
color: color || colorScale(name, sliceId),
};
series.push({
type: 'line',
id: name,
name,
data,
symbolSize: showMarkers ? markerSize : 0,
itemStyle: computedStyle,
lineStyle: computedStyle,
});
}
return series;

View File

@@ -29,9 +29,7 @@ import {
DataRecord,
evalExpression,
FormulaAnnotationLayer,
isRecordAnnotationResult,
isTableAnnotationLayer,
isTimeseriesAnnotationResult,
} from '@superset-ui/core';
import { EchartsTimeseriesChartProps } from '../types';
import { EchartsMixedTimeseriesProps } from '../MixedTimeseries/types';
@@ -79,27 +77,24 @@ export function extractRecordAnnotations(
): Annotation[] {
const { name } = annotationLayer;
const result = annotationData[name];
if (isRecordAnnotationResult(result)) {
const { records } = result;
const {
descriptionColumns = [],
intervalEndColumn = '',
timeColumn = '',
titleColumn = '',
} = isTableAnnotationLayer(annotationLayer)
? annotationLayer
: NATIVE_COLUMN_NAMES;
const records = result?.records || [];
const {
descriptionColumns = [],
intervalEndColumn = '',
timeColumn = '',
titleColumn = '',
} = isTableAnnotationLayer(annotationLayer)
? annotationLayer
: NATIVE_COLUMN_NAMES;
return records.map(record => ({
descriptions: descriptionColumns.map(
column => (record[column] || '') as string,
) as string[],
intervalEnd: (record[intervalEndColumn] || '') as string,
time: (record[timeColumn] || '') as string,
title: (record[titleColumn] || '') as string,
}));
}
throw new Error('Please rerun the query.');
return records.map(record => ({
descriptions: descriptionColumns.map(
column => (record[column] || '') as string,
) as string[],
intervalEnd: (record[intervalEndColumn] || '') as string,
time: (record[timeColumn] || '') as string,
title: (record[titleColumn] || '') as string,
}));
}
export function formatAnnotationLabel(
@@ -120,23 +115,16 @@ export function formatAnnotationLabel(
return labels.join('\n\n');
}
export function extractAnnotationLabels(
layers: AnnotationLayer[],
data: AnnotationData,
): string[] {
export function extractAnnotationLabels(layers: AnnotationLayer[]): string[] {
const formulaAnnotationLabels = layers
.filter(anno => anno.annotationType === AnnotationType.Formula && anno.show)
.map(anno => anno.name);
const timeseriesAnnotationLabels = layers
.filter(
anno => anno.annotationType === AnnotationType.Timeseries && anno.show,
)
.flatMap(anno => {
const result = data[anno.name];
return isTimeseriesAnnotationResult(result)
? result.map(annoSeries => annoSeries.key)
: [];
});
.map(anno => anno.name);
return formulaAnnotationLabels.concat(timeseriesAnnotationLabels);
}

View File

@@ -240,21 +240,12 @@ describe('EchartsTimeseries transformProps', () => {
},
],
},
'My Timeseries': [
{
key: 'My Line',
values: [
{
x: 10000,
y: 11000,
},
{
x: 20000,
y: 21000,
},
],
},
],
'My Timeseries': {
records: [
{ x: 10000, y: 11000 },
{ x: 20000, y: 21000 },
],
},
};
const chartProps = new ChartProps({
...chartPropsConfig,
@@ -274,12 +265,12 @@ describe('EchartsTimeseries transformProps', () => {
expect.objectContaining({
echartOptions: expect.objectContaining({
legend: expect.objectContaining({
data: ['San Francisco', 'New York', 'My Line'],
data: ['San Francisco', 'New York', 'My Timeseries'],
}),
series: expect.arrayContaining([
expect.objectContaining({
type: 'line',
id: 'My Line',
id: 'My Timeseries',
}),
expect.objectContaining({
type: 'line',

View File

@@ -17,7 +17,6 @@
* under the License.
*/
import {
AnnotationData,
AnnotationLayer,
AnnotationOpacity,
AnnotationSourceType,
@@ -128,21 +127,7 @@ describe('extractAnnotationLabels', () => {
showLabel: true,
},
];
const results: AnnotationData = {
'My Interval': {
records: [{ col: 1 }],
},
'My Line': [
{ key: 'Line 1', values: [] },
{ key: 'Line 2', values: [] },
],
};
expect(extractAnnotationLabels(layers, results)).toEqual([
'My Formula',
'Line 1',
'Line 2',
]);
expect(extractAnnotationLabels(layers)).toEqual(['My Formula', 'My Line']);
});
});

View File

@@ -282,30 +282,13 @@ const mockTimeseriesAnnotationLayer: TimeseriesAnnotationLayer = {
};
const mockTimeseriesAnnotationData: AnnotationData = {
'Timeseries annotation layer': [
{
key: 'Key 1',
values: [
{
x: 10,
y: 12,
},
],
},
{
key: 'Key 2',
values: [
{
x: 12,
y: 15,
},
{
x: 15,
y: 20,
},
],
},
],
'Timeseries annotation layer': {
records: [
{ x: 10, y: 12 },
{ x: 12, y: 15 },
{ x: 15, y: 20 },
],
},
};
describe('transformTimeseriesAnnotation', () => {
@@ -319,8 +302,8 @@ describe('transformTimeseriesAnnotation', () => {
CategoricalColorNamespace.getScale(''),
).map(annotation => annotation.data),
).toEqual([
[[10, 12]],
[
[10, 12],
[12, 15],
[15, 20],
],
@@ -339,8 +322,8 @@ describe('transformTimeseriesAnnotation', () => {
OrientationType.Horizontal,
).map(annotation => annotation.data),
).toEqual([
[[12, 10]],
[
[12, 10],
[15, 12],
[20, 15],
],

View File

@@ -31,6 +31,7 @@ import {
styled,
getColumnLabel,
withTheme,
VizType,
} from '@superset-ui/core';
import SelectControl from 'src/explore/components/controls/SelectControl';
import TextControl from 'src/explore/components/controls/TextControl';
@@ -244,7 +245,7 @@ class AnnotationLayer extends PureComponent {
chartMetadata.canBeAnnotationType(annotationType),
)
.map(({ key, value: chartMetadata }) => ({
value: key,
value: key === VizType.Line ? 'line' : key,
label: chartMetadata.name,
}));
// Prepend native source if applicable