@mcdevsl/mc-superset-ui
Version:
353 lines (337 loc) • 10.5 kB
text/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 {
AnnotationData,
AnnotationLayer,
AnnotationOpacity,
CategoricalColorScale,
EventAnnotationLayer,
getTimeFormatter,
IntervalAnnotationLayer,
isTimeseriesAnnotationResult,
smartDateDetailedFormatter,
smartDateFormatter,
TimeFormatter,
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 { MarkLine1DDataItemOption } from 'echarts/types/src/component/marker/MarkLineModel';
import { extractForecastSeriesContext } from '../utils/prophet';
import { ForecastSeriesEnum, LegendOrientation } from '../types';
import { EchartsTimeseriesSeriesType } from './types';
import {
evalFormula,
extractRecordAnnotations,
formatAnnotationLabel,
parseAnnotationOpacity,
} from '../utils/annotation';
import { getChartPadding } from '../utils/series';
import { TIMESERIES_CONSTANTS } from '../constants';
export function transformSeries(
series: SeriesOption,
colorScale: CategoricalColorScale,
opts: {
area?: boolean;
forecastEnabled?: boolean;
markerEnabled?: boolean;
markerSize?: number;
opacity?: number;
seriesType?: EchartsTimeseriesSeriesType;
stack?: boolean;
yAxisIndex?: number;
},
): SeriesOption | undefined {
const { name } = series;
const {
area,
forecastEnabled,
markerEnabled,
markerSize,
opacity,
seriesType,
stack,
yAxisIndex = 0,
} = opts;
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,
yAxisIndex,
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),
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: MarkLine1DDataItemOption[] = [
{
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,
markerSize: number,
data: TimeseriesDataRecord[],
annotationData: AnnotationData,
): SeriesOption[] {
const series: SeriesOption[] = [];
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;
}
export function getPadding(
showLegend: boolean,
legendOrientation: LegendOrientation,
addYAxisLabelOffset: boolean,
zoomable: boolean,
margin?: string | number | null,
): {
bottom: number;
left: number;
right: number;
top: number;
} {
const yAxisOffset = addYAxisLabelOffset ? TIMESERIES_CONSTANTS.yAxisLabelTopOffset : 0;
return getChartPadding(showLegend, legendOrientation, margin, {
top: TIMESERIES_CONSTANTS.gridOffsetTop + yAxisOffset,
bottom: zoomable
? TIMESERIES_CONSTANTS.gridOffsetBottomZoomable
: TIMESERIES_CONSTANTS.gridOffsetBottom,
left: TIMESERIES_CONSTANTS.gridOffsetLeft,
right:
showLegend && legendOrientation === LegendOrientation.Right
? 0
: TIMESERIES_CONSTANTS.gridOffsetRight,
});
}
export function getTooltipTimeFormatter(format?: string): TimeFormatter | StringConstructor {
if (format === smartDateFormatter.id) {
return smartDateDetailedFormatter;
}
if (format) {
return getTimeFormatter(format);
}
return String;
}
export function getXAxisFormatter(format?: string): TimeFormatter | StringConstructor | undefined {
if (format === smartDateFormatter.id || !format) {
return undefined;
}
if (format) {
return getTimeFormatter(format);
}
return String;
}