| /** |
| * 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. |
| */ |
| /* eslint-disable camelcase */ |
| import { |
| AnnotationLayer, |
| CategoricalColorNamespace, |
| GenericDataType, |
| getNumberFormatter, |
| isEventAnnotationLayer, |
| isFormulaAnnotationLayer, |
| isIntervalAnnotationLayer, |
| isTimeseriesAnnotationLayer, |
| TimeseriesChartDataResponseResult, |
| t, |
| getXAxisLabel, |
| isPhysicalColumn, |
| isDefined, |
| } from '@superset-ui/core'; |
| import { isDerivedSeries } from '@superset-ui/chart-controls'; |
| import { EChartsCoreOption, SeriesOption } from 'echarts'; |
| import { ZRLineType } from 'echarts/types/src/util/types'; |
| import { |
| EchartsTimeseriesChartProps, |
| EchartsTimeseriesFormData, |
| EchartsTimeseriesSeriesType, |
| TimeseriesChartTransformedProps, |
| OrientationType, |
| } from './types'; |
| import { DEFAULT_FORM_DATA } from './constants'; |
| import { ForecastSeriesEnum, ForecastValue, Refs } from '../types'; |
| import { parseYAxisBound } from '../utils/controls'; |
| import { |
| currentSeries, |
| dedupSeries, |
| extractSeries, |
| getAxisType, |
| getColtypesMapping, |
| getLegendProps, |
| extractDataTotalValues, |
| extractShowValueIndexes, |
| } from '../utils/series'; |
| import { |
| extractAnnotationLabels, |
| getAnnotationData, |
| } from '../utils/annotation'; |
| import { |
| extractForecastSeriesContext, |
| extractForecastSeriesContexts, |
| extractForecastValuesFromTooltipParams, |
| formatForecastTooltipSeries, |
| rebaseForecastDatum, |
| } from '../utils/forecast'; |
| import { convertInteger } from '../utils/convertInteger'; |
| import { defaultGrid, defaultYAxis } from '../defaults'; |
| import { |
| getPadding, |
| getTooltipTimeFormatter, |
| getXAxisFormatter, |
| transformEventAnnotation, |
| transformFormulaAnnotation, |
| transformIntervalAnnotation, |
| transformSeries, |
| transformTimeseriesAnnotation, |
| } from './transformers'; |
| import { |
| AreaChartExtraControlsValue, |
| TIMESERIES_CONSTANTS, |
| TIMEGRAIN_TO_TIMESTAMP, |
| } from '../constants'; |
| import { getDefaultTooltip } from '../utils/tooltip'; |
| |
| export default function transformProps( |
| chartProps: EchartsTimeseriesChartProps, |
| ): TimeseriesChartTransformedProps { |
| const { |
| width, |
| height, |
| filterState, |
| formData, |
| hooks, |
| queriesData, |
| datasource, |
| theme, |
| inContextMenu, |
| emitCrossFilters, |
| } = chartProps; |
| const { verboseMap = {} } = datasource; |
| const [queryData] = queriesData; |
| const { data = [], label_map: labelMap } = |
| queryData as TimeseriesChartDataResponseResult; |
| const dataTypes = getColtypesMapping(queryData); |
| const annotationData = getAnnotationData(chartProps); |
| |
| const { |
| area, |
| annotationLayers, |
| colorScheme, |
| contributionMode, |
| forecastEnabled, |
| legendOrientation, |
| legendType, |
| legendMargin, |
| logAxis, |
| markerEnabled, |
| markerSize, |
| opacity, |
| minorSplitLine, |
| seriesType, |
| showLegend, |
| stack, |
| truncateYAxis, |
| yAxisFormat, |
| xAxisTimeFormat, |
| yAxisBounds, |
| tooltipTimeFormat, |
| tooltipSortByMetric, |
| zoomable, |
| richTooltip, |
| xAxis: xAxisOrig, |
| xAxisLabelRotation, |
| groupby, |
| showValue, |
| onlyTotal, |
| percentageThreshold, |
| xAxisTitle, |
| yAxisTitle, |
| xAxisTitleMargin, |
| yAxisTitleMargin, |
| yAxisTitlePosition, |
| sliceId, |
| timeGrainSqla, |
| orientation, |
| }: EchartsTimeseriesFormData = { ...DEFAULT_FORM_DATA, ...formData }; |
| const refs: Refs = {}; |
| |
| const colorScale = CategoricalColorNamespace.getScale(colorScheme as string); |
| const rebasedData = rebaseForecastDatum(data, verboseMap); |
| let xAxisLabel = getXAxisLabel(chartProps.rawFormData) as string; |
| if ( |
| isPhysicalColumn(chartProps.rawFormData?.x_axis) && |
| isDefined(verboseMap[xAxisLabel]) |
| ) { |
| xAxisLabel = verboseMap[xAxisLabel]; |
| } |
| const isHorizontal = orientation === OrientationType.horizontal; |
| const { totalStackedValues, thresholdValues } = extractDataTotalValues( |
| rebasedData, |
| { |
| stack, |
| percentageThreshold, |
| xAxisCol: xAxisLabel, |
| }, |
| ); |
| const rawSeries = extractSeries(rebasedData, { |
| fillNeighborValue: stack && !forecastEnabled ? 0 : undefined, |
| xAxis: xAxisLabel, |
| removeNulls: seriesType === EchartsTimeseriesSeriesType.Scatter, |
| stack, |
| totalStackedValues, |
| isHorizontal, |
| }); |
| const showValueIndexes = extractShowValueIndexes(rawSeries, { |
| stack, |
| onlyTotal, |
| isHorizontal, |
| }); |
| const seriesContexts = extractForecastSeriesContexts( |
| Object.values(rawSeries).map(series => series.name as string), |
| ); |
| const isAreaExpand = stack === AreaChartExtraControlsValue.Expand; |
| const xAxisDataType = dataTypes?.[xAxisLabel] ?? dataTypes?.[xAxisOrig]; |
| |
| const xAxisType = getAxisType(xAxisDataType); |
| const series: SeriesOption[] = []; |
| const formatter = getNumberFormatter( |
| contributionMode || isAreaExpand ? ',.0%' : yAxisFormat, |
| ); |
| |
| rawSeries.forEach(entry => { |
| const lineStyle = isDerivedSeries(entry, chartProps.rawFormData) |
| ? { type: 'dashed' as ZRLineType } |
| : {}; |
| const transformedSeries = transformSeries(entry, colorScale, { |
| area, |
| filterState, |
| seriesContexts, |
| markerEnabled, |
| markerSize, |
| areaOpacity: opacity, |
| seriesType, |
| stack, |
| formatter, |
| showValue, |
| onlyTotal, |
| totalStackedValues, |
| showValueIndexes, |
| thresholdValues, |
| richTooltip, |
| sliceId, |
| isHorizontal, |
| lineStyle, |
| }); |
| if (transformedSeries) series.push(transformedSeries); |
| }); |
| |
| const selectedValues = (filterState.selectedValues || []).reduce( |
| (acc: Record<string, number>, selectedValue: string) => { |
| const index = series.findIndex(({ name }) => name === selectedValue); |
| return { |
| ...acc, |
| [index]: selectedValue, |
| }; |
| }, |
| {}, |
| ); |
| |
| annotationLayers |
| .filter((layer: AnnotationLayer) => layer.show) |
| .forEach((layer: AnnotationLayer) => { |
| if (isFormulaAnnotationLayer(layer)) |
| series.push( |
| transformFormulaAnnotation( |
| layer, |
| data, |
| xAxisLabel, |
| xAxisType, |
| colorScale, |
| sliceId, |
| ), |
| ); |
| else if (isIntervalAnnotationLayer(layer)) { |
| series.push( |
| ...transformIntervalAnnotation( |
| layer, |
| data, |
| annotationData, |
| colorScale, |
| theme, |
| sliceId, |
| ), |
| ); |
| } else if (isEventAnnotationLayer(layer)) { |
| series.push( |
| ...transformEventAnnotation( |
| layer, |
| data, |
| annotationData, |
| colorScale, |
| theme, |
| sliceId, |
| ), |
| ); |
| } else if (isTimeseriesAnnotationLayer(layer)) { |
| series.push( |
| ...transformTimeseriesAnnotation( |
| layer, |
| markerSize, |
| data, |
| annotationData, |
| colorScale, |
| sliceId, |
| ), |
| ); |
| } |
| }); |
| |
| // yAxisBounds need to be parsed to replace incompatible values with undefined |
| let [min, max] = (yAxisBounds || []).map(parseYAxisBound); |
| |
| // default to 0-100% range when doing row-level contribution chart |
| if ((contributionMode === 'row' || isAreaExpand) && stack) { |
| if (min === undefined) min = 0; |
| if (max === undefined) max = 1; |
| } |
| |
| const tooltipFormatter = |
| xAxisDataType === GenericDataType.TEMPORAL |
| ? getTooltipTimeFormatter(tooltipTimeFormat) |
| : String; |
| const xAxisFormatter = |
| xAxisDataType === GenericDataType.TEMPORAL |
| ? getXAxisFormatter(xAxisTimeFormat) |
| : String; |
| |
| const { |
| setDataMask = () => {}, |
| setControlValue = () => {}, |
| onContextMenu, |
| } = hooks; |
| |
| const addYAxisLabelOffset = !!yAxisTitle; |
| const addXAxisLabelOffset = !!xAxisTitle; |
| const padding = getPadding( |
| showLegend, |
| legendOrientation, |
| addYAxisLabelOffset, |
| zoomable, |
| legendMargin, |
| addXAxisLabelOffset, |
| yAxisTitlePosition, |
| convertInteger(yAxisTitleMargin), |
| convertInteger(xAxisTitleMargin), |
| ); |
| |
| const legendData = rawSeries |
| .filter( |
| entry => |
| extractForecastSeriesContext(entry.name || '').type === |
| ForecastSeriesEnum.Observation, |
| ) |
| .map(entry => entry.name || '') |
| .concat(extractAnnotationLabels(annotationLayers, annotationData)); |
| |
| let xAxis: any = { |
| type: xAxisType, |
| name: xAxisTitle, |
| nameGap: convertInteger(xAxisTitleMargin), |
| nameLocation: 'middle', |
| axisLabel: { |
| hideOverlap: true, |
| formatter: xAxisFormatter, |
| rotate: xAxisLabelRotation, |
| }, |
| minInterval: |
| xAxisType === 'time' && timeGrainSqla |
| ? TIMEGRAIN_TO_TIMESTAMP[timeGrainSqla] |
| : 0, |
| }; |
| let yAxis: any = { |
| ...defaultYAxis, |
| type: logAxis ? 'log' : 'value', |
| min, |
| max, |
| minorTick: { show: true }, |
| minorSplitLine: { show: minorSplitLine }, |
| axisLabel: { formatter }, |
| scale: truncateYAxis, |
| name: yAxisTitle, |
| nameGap: convertInteger(yAxisTitleMargin), |
| nameLocation: yAxisTitlePosition === 'Left' ? 'middle' : 'end', |
| }; |
| |
| if (isHorizontal) { |
| [xAxis, yAxis] = [yAxis, xAxis]; |
| [padding.bottom, padding.left] = [padding.left, padding.bottom]; |
| } |
| |
| const echartOptions: EChartsCoreOption = { |
| useUTC: true, |
| grid: { |
| ...defaultGrid, |
| ...padding, |
| }, |
| xAxis, |
| yAxis, |
| tooltip: { |
| ...getDefaultTooltip(refs), |
| show: !inContextMenu, |
| trigger: richTooltip ? 'axis' : 'item', |
| formatter: (params: any) => { |
| const [xIndex, yIndex] = isHorizontal ? [1, 0] : [0, 1]; |
| const xValue: number = richTooltip |
| ? params[0].value[xIndex] |
| : params.value[xIndex]; |
| const forecastValue: any[] = richTooltip ? params : [params]; |
| |
| if (richTooltip && tooltipSortByMetric) { |
| forecastValue.sort((a, b) => b.data[yIndex] - a.data[yIndex]); |
| } |
| |
| const rows: Array<string> = [`${tooltipFormatter(xValue)}`]; |
| const forecastValues: Record<string, ForecastValue> = |
| extractForecastValuesFromTooltipParams(forecastValue, isHorizontal); |
| |
| Object.keys(forecastValues).forEach(key => { |
| const value = forecastValues[key]; |
| const content = formatForecastTooltipSeries({ |
| ...value, |
| seriesName: key, |
| formatter, |
| }); |
| if (currentSeries.name === key) { |
| rows.push(`<span style="font-weight: 700">${content}</span>`); |
| } else { |
| rows.push(`<span style="opacity: 0.7">${content}</span>`); |
| } |
| }); |
| return rows.join('<br />'); |
| }, |
| }, |
| legend: { |
| ...getLegendProps(legendType, legendOrientation, showLegend, zoomable), |
| data: legendData as string[], |
| }, |
| series: dedupSeries(series), |
| toolbox: { |
| show: zoomable, |
| top: TIMESERIES_CONSTANTS.toolboxTop, |
| right: TIMESERIES_CONSTANTS.toolboxRight, |
| feature: { |
| dataZoom: { |
| yAxisIndex: false, |
| title: { |
| zoom: t('zoom area'), |
| back: t('restore zoom'), |
| }, |
| }, |
| }, |
| }, |
| dataZoom: zoomable |
| ? [ |
| { |
| type: 'slider', |
| start: TIMESERIES_CONSTANTS.dataZoomStart, |
| end: TIMESERIES_CONSTANTS.dataZoomEnd, |
| bottom: TIMESERIES_CONSTANTS.zoomBottom, |
| }, |
| ] |
| : [], |
| }; |
| |
| return { |
| echartOptions, |
| emitCrossFilters, |
| formData, |
| groupby, |
| height, |
| labelMap, |
| selectedValues, |
| setDataMask, |
| setControlValue, |
| width, |
| legendData, |
| onContextMenu, |
| xValueFormatter: tooltipFormatter, |
| xAxis: { |
| label: xAxisLabel, |
| type: xAxisType, |
| }, |
| refs, |
| }; |
| } |