Files
KnowStreaming/km-console/packages/layout-clusters-fe/src/constants/chartConfig.ts

412 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import moment from 'moment';
import { MetricType } from '@src/api';
import { MetricsDefine } from '@src/pages/CommonConfig';
export interface MetricInfo {
name: string;
desc: string;
type: number;
set: boolean;
rank: number | null;
support: boolean;
}
// 接口返回图表原始数据类型
export interface MetricDefaultChartDataType {
metricName: string;
metricLines: {
name: string;
createTime: number;
updateTime: number;
metricPoints: {
aggType: string;
timeStamp: number;
value: number;
createTime: number;
updateTime: number;
}[];
}[];
}
// 格式化后图表数据类型
export interface MetricChartDataType {
metricName: string;
metricUnit: string;
metricLines: {
name: string;
data: (string | number)[][];
}[];
}
// 图表颜色库
export const CHART_COLOR_LIST = [
'#556ee6',
'#94BEF2',
'#95e7ff',
'#9DDEEB',
'#A7B1EB',
'#C2D0E3',
'#CCABF1',
'#F9D77B',
'#F5C993',
'#A7E6C7',
'#F19FC9',
'#F5B6B3',
'#C9E795',
];
// 图表存储单位换算
export const UNIT_MAP = {
TB: Math.pow(1024, 4),
GB: Math.pow(1024, 3),
MB: Math.pow(1024, 2),
KB: 1024,
};
export const getUnit = (value: number) => Object.entries(UNIT_MAP).find(([, size]) => value / size >= 1) || ['Byte', 1];
// 图表数字单位换算
export const DATA_NUMBER_MAP = {
十亿: Math.pow(1000, 3),
百万: Math.pow(1000, 2),
: 1000,
};
export const getDataNumberUnit = (value: number) => Object.entries(DATA_NUMBER_MAP).find(([, size]) => value / size >= 1) || ['', 1];
// 图表补点间隔计算
export const SUPPLEMENTARY_INTERVAL_MAP = {
'0': 60 * 1000,
// 6 小时10 分钟间隔
'21600000': 10 * 60 * 1000,
// 24 小时1 小时间隔
'86400000': 60 * 60 * 1000,
};
export const getSupplementaryInterval = (range: number) => {
return Object.entries(SUPPLEMENTARY_INTERVAL_MAP)
.reverse()
.find(([curRange]) => range > Number(curRange))[1];
};
// 处理图表排序
export const resolveMetricsRank = (metricList: MetricInfo[]) => {
const isRanked = metricList.some(({ rank }) => rank !== null);
let shouldUpdate = false;
let list: string[] = [];
if (isRanked) {
const rankedMetrics = metricList.filter(({ rank }) => rank !== null).sort((a, b) => a.rank - b.rank);
const unRankedMetrics = metricList.filter(({ rank }) => rank === null);
// 如果有新增/删除指标的情况,需要触发更新
if (unRankedMetrics.length || rankedMetrics.some(({ rank }, i) => rank !== i)) {
shouldUpdate = true;
}
list = [...rankedMetrics.map(({ name }) => name), ...unRankedMetrics.map(({ name }) => name).sort()];
} else {
shouldUpdate = true;
// 按字母先后顺序初始化指标排序
list = metricList.map(({ name }) => name).sort();
}
return {
list,
listInfo: list.map((metric, rank) => ({ metric, rank, set: metricList.find(({ name }) => metric === name)?.set || false })),
shouldUpdate,
};
};
// 补点
export const supplementaryPoints = (
lines: MetricChartDataType['metricLines'],
timeRange: readonly [number, number],
extraCallback?: (point: [number, 0]) => any[]
): void => {
const interval = getSupplementaryInterval(timeRange[1] - timeRange[0]);
lines.forEach(({ data }) => {
// 获取未补点前线条的点的个数
let len = data.length;
// 记录当前处理到的点的下标值
let i = 0;
for (; i < len; i++) {
if (i === 0) {
let firstPointTimestamp = data[0][0] as number;
while (firstPointTimestamp - interval > timeRange[0]) {
const prevPointTimestamp = firstPointTimestamp - interval;
data.unshift(extraCallback ? extraCallback([prevPointTimestamp, 0]) : [prevPointTimestamp, 0]);
firstPointTimestamp = prevPointTimestamp;
len++;
i++;
}
}
if (i === len - 1) {
let lastPointTimestamp = data[i][0] as number;
while (lastPointTimestamp + interval < timeRange[1]) {
const nextPointTimestamp = lastPointTimestamp + interval;
data.push(extraCallback ? extraCallback([nextPointTimestamp, 0]) : [nextPointTimestamp, 0]);
lastPointTimestamp = nextPointTimestamp;
}
break;
}
{
let timestamp = data[i][0] as number;
while (timestamp + interval < data[i + 1][0]) {
const nextPointTimestamp = timestamp + interval;
data.splice(i + 1, 0, extraCallback ? extraCallback([nextPointTimestamp, 0]) : [nextPointTimestamp, 0]);
timestamp = nextPointTimestamp;
len++;
i++;
}
}
}
});
};
// 格式化图表数据
export const formatChartData = (
// 图表源数据
metricData: MetricDefaultChartDataType[],
// 获取指标单位
getMetricDefine: (type: MetricType, metric: string) => MetricsDefine[keyof MetricsDefine],
// 指标类型
metricType: MetricType,
// 图表时间范围,用于补点
timeRange: readonly [number, number],
transformUnit: [string, number] = undefined
): MetricChartDataType[] => {
return metricData.map(({ metricName, metricLines }) => {
const curMetricInfo = (getMetricDefine && getMetricDefine(metricType, metricName)) || null;
const isByteUnit = curMetricInfo?.unit?.toLowerCase().includes('byte');
let maxValue = -1;
const PointsMapMethod = ({ timeStamp, value }: { timeStamp: number; value: string | number }) => {
let parsedValue: string | number = Number(value);
if (Number.isNaN(parsedValue)) {
parsedValue = value;
} else {
// 为避免出现过小的数字影响图表展示效果,图表值统一保留小数点后三位
parsedValue = parseFloat(parsedValue.toFixed(3));
if (maxValue < parsedValue) maxValue = parsedValue;
}
return [timeStamp, parsedValue];
};
// 初始化返回结构
const chartData = {
metricName,
metricUnit: curMetricInfo?.unit || '',
metricLines: metricLines
.sort((a, b) => Number(a.name < b.name) - 0.5)
.map(({ name, metricPoints }) => ({
name,
data: metricPoints.map(PointsMapMethod),
})),
};
// 按时间先后进行对图表点排序
chartData.metricLines.forEach(({ data }) => data.sort((a, b) => (a[0] as number) - (b[0] as number)));
// 图表值单位转换
if (maxValue > 0) {
const [unitName, unitSize]: [string, number] = transformUnit || isByteUnit ? getUnit(maxValue) : getDataNumberUnit(maxValue);
chartData.metricUnit = isByteUnit
? chartData.metricUnit.toLowerCase().replace('byte', unitName)
: `${unitName}${chartData.metricUnit}`;
chartData.metricLines.forEach(({ data }) => data.forEach((point: any) => (point[1] /= unitSize)));
}
// 补点
supplementaryPoints(chartData.metricLines, timeRange);
return chartData;
});
};
// 图表 tooltip 基础展示样式
const tooltipFormatter = (date: any, arr: any, tooltip: any) => {
const str = arr
.map(
(item: any) => `<div style="margin: 3px 0;">
<div style="display:flex;align-items:center;">
<div style="margin-right:4px;width:8px;height:2px;background-color:${item.color};"></div>
<div style="flex:1;display:flex;justify-content:space-between;align-items:center;overflow: hidden;">
<span style="font-size:12px;color:#74788D;pointer-events:auto;margin-left:2px;line-height: 18px;font-family: HelveticaNeue;overflow: hidden; text-overflow: ellipsis; white-space: no-wrap;">
${item.seriesName}
</span>
<span style="font-size:12px;color:#212529;line-height:18px;font-family:HelveticaNeue-Medium; padding-left: 6px;">
${parseFloat(Number(item.value[1]).toFixed(3))}
</span>
</div>
</div>
</div>`
)
.join('');
return `<div style="margin: 0px 0 0; position: relative; z-index: 99;width: ${
tooltip.customWidth ? tooltip.customWidth + 'px' : 'fit-content'
};">
<div style="padding: 8px 0;height: 100%;">
<div style="font-size:12px;padding: 0 12px;color:#212529;line-height:20px;font-family: HelveticaNeue;">
${date}
</div>
<div style="${
tooltip.legendContextMaxHeight ? 'max-height: ' + tooltip.legendContextMaxHeight + 'px' : ''
}; margin: 4px 0 0 0;overflow-y:auto;padding: 0 12px;">
${str}
</div>
</div>
</div>`;
};
// 折线图基础主题配置,返回 echarts 配置项。详见 https://echarts.apache.org/zh/option.html
export const getBasicChartConfig = (props: any = {}) => {
const { title = {}, grid = {}, legend = {}, xAxis = {}, yAxis = {}, tooltip = {}, ...restConfig } = props;
return {
title: {
show: true,
text: '示例标题',
textStyle: {
fontSize: 14,
fontFamily: 'HelveticaNeue-Medium',
color: '#212529',
letterSpacing: 0.5,
lineHeight: 16,
rich: {
unit: {
fontSize: 12,
fontFamily: 'HelveticaNeue-Medium',
color: '#495057',
lineHeight: 16,
},
},
},
top: 12,
left: 16,
zlevel: 1,
...title,
},
// 图表整体布局
grid: {
zlevel: 0,
top: 60,
left: 22,
right: 16,
bottom: 40,
containLabel: true,
...grid,
},
// 图例配置
legend: {
zlevel: 1,
type: 'scroll',
orient: 'horizontal',
left: 20,
top: 'auto',
bottom: 12,
icon: 'rect',
itemHeight: 2,
itemWidth: 8,
itemGap: 8,
textStyle: {
fontSize: 11,
color: '#74788D',
},
pageIcons: {
horizontal: [
'path://M474.496 512l151.616 151.616a9.6 9.6 0 0 1 0 13.568l-31.68 31.68a9.6 9.6 0 0 1-13.568 0l-190.08-190.08a9.6 9.6 0 0 1 0-13.568l190.08-190.08a9.6 9.6 0 0 1 13.568 0l31.68 31.68a9.6 9.6 0 0 1 0 13.568L474.496 512z',
'path://M549.504 512L397.888 360.384a9.6 9.6 0 0 1 0-13.568l31.68-31.68a9.6 9.6 0 0 1 13.568 0l190.08 190.08a9.6 9.6 0 0 1 0 13.568l-190.08 190.08a9.6 9.6 0 0 1-13.568 0l-31.68-31.68a9.6 9.6 0 0 1 0-13.568L549.504 512z',
],
},
pageIconColor: '#495057',
pageIconInactiveColor: '#ADB5BC',
pageIconSize: 6,
tooltip: false,
...legend,
},
// 横坐标配置
xAxis: {
type: 'category',
boundaryGap: true,
axisLine: {
lineStyle: {
color: '#c5c5c5',
width: 1,
},
},
axisLabel: {
formatter: (value: number) => {
value = Number(value);
return [`{date|${moment(value).format('MM-DD')}}`, `{time|${moment(value).format('HH:mm')}}`].join('\n');
},
padding: 0,
rich: {
date: {
color: '#495057',
fontSize: 11,
lineHeight: 18,
fontFamily: 'HelveticaNeue',
},
time: {
color: '#ADB5BC',
fontSize: 11,
lineHeight: 11,
fontFamily: 'HelveticaNeue',
},
},
},
...xAxis,
},
// 纵坐标配置
yAxis: {
type: 'value',
axisLabel: {
color: '#495057',
fontSize: 12,
},
splitLine: {
show: true,
lineStyle: {
width: 1,
type: 'dashed',
color: ['#E4E7ED'],
},
},
...yAxis,
},
// 提示框浮层配置
tooltip: {
position: function (pos: any, params: any, el: any, elRect: any, size: any) {
const tooltipWidth = el.offsetWidth || 120;
const result =
tooltipWidth + pos[0] < size.viewSize[0]
? {
top: 10,
left: pos[0] + 30,
}
: {
top: 10,
left: pos[0] - tooltipWidth - 30,
};
return result;
},
formatter: function (params: any) {
let res = '';
if (params != null && params.length > 0) {
// 传入tooltip是为了便于拿到外部传入的控制这个自定义浮层的样式
// 例如tooltip里写customWidth: 200则tooltipFormatter里可以取出这个宽度使用
res += tooltipFormatter(moment(Number(params[0].axisValue)).format('YYYY-MM-DD HH:mm'), params, tooltip);
}
return res;
},
extraCssText:
'padding: 0;box-shadow: 0 -2px 4px 0 rgba(0,0,0,0.02), 0 2px 6px 6px rgba(0,0,0,0.02), 0 2px 6px 0 rgba(0,0,0,0.06);border-radius: 8px;',
axisPointer: {
type: 'line',
},
...tooltip,
},
...restConfig,
};
};