mirror of
https://github.com/apache/superset.git
synced 2026-04-20 16:44:46 +00:00
refactor(monorepo): move superset-ui to superset(stage 2) (#17552)
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
/**
|
||||
* 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 {
|
||||
Annotation,
|
||||
AnnotationData,
|
||||
AnnotationLayer,
|
||||
AnnotationOpacity,
|
||||
AnnotationType,
|
||||
evalExpression,
|
||||
FormulaAnnotationLayer,
|
||||
isRecordAnnotationResult,
|
||||
isTableAnnotationLayer,
|
||||
isTimeseriesAnnotationResult,
|
||||
TimeseriesDataRecord,
|
||||
} from '@superset-ui/core';
|
||||
|
||||
export function evalFormula(
|
||||
formula: FormulaAnnotationLayer,
|
||||
data: TimeseriesDataRecord[],
|
||||
): [Date, number][] {
|
||||
const { value: expression } = formula;
|
||||
|
||||
return data.map(row => [
|
||||
new Date(Number(row.__timestamp)),
|
||||
evalExpression(expression, row.__timestamp as number),
|
||||
]);
|
||||
}
|
||||
|
||||
export function parseAnnotationOpacity(opacity?: AnnotationOpacity): number {
|
||||
switch (opacity) {
|
||||
case AnnotationOpacity.Low:
|
||||
return 0.2;
|
||||
case AnnotationOpacity.Medium:
|
||||
return 0.5;
|
||||
case AnnotationOpacity.High:
|
||||
return 0.8;
|
||||
default:
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
const NATIVE_COLUMN_NAMES = {
|
||||
descriptionColumns: ['long_descr'],
|
||||
intervalEndColumn: 'end_dttm',
|
||||
timeColumn: 'start_dttm',
|
||||
titleColumn: 'short_descr',
|
||||
};
|
||||
|
||||
export function extractRecordAnnotations(
|
||||
annotationLayer: AnnotationLayer,
|
||||
annotationData: AnnotationData,
|
||||
): Annotation[] {
|
||||
const { name } = annotationLayer;
|
||||
const result = annotationData[name];
|
||||
if (isRecordAnnotationResult(result)) {
|
||||
const { records } = result;
|
||||
const {
|
||||
descriptionColumns = [],
|
||||
intervalEndColumn = '',
|
||||
timeColumn = '',
|
||||
titleColumn = '',
|
||||
} = isTableAnnotationLayer(annotationLayer)
|
||||
? annotationLayer
|
||||
: NATIVE_COLUMN_NAMES;
|
||||
|
||||
return records.map(record => ({
|
||||
descriptions: descriptionColumns.map(
|
||||
column => (record[column] || '') as string,
|
||||
) as string[],
|
||||
intervalEnd: (record[intervalEndColumn] || '') as string,
|
||||
time: (record[timeColumn] || '') as string,
|
||||
title: (record[titleColumn] || '') as string,
|
||||
}));
|
||||
}
|
||||
throw new Error('Please rerun the query.');
|
||||
}
|
||||
|
||||
export function formatAnnotationLabel(
|
||||
name?: string,
|
||||
title?: string,
|
||||
descriptions: string[] = [],
|
||||
): string {
|
||||
const labels: string[] = [];
|
||||
const titleLabels: string[] = [];
|
||||
const filteredDescriptions = descriptions.filter(
|
||||
description => !!description,
|
||||
);
|
||||
if (name) titleLabels.push(name);
|
||||
if (title) titleLabels.push(title);
|
||||
if (titleLabels.length > 0) labels.push(titleLabels.join(' - '));
|
||||
if (filteredDescriptions.length > 0)
|
||||
labels.push(filteredDescriptions.join('\n'));
|
||||
return labels.join('\n\n');
|
||||
}
|
||||
|
||||
export function extractAnnotationLabels(
|
||||
layers: AnnotationLayer[],
|
||||
data: AnnotationData,
|
||||
): string[] {
|
||||
const formulaAnnotationLabels = layers
|
||||
.filter(anno => anno.annotationType === AnnotationType.Formula && anno.show)
|
||||
.map(anno => anno.name);
|
||||
const timeseriesAnnotationLabels = layers
|
||||
.filter(
|
||||
anno => anno.annotationType === AnnotationType.Timeseries && anno.show,
|
||||
)
|
||||
.flatMap(anno => {
|
||||
const result = data[anno.name];
|
||||
return isTimeseriesAnnotationResult(result)
|
||||
? result.map(annoSeries => annoSeries.key)
|
||||
: [];
|
||||
});
|
||||
|
||||
return formulaAnnotationLabels.concat(timeseriesAnnotationLabels);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* 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 { validateNumber } from '@superset-ui/core';
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export function parseYAxisBound(
|
||||
bound?: string | number | null,
|
||||
): number | undefined {
|
||||
if (bound === undefined || bound === null || Number.isNaN(Number(bound))) {
|
||||
return undefined;
|
||||
}
|
||||
return Number(bound);
|
||||
}
|
||||
|
||||
export function parseNumbersList(value: string, delim = ';') {
|
||||
if (!value || !value.trim()) return [];
|
||||
return value.split(delim).map(num => {
|
||||
if (validateNumber(num)) throw new Error('All values must be numeric');
|
||||
return Number(num);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
/**
|
||||
* 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 { TimeseriesDataRecord, NumberFormatter } from '@superset-ui/core';
|
||||
import { CallbackDataParams, OptionName } from 'echarts/types/src/util/types';
|
||||
import { TooltipMarker } from 'echarts/types/src/util/format';
|
||||
import {
|
||||
ForecastSeriesContext,
|
||||
ForecastSeriesEnum,
|
||||
ProphetValue,
|
||||
} from '../types';
|
||||
import { sanitizeHtml } from './series';
|
||||
|
||||
const seriesTypeRegex = new RegExp(
|
||||
`(.+)(${ForecastSeriesEnum.ForecastLower}|${ForecastSeriesEnum.ForecastTrend}|${ForecastSeriesEnum.ForecastUpper})$`,
|
||||
);
|
||||
export const extractForecastSeriesContext = (
|
||||
seriesName: OptionName,
|
||||
): ForecastSeriesContext => {
|
||||
const name = seriesName as string;
|
||||
const regexMatch = seriesTypeRegex.exec(name);
|
||||
if (!regexMatch) return { name, type: ForecastSeriesEnum.Observation };
|
||||
return {
|
||||
name: regexMatch[1],
|
||||
type: regexMatch[2] as ForecastSeriesEnum,
|
||||
};
|
||||
};
|
||||
|
||||
export const extractForecastSeriesContexts = (
|
||||
seriesNames: string[],
|
||||
): { [key: string]: ForecastSeriesEnum[] } =>
|
||||
seriesNames.reduce((agg, name) => {
|
||||
const context = extractForecastSeriesContext(name);
|
||||
const currentContexts = agg[context.name] || [];
|
||||
currentContexts.push(context.type);
|
||||
return { ...agg, [context.name]: currentContexts };
|
||||
}, {} as { [key: string]: ForecastSeriesEnum[] });
|
||||
|
||||
export const extractProphetValuesFromTooltipParams = (
|
||||
params: (CallbackDataParams & { seriesId: string })[],
|
||||
): Record<string, ProphetValue> => {
|
||||
const values: Record<string, ProphetValue> = {};
|
||||
params.forEach(param => {
|
||||
const { marker, seriesId, value } = param;
|
||||
const context = extractForecastSeriesContext(seriesId);
|
||||
const numericValue = (value as [Date, number])[1];
|
||||
if (numericValue) {
|
||||
if (!(context.name in values))
|
||||
values[context.name] = {
|
||||
marker: marker || '',
|
||||
};
|
||||
const prophetValues = values[context.name];
|
||||
if (context.type === ForecastSeriesEnum.Observation)
|
||||
prophetValues.observation = numericValue;
|
||||
if (context.type === ForecastSeriesEnum.ForecastTrend)
|
||||
prophetValues.forecastTrend = numericValue;
|
||||
if (context.type === ForecastSeriesEnum.ForecastLower)
|
||||
prophetValues.forecastLower = numericValue;
|
||||
if (context.type === ForecastSeriesEnum.ForecastUpper)
|
||||
prophetValues.forecastUpper = numericValue;
|
||||
}
|
||||
});
|
||||
return values;
|
||||
};
|
||||
|
||||
export const formatProphetTooltipSeries = ({
|
||||
seriesName,
|
||||
observation,
|
||||
forecastTrend,
|
||||
forecastLower,
|
||||
forecastUpper,
|
||||
marker,
|
||||
formatter,
|
||||
}: ProphetValue & {
|
||||
seriesName: string;
|
||||
marker: TooltipMarker;
|
||||
formatter: NumberFormatter;
|
||||
}): string => {
|
||||
let row = `${marker}${sanitizeHtml(seriesName)}: `;
|
||||
let isObservation = false;
|
||||
if (observation) {
|
||||
isObservation = true;
|
||||
row += `${formatter(observation)}`;
|
||||
}
|
||||
if (forecastTrend) {
|
||||
if (isObservation) row += ', ';
|
||||
row += `ŷ = ${formatter(forecastTrend)}`;
|
||||
}
|
||||
if (forecastLower && forecastUpper)
|
||||
// the lower bound needs to be added to the upper bound
|
||||
row = `${row.trim()} (${formatter(forecastLower)}, ${formatter(
|
||||
forecastLower + forecastUpper,
|
||||
)})`;
|
||||
return `${row.trim()}`;
|
||||
};
|
||||
|
||||
export function rebaseTimeseriesDatum(
|
||||
data: TimeseriesDataRecord[],
|
||||
verboseMap: Record<string, string> = {},
|
||||
) {
|
||||
const keys = data.length > 0 ? Object.keys(data[0]) : [];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return data.map(row => {
|
||||
const newRow: TimeseriesDataRecord = { __timestamp: '' };
|
||||
keys.forEach(key => {
|
||||
const forecastContext = extractForecastSeriesContext(key);
|
||||
const lowerKey = `${forecastContext.name}${ForecastSeriesEnum.ForecastLower}`;
|
||||
let value = row[key] as number;
|
||||
if (
|
||||
forecastContext.type === ForecastSeriesEnum.ForecastUpper &&
|
||||
keys.includes(lowerKey) &&
|
||||
value !== null &&
|
||||
row[lowerKey] !== null
|
||||
) {
|
||||
value -= row[lowerKey] as number;
|
||||
}
|
||||
const newKey =
|
||||
key !== '__timestamp' && verboseMap[key] ? verboseMap[key] : key;
|
||||
newRow[newKey] = value;
|
||||
});
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return newRow;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
/**
|
||||
* 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 {
|
||||
ChartDataResponseResult,
|
||||
DataRecord,
|
||||
DataRecordValue,
|
||||
ensureIsArray,
|
||||
GenericDataType,
|
||||
NumberFormatter,
|
||||
TimeFormatter,
|
||||
TimeseriesDataRecord,
|
||||
} from '@superset-ui/core';
|
||||
import { format, LegendComponentOption, SeriesOption } from 'echarts';
|
||||
import { NULL_STRING, TIMESERIES_CONSTANTS } from '../constants';
|
||||
import { LegendOrientation, LegendType } from '../types';
|
||||
import { defaultLegendPadding } from '../defaults';
|
||||
|
||||
function isDefined<T>(value: T | undefined | null): boolean {
|
||||
return value !== undefined && value !== null;
|
||||
}
|
||||
|
||||
export function extractTimeseriesSeries(
|
||||
data: TimeseriesDataRecord[],
|
||||
opts: { fillNeighborValue?: number } = {},
|
||||
): SeriesOption[] {
|
||||
const { fillNeighborValue } = opts;
|
||||
if (data.length === 0) return [];
|
||||
const rows: TimeseriesDataRecord[] = data.map(datum => ({
|
||||
...datum,
|
||||
__timestamp:
|
||||
datum.__timestamp || datum.__timestamp === 0
|
||||
? new Date(datum.__timestamp)
|
||||
: null,
|
||||
}));
|
||||
|
||||
return Object.keys(rows[0])
|
||||
.filter(key => key !== '__timestamp')
|
||||
.map(key => ({
|
||||
id: key,
|
||||
name: key,
|
||||
data: rows.map((row, idx) => {
|
||||
const isNextToDefinedValue =
|
||||
isDefined(rows[idx - 1]?.[key]) || isDefined(rows[idx + 1]?.[key]);
|
||||
return [
|
||||
row.__timestamp,
|
||||
!isDefined(row[key]) &&
|
||||
isNextToDefinedValue &&
|
||||
fillNeighborValue !== undefined
|
||||
? fillNeighborValue
|
||||
: row[key],
|
||||
];
|
||||
}),
|
||||
}));
|
||||
}
|
||||
|
||||
export function formatSeriesName(
|
||||
name: DataRecordValue | undefined,
|
||||
{
|
||||
numberFormatter,
|
||||
timeFormatter,
|
||||
coltype,
|
||||
}: {
|
||||
numberFormatter?: NumberFormatter;
|
||||
timeFormatter?: TimeFormatter;
|
||||
coltype?: GenericDataType;
|
||||
} = {},
|
||||
): string {
|
||||
if (name === undefined || name === null) {
|
||||
return NULL_STRING;
|
||||
}
|
||||
if (typeof name === 'number') {
|
||||
return numberFormatter ? numberFormatter(name) : name.toString();
|
||||
}
|
||||
if (typeof name === 'boolean') {
|
||||
return name.toString();
|
||||
}
|
||||
if (name instanceof Date || coltype === GenericDataType.TEMPORAL) {
|
||||
const d = name instanceof Date ? name : new Date(name);
|
||||
|
||||
return timeFormatter ? timeFormatter(d) : d.toISOString();
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
export const getColtypesMapping = ({
|
||||
coltypes = [],
|
||||
colnames = [],
|
||||
}: ChartDataResponseResult): Record<string, GenericDataType> =>
|
||||
colnames.reduce(
|
||||
(accumulator, item, index) => ({ ...accumulator, [item]: coltypes[index] }),
|
||||
{},
|
||||
);
|
||||
|
||||
export function extractGroupbyLabel({
|
||||
datum = {},
|
||||
groupby,
|
||||
numberFormatter,
|
||||
timeFormatter,
|
||||
coltypeMapping = {},
|
||||
}: {
|
||||
datum?: DataRecord;
|
||||
groupby?: string[] | null;
|
||||
numberFormatter?: NumberFormatter;
|
||||
timeFormatter?: TimeFormatter;
|
||||
coltypeMapping: Record<string, GenericDataType>;
|
||||
}): string {
|
||||
return ensureIsArray(groupby)
|
||||
.map(val =>
|
||||
formatSeriesName(datum[val], {
|
||||
numberFormatter,
|
||||
timeFormatter,
|
||||
...(coltypeMapping[val] && { coltype: coltypeMapping[val] }),
|
||||
}),
|
||||
)
|
||||
.join(', ');
|
||||
}
|
||||
|
||||
export function getLegendProps(
|
||||
type: LegendType,
|
||||
orientation: LegendOrientation,
|
||||
show: boolean,
|
||||
zoomable = false,
|
||||
): LegendComponentOption | LegendComponentOption[] {
|
||||
const legend: LegendComponentOption | LegendComponentOption[] = {
|
||||
orient: [LegendOrientation.Top, LegendOrientation.Bottom].includes(
|
||||
orientation,
|
||||
)
|
||||
? 'horizontal'
|
||||
: 'vertical',
|
||||
show,
|
||||
type,
|
||||
};
|
||||
switch (orientation) {
|
||||
case LegendOrientation.Left:
|
||||
legend.left = 0;
|
||||
break;
|
||||
case LegendOrientation.Right:
|
||||
legend.right = 0;
|
||||
legend.top = zoomable ? TIMESERIES_CONSTANTS.legendRightTopOffset : 0;
|
||||
break;
|
||||
case LegendOrientation.Bottom:
|
||||
legend.bottom = 0;
|
||||
break;
|
||||
case LegendOrientation.Top:
|
||||
default:
|
||||
legend.top = 0;
|
||||
legend.right = zoomable ? TIMESERIES_CONSTANTS.legendTopRightOffset : 0;
|
||||
break;
|
||||
}
|
||||
return legend;
|
||||
}
|
||||
|
||||
export function getChartPadding(
|
||||
show: boolean,
|
||||
orientation: LegendOrientation,
|
||||
margin?: string | number | null,
|
||||
padding?: { top?: number; bottom?: number; left?: number; right?: number },
|
||||
): {
|
||||
bottom: number;
|
||||
left: number;
|
||||
right: number;
|
||||
top: number;
|
||||
} {
|
||||
let legendMargin;
|
||||
if (!show) {
|
||||
legendMargin = 0;
|
||||
} else if (
|
||||
margin === null ||
|
||||
margin === undefined ||
|
||||
typeof margin === 'string'
|
||||
) {
|
||||
legendMargin = defaultLegendPadding[orientation];
|
||||
} else {
|
||||
legendMargin = margin;
|
||||
}
|
||||
|
||||
const { bottom = 0, left = 0, right = 0, top = 0 } = padding || {};
|
||||
return {
|
||||
left: left + (orientation === LegendOrientation.Left ? legendMargin : 0),
|
||||
right: right + (orientation === LegendOrientation.Right ? legendMargin : 0),
|
||||
top: top + (orientation === LegendOrientation.Top ? legendMargin : 0),
|
||||
bottom:
|
||||
bottom + (orientation === LegendOrientation.Bottom ? legendMargin : 0),
|
||||
};
|
||||
}
|
||||
|
||||
export function dedupSeries(series: SeriesOption[]): SeriesOption[] {
|
||||
const counter = new Map<string, number>();
|
||||
return series.map(row => {
|
||||
let { id } = row;
|
||||
if (id === undefined) return row;
|
||||
id = String(id);
|
||||
const count = counter.get(id) || 0;
|
||||
const suffix = count > 0 ? ` (${count})` : '';
|
||||
counter.set(id, count + 1);
|
||||
return {
|
||||
...row,
|
||||
id: `${id}${suffix}`,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function sanitizeHtml(text: string): string {
|
||||
return format.encodeHTML(text);
|
||||
}
|
||||
|
||||
// TODO: Better use other method to maintain this state
|
||||
export const currentSeries = {
|
||||
name: '',
|
||||
legend: '',
|
||||
};
|
||||
Reference in New Issue
Block a user