278 lines
7.4 KiB
TypeScript
278 lines
7.4 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 {
|
|
extractTimegrain,
|
|
getNumberFormatter,
|
|
NumberFormats,
|
|
GenericDataType,
|
|
getMetricLabel,
|
|
t,
|
|
smartDateVerboseFormatter,
|
|
NumberFormatter,
|
|
TimeFormatter,
|
|
getXAxisLabel,
|
|
} from '@superset-ui/core';
|
|
import { EChartsCoreOption, graphic } from 'echarts';
|
|
import {
|
|
BigNumberVizProps,
|
|
BigNumberDatum,
|
|
BigNumberWithTrendlineChartProps,
|
|
TimeSeriesDatum,
|
|
} from '../types';
|
|
import { getDateFormatter, parseMetricValue } from '../utils';
|
|
import { getDefaultTooltip } from '../../utils/tooltip';
|
|
import { Refs } from '../../types';
|
|
|
|
const defaultNumberFormatter = getNumberFormatter();
|
|
export function renderTooltipFactory(
|
|
formatDate: TimeFormatter = smartDateVerboseFormatter,
|
|
formatValue: NumberFormatter | TimeFormatter = defaultNumberFormatter,
|
|
) {
|
|
return function renderTooltip(params: { data: TimeSeriesDatum }[]) {
|
|
return `
|
|
${formatDate(params[0].data[0])}
|
|
<br />
|
|
<strong>
|
|
${
|
|
params[0].data[1] === null ? t('N/A') : formatValue(params[0].data[1])
|
|
}
|
|
</strong>
|
|
`;
|
|
};
|
|
}
|
|
|
|
const formatPercentChange = getNumberFormatter(
|
|
NumberFormats.PERCENT_SIGNED_1_POINT,
|
|
);
|
|
|
|
export default function transformProps(
|
|
chartProps: BigNumberWithTrendlineChartProps,
|
|
): BigNumberVizProps {
|
|
const {
|
|
width,
|
|
height,
|
|
queriesData,
|
|
formData,
|
|
rawFormData,
|
|
theme,
|
|
hooks,
|
|
inContextMenu,
|
|
} = chartProps;
|
|
const {
|
|
colorPicker,
|
|
compareLag: compareLag_,
|
|
compareSuffix = '',
|
|
timeFormat,
|
|
headerFontSize,
|
|
metric = 'value',
|
|
showTimestamp,
|
|
showTrendLine,
|
|
startYAxisAtZero,
|
|
subheader = '',
|
|
subheaderFontSize,
|
|
forceTimestampFormatting,
|
|
yAxisFormat,
|
|
timeRangeFixed,
|
|
} = formData;
|
|
const granularity = extractTimegrain(rawFormData);
|
|
const {
|
|
data = [],
|
|
colnames = [],
|
|
coltypes = [],
|
|
from_dttm: fromDatetime,
|
|
to_dttm: toDatetime,
|
|
} = queriesData[0];
|
|
const refs: Refs = {};
|
|
const metricName = getMetricLabel(metric);
|
|
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;
|
|
|
|
const metricColtypeIndex = colnames.findIndex(name => name === metricName);
|
|
const metricColtype =
|
|
metricColtypeIndex > -1 ? coltypes[metricColtypeIndex] : null;
|
|
|
|
if (data.length > 0) {
|
|
const sortedData = (data as BigNumberDatum[])
|
|
.map(d => [d[xAxisLabel], parseMetricValue(d[metricName])])
|
|
// sort in time descending order
|
|
.sort((a, b) => (a[0] !== null && b[0] !== null ? b[0] - a[0] : 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) {
|
|
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
|
|
? (bigNumber - compareValue) / Math.abs(compareValue)
|
|
: 0;
|
|
formattedSubheader = `${formatPercentChange(
|
|
percentChange,
|
|
)} ${compareSuffix}`;
|
|
}
|
|
}
|
|
}
|
|
sortedData.reverse();
|
|
// @ts-ignore
|
|
trendLineData = showTrendLine ? sortedData : undefined;
|
|
}
|
|
|
|
let className = '';
|
|
if (percentChange > 0) {
|
|
className = 'positive';
|
|
} else if (percentChange < 0) {
|
|
className = 'negative';
|
|
}
|
|
|
|
let metricEntry;
|
|
if (chartProps.datasource?.metrics) {
|
|
metricEntry = chartProps.datasource.metrics.find(
|
|
metricEntry => metricEntry.metric_name === metric,
|
|
);
|
|
}
|
|
|
|
const formatTime = getDateFormatter(
|
|
timeFormat,
|
|
granularity,
|
|
metricEntry?.d3format,
|
|
);
|
|
|
|
const headerFormatter =
|
|
metricColtype === GenericDataType.TEMPORAL ||
|
|
metricColtype === GenericDataType.STRING ||
|
|
forceTimestampFormatting
|
|
? formatTime
|
|
: getNumberFormatter(yAxisFormat ?? metricEntry?.d3format ?? undefined);
|
|
|
|
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: renderTooltipFactory(formatTime, headerFormatter),
|
|
},
|
|
aria: {
|
|
enabled: true,
|
|
label: {
|
|
description: `Big number visualization ${subheader}`,
|
|
},
|
|
},
|
|
}
|
|
: {};
|
|
|
|
const { onContextMenu } = hooks;
|
|
|
|
return {
|
|
width,
|
|
height,
|
|
bigNumber,
|
|
// @ts-ignore
|
|
bigNumberFallback,
|
|
className,
|
|
headerFormatter,
|
|
formatTime,
|
|
formData,
|
|
headerFontSize,
|
|
subheaderFontSize,
|
|
mainColor,
|
|
showTimestamp,
|
|
showTrendLine,
|
|
startYAxisAtZero,
|
|
subheader: formattedSubheader,
|
|
timestamp,
|
|
trendLineData,
|
|
echartOptions,
|
|
onContextMenu,
|
|
xValueFormatter: formatTime,
|
|
refs,
|
|
};
|
|
}
|