mirror of
https://github.com/apache/superset.git
synced 2026-04-21 00:54:44 +00:00
408 lines
12 KiB
TypeScript
408 lines
12 KiB
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 { t } from '@apache-superset/core';
|
|
import {
|
|
extractTimegrain,
|
|
getNumberFormatter,
|
|
NumberFormats,
|
|
getMetricLabel,
|
|
getXAxisLabel,
|
|
Metric,
|
|
getValueFormatter,
|
|
tooltipHtml,
|
|
} from '@superset-ui/core';
|
|
import { GenericDataType } from '@apache-superset/core/api/core';
|
|
import { EChartsCoreOption, graphic } from 'echarts/core';
|
|
import { aggregationChoices } from '@superset-ui/chart-controls';
|
|
import { TIMESERIES_CONSTANTS } from '../../constants';
|
|
import { getXAxisFormatter } from '../../utils/formatters';
|
|
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,
|
|
);
|
|
|
|
// Client-side aggregation function using shared aggregationChoices
|
|
function computeClientSideAggregation(
|
|
data: [number | null, number | null][],
|
|
aggregation: string | undefined | null,
|
|
): number | null {
|
|
if (!data.length) return null;
|
|
|
|
// Find the aggregation method, handling case variations
|
|
const methodKey = Object.keys(aggregationChoices).find(
|
|
key => key.toLowerCase() === (aggregation || '').toLowerCase(),
|
|
);
|
|
|
|
// Use the compute method from aggregationChoices, fallback to LAST_VALUE
|
|
const selectedMethod = methodKey
|
|
? aggregationChoices[methodKey as keyof typeof aggregationChoices]
|
|
: aggregationChoices.LAST_VALUE;
|
|
|
|
// Extract values from tuple array and filter out nulls
|
|
const values = data
|
|
.map(([, value]) => value)
|
|
.filter((v): v is number => v !== null);
|
|
|
|
return selectedMethod.compute(values);
|
|
}
|
|
|
|
export default function transformProps(
|
|
chartProps: BigNumberWithTrendlineChartProps,
|
|
): BigNumberVizProps {
|
|
const {
|
|
width,
|
|
height,
|
|
queriesData,
|
|
formData,
|
|
rawFormData,
|
|
hooks,
|
|
inContextMenu,
|
|
theme,
|
|
datasource: {
|
|
currencyFormats = {},
|
|
columnFormats = {},
|
|
currencyCodeColumn,
|
|
},
|
|
} = chartProps;
|
|
const {
|
|
colorPicker,
|
|
compareLag: compareLag_,
|
|
compareSuffix = '',
|
|
timeFormat,
|
|
metricNameFontSize,
|
|
headerFontSize,
|
|
metric = 'value',
|
|
showTimestamp,
|
|
showTrendLine,
|
|
subtitle = '',
|
|
subtitleFontSize,
|
|
aggregation,
|
|
startYAxisAtZero,
|
|
subheader = '',
|
|
subheaderFontSize,
|
|
forceTimestampFormatting,
|
|
yAxisFormat,
|
|
currencyFormat,
|
|
timeRangeFixed,
|
|
showXAxis = false,
|
|
showXAxisMinMaxLabels = false,
|
|
showYAxis = false,
|
|
showYAxisMinMaxLabels = false,
|
|
} = formData;
|
|
const granularity = extractTimegrain(rawFormData);
|
|
const {
|
|
data = [],
|
|
colnames = [],
|
|
coltypes = [],
|
|
from_dttm: fromDatetime,
|
|
to_dttm: toDatetime,
|
|
detected_currency: detectedCurrency,
|
|
} = 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 (sortedData.length > 0) {
|
|
timestamp = sortedData[0][0];
|
|
|
|
// Raw aggregation uses server-side data, all others use client-side
|
|
if (aggregation === 'raw' && hasAggregatedData && aggregatedData) {
|
|
// Use server-side aggregation for raw
|
|
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;
|
|
}
|
|
} else {
|
|
// Use client-side aggregation for all other methods
|
|
bigNumber = computeClientSideAggregation(sortedData, aggregation);
|
|
}
|
|
|
|
// Handle null bigNumber case
|
|
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 compareFromValue = sortedData[compareIndex][1];
|
|
const compareToValue = sortedData[0][1];
|
|
// compare values must both be non-nulls
|
|
if (compareToValue !== null && compareFromValue !== null) {
|
|
percentChange = compareFromValue
|
|
? (Number(compareToValue) - compareFromValue) /
|
|
Math.abs(compareFromValue)
|
|
: 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,
|
|
);
|
|
|
|
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 numberFormatter = getValueFormatter(
|
|
metric,
|
|
currencyFormats,
|
|
columnFormats,
|
|
metricEntry?.d3format || yAxisFormat,
|
|
currencyFormat,
|
|
undefined,
|
|
data,
|
|
currencyCodeColumn,
|
|
detectedCurrency,
|
|
);
|
|
const xAxisFormatter = getXAxisFormatter(timeFormat);
|
|
const yAxisFormatter =
|
|
metricColtype === GenericDataType.Temporal ||
|
|
metricColtype === GenericDataType.String ||
|
|
forceTimestampFormatting
|
|
? formatTime
|
|
: numberFormatter;
|
|
|
|
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.colorBgContainer,
|
|
},
|
|
]),
|
|
},
|
|
},
|
|
],
|
|
xAxis: {
|
|
type: 'time',
|
|
show: showXAxis,
|
|
splitLine: {
|
|
show: false,
|
|
},
|
|
axisLabel: {
|
|
hideOverlap: true,
|
|
formatter: xAxisFormatter,
|
|
alignMinLabel: 'left',
|
|
alignMaxLabel: 'right',
|
|
showMinLabel: showXAxisMinMaxLabels,
|
|
showMaxLabel: showXAxisMinMaxLabels,
|
|
},
|
|
},
|
|
yAxis: {
|
|
type: 'value',
|
|
show: showYAxis,
|
|
scale: !startYAxisAtZero,
|
|
splitLine: {
|
|
show: false,
|
|
},
|
|
axisLabel: {
|
|
hideOverlap: true,
|
|
formatter: yAxisFormatter,
|
|
showMinLabel: showYAxisMinMaxLabels,
|
|
showMaxLabel: showYAxisMinMaxLabels,
|
|
},
|
|
},
|
|
grid:
|
|
showXAxis || showYAxis
|
|
? {
|
|
containLabel: true,
|
|
bottom: TIMESERIES_CONSTANTS.gridOffsetBottom,
|
|
left: TIMESERIES_CONSTANTS.gridOffsetLeft,
|
|
right: TIMESERIES_CONSTANTS.gridOffsetRight,
|
|
top: TIMESERIES_CONSTANTS.gridOffsetTop,
|
|
}
|
|
: {
|
|
bottom: 0,
|
|
left: 0,
|
|
right: 0,
|
|
top: 0,
|
|
},
|
|
tooltip: {
|
|
...getDefaultTooltip(refs),
|
|
show: !inContextMenu,
|
|
trigger: 'axis',
|
|
formatter: (params: { data: TimeSeriesDatum }[]) =>
|
|
tooltipHtml(
|
|
[
|
|
[
|
|
metricName,
|
|
params[0].data[1] === null
|
|
? t('N/A')
|
|
: yAxisFormatter.format(params[0].data[1]),
|
|
],
|
|
],
|
|
formatTime(params[0].data[0]),
|
|
),
|
|
},
|
|
aria: {
|
|
enabled: true,
|
|
label: {
|
|
description: `Big number visualization ${subheader}`,
|
|
},
|
|
},
|
|
useUTC: true,
|
|
}
|
|
: {};
|
|
|
|
const { onContextMenu } = hooks;
|
|
|
|
return {
|
|
width,
|
|
height,
|
|
bigNumber,
|
|
// @ts-ignore
|
|
bigNumberFallback,
|
|
className,
|
|
headerFormatter: yAxisFormatter,
|
|
formatTime,
|
|
formData,
|
|
metricName: originalLabel,
|
|
showMetricName,
|
|
metricNameFontSize,
|
|
headerFontSize,
|
|
subtitleFontSize,
|
|
subtitle,
|
|
subheaderFontSize,
|
|
mainColor,
|
|
showTimestamp,
|
|
showTrendLine,
|
|
startYAxisAtZero,
|
|
subheader: formattedSubheader,
|
|
timestamp,
|
|
trendLineData,
|
|
echartOptions,
|
|
onContextMenu,
|
|
xValueFormatter: formatTime,
|
|
refs,
|
|
};
|
|
}
|