blob: 36873e76be091965057a8b86eec6a7a0288d06e2 [file] [log] [blame]
/**
* 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 {
AnnotationData,
AnnotationLayer,
AnnotationOpacity,
CategoricalColorScale,
EventAnnotationLayer,
IntervalAnnotationLayer,
isTimeseriesAnnotationResult,
TimeseriesAnnotationLayer,
TimeseriesDataRecord,
} from '@superset-ui/core';
import { SeriesOption } from 'echarts';
import {
CallbackDataParams,
DefaultExtraStateOpts,
ItemStyleOption,
LineStyleOption,
OptionName,
ZRLineType,
} from 'echarts/types/src/util/types';
import {
MarkArea1DDataItemOption,
MarkArea2DDataItemOption,
} from 'echarts/types/src/component/marker/MarkAreaModel';
import { extractForecastSeriesContext } from '../utils/prophet';
import { ForecastSeriesEnum } from '../types';
import { DEFAULT_FORM_DATA, EchartsTimeseriesFormData } from './types';
import {
evalFormula,
extractRecordAnnotations,
formatAnnotationLabel,
parseAnnotationOpacity,
} from '../utils/annotation';
export function transformSeries(
series: SeriesOption,
formData: EchartsTimeseriesFormData,
colorScale: CategoricalColorScale,
): SeriesOption | undefined {
const { name } = series;
const {
area,
forecastEnabled,
markerEnabled,
markerSize,
opacity,
seriesType,
stack,
richTooltip,
}: EchartsTimeseriesFormData = {
...DEFAULT_FORM_DATA,
...formData,
};
const forecastSeries = extractForecastSeriesContext(name || '');
const isConfidenceBand =
forecastSeries.type === ForecastSeriesEnum.ForecastLower ||
forecastSeries.type === ForecastSeriesEnum.ForecastUpper;
// don't create a series if doing a stack or area chart and the result
// is a confidence band
if ((stack || area) && isConfidenceBand) return undefined;
const isObservation = forecastSeries.type === ForecastSeriesEnum.Observation;
const isTrend = forecastSeries.type === ForecastSeriesEnum.ForecastTrend;
let stackId;
if (isConfidenceBand) {
stackId = forecastSeries.name;
} else if (stack && isObservation) {
// the suffix of the observation series is '' (falsy), which disables
// stacking. Therefore we need to set something that is truthy.
stackId = 'obs';
} else if (stack && isTrend) {
stackId = forecastSeries.type;
}
let plotType;
if (!isConfidenceBand && (seriesType === 'scatter' || (forecastEnabled && isObservation))) {
plotType = 'scatter';
} else if (isConfidenceBand) {
plotType = 'line';
} else {
plotType = seriesType === 'bar' ? 'bar' : 'line';
}
const lineStyle = isConfidenceBand ? { opacity: 0 } : {};
return {
...series,
name: forecastSeries.name,
itemStyle: {
color: colorScale(forecastSeries.name),
},
// @ts-ignore
type: plotType,
smooth: seriesType === 'smooth',
// @ts-ignore
step: ['start', 'middle', 'end'].includes(seriesType as string) ? seriesType : undefined,
stack: stackId,
lineStyle,
areaStyle: {
opacity: forecastSeries.type === ForecastSeriesEnum.ForecastUpper || area ? opacity : 0,
},
showSymbol:
!isConfidenceBand &&
(plotType === 'scatter' ||
(forecastEnabled && isObservation) ||
markerEnabled ||
!richTooltip), // TODO: forcing markers when richTooltip is enabled will be removed once ECharts supports item based tooltips without markers
symbolSize: markerSize,
};
}
export function transformFormulaAnnotation(
layer: AnnotationLayer,
data: TimeseriesDataRecord[],
colorScale: CategoricalColorScale,
): SeriesOption {
const { name, color, opacity, width, style } = layer;
return {
name,
id: name,
itemStyle: {
color: color || colorScale(name),
},
lineStyle: {
opacity: parseAnnotationOpacity(opacity),
type: style as ZRLineType,
width,
},
type: 'line',
smooth: true,
data: evalFormula(layer, data),
symbolSize: 0,
z: 0,
};
}
export function transformIntervalAnnotation(
layer: IntervalAnnotationLayer,
data: TimeseriesDataRecord[],
annotationData: AnnotationData,
colorScale: CategoricalColorScale,
): SeriesOption[] {
const series: SeriesOption[] = [];
const annotations = extractRecordAnnotations(layer, annotationData);
annotations.forEach(annotation => {
const { name, color, opacity } = layer;
const { descriptions, intervalEnd, time, title } = annotation;
const label = formatAnnotationLabel(name, title, descriptions);
const intervalData: (MarkArea1DDataItemOption | MarkArea2DDataItemOption)[] = [
[
{
name: label,
xAxis: time,
},
{
xAxis: intervalEnd,
},
],
];
series.push({
id: `Interval - ${label}`,
type: 'line',
animation: false,
markArea: {
silent: false,
itemStyle: {
color: color || colorScale(name),
opacity: parseAnnotationOpacity(opacity || AnnotationOpacity.Medium),
emphasis: {
opacity: 0.8,
},
} as ItemStyleOption,
label: {
show: false,
color: '#000000',
// @ts-ignore
emphasis: {
fontWeight: 'bold',
show: true,
position: 'insideTop',
verticalAlign: 'top',
backgroundColor: '#ffffff',
},
},
data: intervalData,
},
});
});
return series;
}
export function transformEventAnnotation(
layer: EventAnnotationLayer,
data: TimeseriesDataRecord[],
annotationData: AnnotationData,
colorScale: CategoricalColorScale,
): SeriesOption[] {
const series: SeriesOption[] = [];
const annotations = extractRecordAnnotations(layer, annotationData);
annotations.forEach(annotation => {
const { name, color, opacity, style, width } = layer;
const { descriptions, time, title } = annotation;
const label = formatAnnotationLabel(name, title, descriptions);
const eventData = [
{
name: label,
xAxis: (time as unknown) as number,
},
];
const lineStyle: LineStyleOption & DefaultExtraStateOpts['emphasis'] = {
width,
type: style as ZRLineType,
color: color || colorScale(name),
opacity: parseAnnotationOpacity(opacity),
emphasis: {
width: width ? width + 1 : width,
opacity: 1,
},
};
series.push({
id: `Event - ${label}`,
type: 'line',
animation: false,
markLine: {
silent: false,
symbol: 'none',
lineStyle,
label: {
show: false,
color: '#000000',
position: 'insideEndTop',
// @ts-ignore
emphasis: {
formatter: (params: CallbackDataParams) => params.name,
fontWeight: 'bold',
show: true,
backgroundColor: '#ffffff',
},
},
data: eventData,
},
});
});
return series;
}
export function transformTimeseriesAnnotation(
layer: TimeseriesAnnotationLayer,
formData: EchartsTimeseriesFormData,
data: TimeseriesDataRecord[],
annotationData: AnnotationData,
): SeriesOption[] {
const series: SeriesOption[] = [];
const { markerSize } = formData;
const { hideLine, name, opacity, showMarkers, style, width } = layer;
const result = annotationData[name];
if (isTimeseriesAnnotationResult(result)) {
result.forEach(annotation => {
const { key, values } = annotation;
series.push({
type: 'line',
id: key,
name: key,
data: values.map(row => [row.x, row.y] as [OptionName, number]),
symbolSize: showMarkers ? markerSize : 0,
lineStyle: {
opacity: parseAnnotationOpacity(opacity),
type: style as ZRLineType,
width: hideLine ? 0 : width,
},
});
});
}
return series;
}