/**
* 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 `