/** * 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 { invert } from 'lodash-es'; import { rebaseToPercentChange } from './percentChange'; import { t } from '@apache-superset/core/translation'; import { AnnotationLayer, AxisType, buildCustomFormatters, CategoricalColorNamespace, CurrencyFormatter, DataRecordValue, DTTM_ALIAS, ensureIsArray, tooltipHtml, getCustomFormatter, getMetricLabel, getNumberFormatter, getXAxisLabel, isDefined, isEventAnnotationLayer, isFormulaAnnotationLayer, isIntervalAnnotationLayer, isPhysicalColumn, isTimeseriesAnnotationLayer, resolveAutoCurrency, TimeseriesChartDataResponseResult, TimeseriesDataRecord, NumberFormats, } from '@superset-ui/core'; import { GenericDataType } from '@apache-superset/core/common'; import { extractExtraMetrics, getOriginalSeries, getTimeOffset, isDerivedSeries, } from '@superset-ui/chart-controls'; import type { EChartsCoreOption } from 'echarts/core'; import type { LineStyleOption, CallbackDataParams, } from 'echarts/types/src/util/types'; import type { SeriesOption } from 'echarts'; import { EchartsTimeseriesChartProps, EchartsTimeseriesFormData, EchartsTimeseriesSeriesType, OrientationType, TimeseriesChartTransformedProps, } from './types'; import { DEFAULT_FORM_DATA } from './constants'; import { ForecastSeriesEnum, ForecastValue, LegendOrientation, LegendType, Refs, } from '../types'; import { parseAxisBound } from '../utils/controls'; import { calculateLowerLogTick, dedupSeries, extractDataTotalValues, extractSeries, extractShowValueIndexes, extractTooltipKeys, getAreaScaledSymbolSize, getAxisType, getColtypesMapping, getHorizontalLegendAvailableWidth, getLegendProps, getMinAndMaxFromBounds, } from '../utils/series'; import { resolveLegendLayout } from '../utils/legendLayout'; import { extractAnnotationLabels, getAnnotationData, } from '../utils/annotation'; import { extractForecastSeriesContext, extractForecastSeriesContexts, extractForecastValuesFromTooltipParams, formatForecastTooltipSeries, rebaseForecastDatum, reorderForecastSeries, } from '../utils/forecast'; import { convertInteger } from '../utils/convertInteger'; import { defaultGrid, defaultYAxis } from '../defaults'; import { getBaselineSeriesForStream, getPadding, transformEventAnnotation, transformFormulaAnnotation, transformIntervalAnnotation, transformSeries, transformTimeseriesAnnotation, } from './transformers'; import { OpacityEnum, StackControlsValue, TIMEGRAIN_TO_TIMESTAMP, TIMESERIES_CONSTANTS, } from '../constants'; import { getDefaultTooltip } from '../utils/tooltip'; import { getPercentFormatter, getTooltipTimeFormatter, getXAxisFormatter, getYAxisFormatter, } from '../utils/formatters'; import { safeParseEChartOptions } from '../utils/safeEChartOptionsParser'; import { mergeCustomEChartOptions } from '../utils/mergeCustomEChartOptions'; const visibleDashPatterns: ([number, number] | 'dashed' | 'dotted')[] = [ 'dashed', 'dotted', [6, 15], // narrow dashed [2, 10], // wide dotted [20, 3], // wide dashed ]; const visibleSymbols = [ 'rect', 'triangle', 'diamond', 'roundRect', 'pin', ] as const; function getSymbolMarker(symbol: string, color: string) { const size = 10; switch (symbol) { case 'circle': return ``; case 'rect': return ``; case 'roundRect': return ``; case 'triangle': return ``; case 'diamond': return ``; case 'pin': return ``; default: return ``; } } export default function transformProps( chartProps: EchartsTimeseriesChartProps, ): TimeseriesChartTransformedProps { const { width, height, filterState, legendState, formData: { echartOptions: _echartOptions, ...formData }, hooks, queriesData, datasource, theme, inContextMenu, emitCrossFilters, legendIndex, } = chartProps; let focusedSeries: string | null = null; const { verboseMap = {}, columnFormats = {}, currencyFormats = {}, currencyCodeColumn, } = datasource; const [queryData] = queriesData; const { data = [], label_map = {}, detected_currency: backendDetectedCurrency, } = queryData as TimeseriesChartDataResponseResult; const dataTypes = getColtypesMapping(queryData); const annotationData = getAnnotationData(chartProps); const { area, annotationLayers, colorScheme, contributionMode, forecastEnabled, groupby, legendOrientation, legendType, legendMargin, legendSort, logAxis, markerEnabled, markerSize, maxMarkerSize = 30, minMarkerSize = 5, metrics, minorSplitLine, minorTicks, onlyTotal, opacity, orientation, percentageThreshold, richTooltip, seriesType, showLegend, showValue, size, colorByPrimaryAxis, sliceId, sortSeriesType, sortSeriesAscending, timeGrainSqla, forceMaxInterval, timeCompare, timeShiftColor, stack, tooltipTimeFormat, tooltipSortByMetric, showTooltipTotal, showTooltipPercentage, truncateXAxis, truncateYAxis, xAxis: xAxisOrig, xAxisBounds, xAxisForceCategorical, xAxisLabelRotation, xAxisLabelInterval, xAxisSort, xAxisSortAsc, xAxisTimeFormat, xAxisNumberFormat, xAxisTitle, xAxisTitleMargin, yAxisBounds, yAxisFormat, currencyFormat, yAxisTitle, yAxisTitleMargin, yAxisTitlePosition, zoomable, stackDimension, }: EchartsTimeseriesFormData = { ...DEFAULT_FORM_DATA, ...formData }; const refs: Refs = {}; const groupBy = ensureIsArray(groupby); const labelMap: { [key: string]: string[] } = Object.entries( label_map, ).reduce((acc, entry) => { if ( entry[1].length > groupBy.length && Array.isArray(timeCompare) && timeCompare.includes(entry[1][0]) ) { entry[1].shift(); } return { ...acc, [entry[0]]: entry[1] }; }, {}); const colorScale = CategoricalColorNamespace.getScale(colorScheme as string); const forecastRebasedData = rebaseForecastDatum(data, verboseMap); let xAxisLabel = getXAxisLabel(chartProps.rawFormData) as string; if ( isPhysicalColumn(chartProps.rawFormData?.x_axis) && isDefined(verboseMap[xAxisLabel]) ) { xAxisLabel = verboseMap[xAxisLabel]; } // Restores the legacy nvd3 "Time-series Percent Change" view: every series // rebased client-side to its percent change from the first point. The // baseline can be re-indexed interactively via the draggable line the // chart component installs. const rebasePercentChange = Boolean( (formData as { rebasePercentChange?: boolean }).rebasePercentChange, ); const rebasedData = rebasePercentChange ? // the same temporal-alias fallback extractSeries applies, so a chart // with no explicit x-axis cannot have its x column rebased as data rebaseToPercentChange(forecastRebasedData, xAxisLabel || DTTM_ALIAS) : forecastRebasedData; const isHorizontal = orientation === OrientationType.Horizontal; const { totalStackedValues, thresholdValues } = extractDataTotalValues( rebasedData, { stack, percentageThreshold, xAxisCol: xAxisLabel, legendState, }, ); const extraMetricLabels = extractExtraMetrics(chartProps.rawFormData).map( getMetricLabel, ); const isMultiSeries = groupBy.length || metrics?.length > 1; const xAxisDataType = dataTypes?.[xAxisLabel] ?? dataTypes?.[xAxisOrig]; const xAxisType = getAxisType( stack, xAxisForceCategorical, xAxisDataType, seriesType, ); const [allRawSeries, sortedTotalValues, minPositiveValue] = extractSeries( rebasedData, { fillNeighborValue: stack && !forecastEnabled ? 0 : undefined, xAxis: xAxisLabel, extraMetricLabels, stack, totalStackedValues, isHorizontal, sortSeriesType, sortSeriesAscending, xAxisSortSeries: isMultiSeries ? xAxisSort : undefined, xAxisSortSeriesAscending: isMultiSeries ? xAxisSortAsc : undefined, xAxisType, }, ); // Dot size by metric (scatter): the size metric's series are excluded from // rendering and instead provide per-point values that scale each marker's // area between minMarkerSize and maxMarkerSize. const sizeMetricLabel = seriesType === EchartsTimeseriesSeriesType.Scatter && size ? getMetricLabel(size) : undefined; const sizeSeriesLabel = isDefined(sizeMetricLabel) ? (verboseMap[sizeMetricLabel!] ?? sizeMetricLabel) : undefined; const valueMetricLabels = ensureIsArray(metrics) .map(getMetricLabel) .map(label => verboseMap[label] ?? label); // When the size metric is also a value metric, the query dedupes them into a // single column, so each point's own value doubles as its size value. const sizeIsValueMetric = isDefined(sizeSeriesLabel) ? valueMetricLabels.includes(sizeSeriesLabel!) : false; // Time-comparison series keep the raw metric label with an `__` // suffix (verbose mapping only applies to base columns, see // rebaseForecastDatum), so a metric's series can be named `