superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/transformProps.ts (242 lines of code) (raw):
/**
* 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 {
extractTimegrain,
getNumberFormatter,
NumberFormats,
GenericDataType,
getMetricLabel,
getXAxisLabel,
Metric,
getValueFormatter,
t,
tooltipHtml,
} from '@superset-ui/core';
import { EChartsCoreOption, graphic } from 'echarts/core';
import {
BigNumberVizProps,
BigNumberDatum,
BigNumberWithTrendlineChartProps,
TimeSeriesDatum,
} from '../types';
import { getDateFormatter, parseMetricValue, getOriginalLabel } from '../utils';
import { getDefaultTooltip } from '../../utils/tooltip';
import { Refs } from '../../types';
const formatPercentChange = getNumberFormatter(
NumberFormats.PERCENT_SIGNED_1_POINT,
);
export default function transformProps(
chartProps: BigNumberWithTrendlineChartProps,
): BigNumberVizProps {
const {
width,
height,
queriesData,
formData,
rawFormData,
theme,
hooks,
inContextMenu,
datasource: { currencyFormats = {}, columnFormats = {} },
} = chartProps;
const {
colorPicker,
compareLag: compareLag_,
compareSuffix = '',
timeFormat,
metricNameFontSize,
headerFontSize,
metric = 'value',
showTimestamp,
showTrendLine,
subtitle = '',
subtitleFontSize,
aggregation,
startYAxisAtZero,
subheader = '',
subheaderFontSize,
forceTimestampFormatting,
yAxisFormat,
currencyFormat,
timeRangeFixed,
} = formData;
const granularity = extractTimegrain(rawFormData);
const {
data = [],
colnames = [],
coltypes = [],
from_dttm: fromDatetime,
to_dttm: toDatetime,
} = queriesData[0];
const aggregatedQueryData = queriesData.length > 1 ? queriesData[1] : null;
const hasAggregatedData =
aggregatedQueryData?.data &&
aggregatedQueryData.data.length > 0 &&
aggregation !== 'LAST_VALUE';
const aggregatedData = hasAggregatedData ? aggregatedQueryData.data[0] : null;
const refs: Refs = {};
const metricName = getMetricLabel(metric);
const metrics = chartProps.datasource?.metrics || [];
const originalLabel = getOriginalLabel(metric, metrics);
const showMetricName = chartProps.rawFormData?.show_metric_name ?? false;
const compareLag = Number(compareLag_) || 0;
let formattedSubheader = subheader;
const { r, g, b } = colorPicker;
const mainColor = `rgb(${r}, ${g}, ${b})`;
const xAxisLabel = getXAxisLabel(rawFormData) as string;
let trendLineData: TimeSeriesDatum[] | undefined;
let percentChange = 0;
let bigNumber = data.length === 0 ? null : data[0][metricName];
let timestamp = data.length === 0 ? null : data[0][xAxisLabel];
let bigNumberFallback = null;
let sortedData: [number | null, number | null][] = [];
if (data.length > 0) {
sortedData = (data as BigNumberDatum[])
.map(
d =>
[d[xAxisLabel], parseMetricValue(d[metricName])] as [
number | null,
number | null,
],
)
// sort in time descending order
.sort((a, b) => (a[0] !== null && b[0] !== null ? b[0] - a[0] : 0));
}
if (hasAggregatedData && aggregatedData) {
if (
aggregatedData[metricName] !== null &&
aggregatedData[metricName] !== undefined
) {
bigNumber = aggregatedData[metricName];
} else {
const metricKeys = Object.keys(aggregatedData).filter(
key =>
key !== xAxisLabel &&
aggregatedData[key] !== null &&
typeof aggregatedData[key] === 'number',
);
bigNumber = metricKeys.length > 0 ? aggregatedData[metricKeys[0]] : null;
}
timestamp = sortedData.length > 0 ? sortedData[0][0] : null;
} else if (sortedData.length > 0) {
bigNumber = sortedData[0][1];
timestamp = sortedData[0][0];
if (bigNumber === null) {
bigNumberFallback = sortedData.find(d => d[1] !== null);
bigNumber = bigNumberFallback ? bigNumberFallback[1] : null;
timestamp = bigNumberFallback ? bigNumberFallback[0] : null;
}
}
if (compareLag > 0 && sortedData.length > 0) {
const compareIndex = compareLag;
if (compareIndex < sortedData.length) {
const compareValue = sortedData[compareIndex][1];
// compare values must both be non-nulls
if (bigNumber !== null && compareValue !== null) {
percentChange = compareValue
? (Number(bigNumber) - compareValue) / Math.abs(compareValue)
: 0;
formattedSubheader = `${formatPercentChange(
percentChange,
)} ${compareSuffix}`;
}
}
}
if (data.length > 0) {
const reversedData = [...sortedData].reverse();
// @ts-ignore
trendLineData = showTrendLine ? reversedData : undefined;
}
let className = '';
if (percentChange > 0) {
className = 'positive';
} else if (percentChange < 0) {
className = 'negative';
}
const metricColtypeIndex = colnames.findIndex(name => name === metricName);
const metricColtype =
metricColtypeIndex > -1 ? coltypes[metricColtypeIndex] : null;
let metricEntry: Metric | undefined;
if (chartProps.datasource?.metrics) {
metricEntry = chartProps.datasource.metrics.find(
metricEntry => metricEntry.metric_name === metric,
);
}
const formatTime = getDateFormatter(
timeFormat,
granularity,
metricEntry?.d3format,
);
const numberFormatter = getValueFormatter(
metric,
currencyFormats,
columnFormats,
yAxisFormat,
currencyFormat,
);
const headerFormatter =
metricColtype === GenericDataType.Temporal ||
metricColtype === GenericDataType.String ||
forceTimestampFormatting
? formatTime
: numberFormatter;
if (trendLineData && timeRangeFixed && fromDatetime) {
const toDatetimeOrToday = toDatetime ?? Date.now();
if (!trendLineData[0][0] || trendLineData[0][0] > fromDatetime) {
trendLineData.unshift([fromDatetime, null]);
}
if (
!trendLineData[trendLineData.length - 1][0] ||
trendLineData[trendLineData.length - 1][0]! < toDatetimeOrToday
) {
trendLineData.push([toDatetimeOrToday, null]);
}
}
const echartOptions: EChartsCoreOption = trendLineData
? {
series: [
{
data: trendLineData,
type: 'line',
smooth: true,
symbol: 'circle',
symbolSize: 10,
showSymbol: false,
color: mainColor,
areaStyle: {
color: new graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: mainColor,
},
{
offset: 1,
color: theme.colors.grayscale.light5,
},
]),
},
},
],
xAxis: {
min: trendLineData[0][0],
max: trendLineData[trendLineData.length - 1][0],
show: false,
type: 'value',
},
yAxis: {
scale: !startYAxisAtZero,
show: false,
},
grid: {
left: 0,
right: 0,
top: 0,
bottom: 0,
},
tooltip: {
...getDefaultTooltip(refs),
show: !inContextMenu,
trigger: 'axis',
formatter: (params: { data: TimeSeriesDatum }[]) =>
tooltipHtml(
[
[
metricName,
params[0].data[1] === null
? t('N/A')
: headerFormatter.format(params[0].data[1]),
],
],
formatTime(params[0].data[0]),
),
},
aria: {
enabled: true,
label: {
description: `Big number visualization ${subheader}`,
},
},
}
: {};
const { onContextMenu } = hooks;
return {
width,
height,
bigNumber,
// @ts-ignore
bigNumberFallback,
className,
headerFormatter,
formatTime,
formData,
metricName: originalLabel,
showMetricName,
metricNameFontSize,
headerFontSize,
subtitleFontSize,
subtitle,
subheaderFontSize,
mainColor,
showTimestamp,
showTrendLine,
startYAxisAtZero,
subheader: formattedSubheader,
timestamp,
trendLineData,
echartOptions,
onContextMenu,
xValueFormatter: formatTime,
refs,
};
}