mirror of
https://github.com/apache/superset.git
synced 2026-04-20 16:44:46 +00:00
* feat(echarts): [feature-parity] support extra control * add extra control for plugin * refactor: extract ExtraControl * fix: lint * fix some problems
321 lines
8.4 KiB
TypeScript
321 lines
8.4 KiB
TypeScript
/* 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,
|
|
DTTM_ALIAS,
|
|
ensureIsArray,
|
|
GenericDataType,
|
|
NumberFormatter,
|
|
TimeFormatter,
|
|
} from '@superset-ui/core';
|
|
import { format, LegendComponentOption, SeriesOption } from 'echarts';
|
|
import {
|
|
AreaChartExtraControlsValue,
|
|
NULL_STRING,
|
|
TIMESERIES_CONSTANTS,
|
|
} from '../constants';
|
|
import { LegendOrientation, LegendType, StackType } from '../types';
|
|
import { defaultLegendPadding } from '../defaults';
|
|
|
|
function isDefined<T>(value: T | undefined | null): boolean {
|
|
return value !== undefined && value !== null;
|
|
}
|
|
|
|
export function extractDataTotalValues(
|
|
data: DataRecord[],
|
|
opts: {
|
|
stack: StackType;
|
|
percentageThreshold: number;
|
|
xAxisCol: string;
|
|
},
|
|
): {
|
|
totalStackedValues: number[];
|
|
thresholdValues: number[];
|
|
} {
|
|
const totalStackedValues: number[] = [];
|
|
const thresholdValues: number[] = [];
|
|
const { stack, percentageThreshold, xAxisCol } = opts;
|
|
if (stack) {
|
|
data.forEach(datum => {
|
|
const values = Object.keys(datum).reduce((prev, curr) => {
|
|
if (curr === xAxisCol) {
|
|
return prev;
|
|
}
|
|
const value = datum[curr] || 0;
|
|
return prev + (value as number);
|
|
}, 0);
|
|
totalStackedValues.push(values);
|
|
thresholdValues.push(((percentageThreshold || 0) / 100) * values);
|
|
});
|
|
}
|
|
return {
|
|
totalStackedValues,
|
|
thresholdValues,
|
|
};
|
|
}
|
|
|
|
export function extractShowValueIndexes(
|
|
series: SeriesOption[],
|
|
opts: {
|
|
stack: StackType;
|
|
},
|
|
): number[] {
|
|
const showValueIndexes: number[] = [];
|
|
if (opts.stack) {
|
|
series.forEach((entry, seriesIndex) => {
|
|
const { data = [] } = entry;
|
|
(data as [any, number][]).forEach((datum, dataIndex) => {
|
|
if (datum[1] !== null) {
|
|
showValueIndexes[dataIndex] = seriesIndex;
|
|
}
|
|
});
|
|
});
|
|
}
|
|
return showValueIndexes;
|
|
}
|
|
|
|
export function extractSeries(
|
|
data: DataRecord[],
|
|
opts: {
|
|
fillNeighborValue?: number;
|
|
xAxis?: string;
|
|
removeNulls?: boolean;
|
|
stack?: StackType;
|
|
totalStackedValues?: number[];
|
|
isHorizontal?: boolean;
|
|
} = {},
|
|
): SeriesOption[] {
|
|
const {
|
|
fillNeighborValue,
|
|
xAxis = DTTM_ALIAS,
|
|
removeNulls = false,
|
|
stack = false,
|
|
totalStackedValues = [],
|
|
isHorizontal = false,
|
|
} = opts;
|
|
if (data.length === 0) return [];
|
|
const rows: DataRecord[] = data.map(datum => ({
|
|
...datum,
|
|
[xAxis]: datum[xAxis],
|
|
}));
|
|
|
|
return Object.keys(rows[0])
|
|
.filter(key => key !== xAxis && key !== DTTM_ALIAS)
|
|
.map(key => ({
|
|
id: key,
|
|
name: key,
|
|
data: rows
|
|
.map((row, idx) => {
|
|
const isNextToDefinedValue =
|
|
isDefined(rows[idx - 1]?.[key]) || isDefined(rows[idx + 1]?.[key]);
|
|
const isFillNeighborValue =
|
|
!isDefined(row[key]) &&
|
|
isNextToDefinedValue &&
|
|
fillNeighborValue !== undefined;
|
|
let value: DataRecordValue | undefined = row[key];
|
|
if (isFillNeighborValue) {
|
|
value = fillNeighborValue;
|
|
} else if (
|
|
stack === AreaChartExtraControlsValue.Expand &&
|
|
totalStackedValues.length > 0
|
|
) {
|
|
value = ((value || 0) as number) / totalStackedValues[idx];
|
|
}
|
|
return [row[xAxis], value];
|
|
})
|
|
.filter(obs => !removeNulls || (obs[0] !== null && obs[1] !== null))
|
|
.map(obs => (isHorizontal ? [obs[1], obs[0]] : obs)),
|
|
}));
|
|
}
|
|
|
|
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 === '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();
|
|
}
|
|
if (typeof name === 'number') {
|
|
return numberFormatter ? numberFormatter(name) : name.toString();
|
|
}
|
|
return name;
|
|
}
|
|
|
|
export const getColtypesMapping = ({
|
|
coltypes = [],
|
|
colnames = [],
|
|
}: Pick<ChartDataResponseResult, 'coltypes' | 'colnames'>): 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: '',
|
|
};
|
|
|
|
export function getAxisType(
|
|
dataType?: GenericDataType,
|
|
): 'time' | 'value' | 'category' {
|
|
if (dataType === GenericDataType.TEMPORAL) {
|
|
return 'time';
|
|
}
|
|
if (dataType === GenericDataType.NUMERIC) {
|
|
return 'value';
|
|
}
|
|
return 'category';
|
|
}
|