* -------------------------------------------------------------------------
* This file is part of the MindStudio project.
* Copyright (c) 2025 Huawei Technologies Co.,Ltd.
*
* MindStudio is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
*
* http://license.coscl.org.cn/MulanPSL2
*
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
* -------------------------------------------------------------------------
*/
import * as React from 'react';
import { useTranslation } from 'react-i18next';
import type { TFunction } from 'i18next';
import type { Graph } from '../entity/memory';
import { binarySearch, useResizeEventDependency } from '../utils/memoryUtils';
import * as echarts from 'echarts';
import { convertTime, useChartCharacter } from './Common';
import styled from '@emotion/styled';
import { chartColors, getDefaultChartOptions, getLegendStyle, safeStr } from '@insight/lib/utils';
import { type Theme, useTheme } from '@emotion/react';
import type { RangeFlagList } from '../entity/memorySession';
const MAX_PLAIN_LEGENDS_COUNT = 9;
const SHOW_ALL_SYMBOL_THRESHOLD = 1000;
const ChartDesc = styled.div`
color: ${(props): string => props.theme.textColor};
margin-bottom: 24px;
`;
interface IProps {
graph: Graph;
hAxisTitle: string;
vAxisTitle: string;
onSelectionChanged?: (start: number, end: number) => void;
record?: any;
isDark: boolean;
isStatic: boolean;
rangeFlagData?: RangeFlagList[];
}
const _getLegendData = (data: string[]): string[] => {
const tempData = [...data];
tempData.shift();
if (tempData.length < 2) {
return tempData;
}
for (let i = 1; i < tempData.length; i++) {
if ((tempData[i].endsWith('Baseline') || tempData[i].startsWith('基线'))) {
if ((tempData[i - 1].endsWith('Comparison') || tempData[i - 1].startsWith('比对'))) {
tempData.splice(i, 0, '');
}
return tempData;
}
}
return tempData;
};
const _getOriginOption = (props: IProps, theme: Theme): echarts.EChartsOption => {
const { isStatic, isDark, hAxisTitle, vAxisTitle } = props;
const legendDatas = _getLegendData(props.graph.columns);
return {
textStyle: getDefaultChartOptions().textStyle,
title: { text: '' },
tooltip: {
trigger: 'axis',
formatter: (params: any): string => {
let res = `${isStatic ? safeStr(params?.[0]?.name) : convertTime(params?.[0]?.name)} <br/>`;
for (const item of params) {
if (!isNaN(Number(item?.value?.[item?.encode?.y?.[0]]))) {
res += `<span style="background: ${item.color};
height: 10px;
width: 10px;
border-radius: 50%;
display: inline-block;
margin-right: 10px;"></span>
${safeStr(item.seriesName)}: ${safeStr(item?.value?.[item?.encode?.y?.[0]])}<br/>`;
}
}
return res;
},
...getDefaultChartOptions(isDark).tooltip,
},
legend: {
itemGap: 20,
data: legendDatas,
type: (legendDatas.length > MAX_PLAIN_LEGENDS_COUNT && !legendDatas.includes('')) ? 'scroll' : 'plain',
...getLegendStyle(theme),
},
grid: { left: '100', right: '100', bottom: 40 },
xAxis: { type: 'category', boundaryGap: false, name: hAxisTitle },
yAxis: { type: 'value', name: vAxisTitle, scale: true },
toolbox: {
feature: {
dataZoom: {
icon: { back: 'none' },
yAxisIndex: 'none',
emphasis: { iconStyle: { textPosition: 'top' } },
},
restore: {
emphasis: { iconStyle: { textPosition: 'top' } },
},
},
top: 20,
right: 10,
},
backgroundColor: 'transparent',
};
};
const findClosestIndex = (data: Graph['rows'], target: number): number => {
if (data.length === 0) {
return -1;
}
let left = 0;
let right = data.length - 1;
while (left < right - 1) {
const mid = Math.floor((left + right) / 2);
if (Number(data[mid][0]) === target) {
return mid;
} else if (Number(data[mid][0]) < target) {
left = mid;
} else {
right = mid;
}
}
if (Math.abs(Number(data[left][0]) - target) <= Math.abs(Number(data[right][0]) - target)) {
return left;
} else {
return right;
}
};
const _getMarkAreaOptions = (rangeFlagData: RangeFlagList[], data: Graph['rows'], chartWidth: number): echarts.EChartsOption => {
return {
name: 'Mark Area',
type: 'line',
data: [],
markArea: {
data: rangeFlagData.map(item => {
const startIndex = findClosestIndex(data, item.timeStamp / 1000000);
let endIndex = findClosestIndex(data, item.anotherTimeStamp / 1000000);
if (startIndex === endIndex && startIndex !== 0) {
endIndex++;
}
const titleWidth = chartWidth / data.length * (endIndex - startIndex);
return [
{
xAxis: startIndex,
label: {
show: titleWidth > 30,
position: 'top',
formatter: item.description,
overflow: 'truncate',
width: titleWidth,
},
itemStyle: {
color: item.color,
opacity: 0.3,
},
},
{
xAxis: endIndex,
},
];
}),
},
};
};
const _handleOption = (option: echarts.EChartsOption, graph: Graph, chartWidth: number, rangeFlagData?: RangeFlagList[]): echarts.EChartsOption => {
const lineSeries: echarts.SeriesOption = {
type: 'line',
connectNulls: true,
showAllSymbol: graph.rows.length < SHOW_ALL_SYMBOL_THRESHOLD ? true : 'auto',
emphasis: {
label: {
show: true,
},
itemStyle: {
borderWidth: 5,
shadowBlur: 5,
shadowColor: '#ffffff',
},
},
select: {
itemStyle: {
borderWidth: 5,
shadowBlur: 5,
},
},
animation: false,
};
const series = Array(graph.columns.length - 1).fill(lineSeries);
if (rangeFlagData !== undefined && rangeFlagData.length > 0) {
series.push(_getMarkAreaOptions(rangeFlagData, graph.rows, chartWidth));
}
const newOption = {
...option,
color: chartColors,
animation: false,
dataset:
{
source: [graph.columns, ...graph.rows],
},
series,
};
return newOption;
};
const _showGraph = (myChart: echarts.ECharts, selectedPoints: React.MutableRefObject<number[]>,
props: IProps, theme: Theme, chartWidth: number): void => {
const { graph, onSelectionChanged, rangeFlagData } = props;
let option = _getOriginOption(props, theme);
option = _handleOption(option, graph, chartWidth, rangeFlagData);
requestAnimationFrame(() => {
myChart.setOption(option, { notMerge: true, lazyUpdate: true });
myChart.dispatchAction({
type: 'takeGlobalCursor',
key: 'dataZoomSelect',
dataZoomSelectActive: true,
});
});
myChart.on('dataZoom', (param: any) => {
onSelectionChanged?.(param?.batch?.[0]?.startValue, param?.batch?.[0]?.endValue);
});
myChart.on('restore', () => {
onSelectionChanged?.(0, -1);
});
myChart.on('click', (param) => {
myChart.dispatchAction({
type: 'unselect',
seriesId: param.seriesId,
dataIndex: selectedPoints.current,
});
myChart.dispatchAction({
type: 'select',
seriesId: param.seriesId,
dataIndex: param.dataIndex,
});
selectedPoints.current = [param.dataIndex];
});
myChart.getZr().on('contextmenu', () => {
myChart.dispatchAction({
type: 'restore',
});
myChart.dispatchAction({
type: 'takeGlobalCursor',
key: 'dataZoomSelect',
dataZoomSelectActive: true,
});
});
};
const _handleEvents = (chartObj: echarts.ECharts | undefined, props: IProps,
selectedPoints: React.MutableRefObject<number[]>, graph: Graph, t: TFunction): void => {
const { record } = props;
const compareFun = (key: number, mid: Array<number | string>): number => key - parseFloat(mid[0] as string);
if (chartObj) {
if (record !== undefined) {
const startId = binarySearch(graph.rows, Number(record?.allocationTime ?? record?.nodeIndexStart), compareFun);
const endId = binarySearch(graph.rows, Number(record?.releaseTime ?? record?.nodeIndexEnd), compareFun);
const selection = [];
if (startId >= 0) {
selection.push(startId);
}
if (endId >= 0) {
selection.push(endId);
}
chartObj.dispatchAction({
type: 'downplay',
seriesName: t('Operators Allocated'),
dataIndex: selectedPoints.current,
});
chartObj.dispatchAction({
type: 'highlight',
seriesName: t('Operators Allocated'),
dataIndex: selection,
});
selectedPoints.current = selection;
} else {
chartObj.dispatchAction({
type: 'downplay',
seriesName: t('Operators Allocated'),
dataIndex: selectedPoints.current,
});
selectedPoints.current = [];
}
}
};
const useTitle = (title: string): string => {
const { t } = useTranslation('memory', { keyPrefix: 'searchCriteria' });
const regexItem = [
'Peak Memory Usage',
'Operator Activated',
'Operator Allocated',
'Operator Reserved',
'PTA Allocated',
'PTA Reserved',
'PTA Activated',
'GE Allocated',
'GE Reserved',
'GE Activated',
'APP Reserved',
];
const regex = new RegExp(regexItem.join('|'), 'g');
const translatedMessage = title.replace(regex, match => t(match));
return translatedMessage;
};
export const LineChart: React.FC<IProps> = (props) => {
const { graph, record, isDark, rangeFlagData } = props;
const graphRef = React.useRef<HTMLDivElement>(null);
const [resizeEventDependency] = useResizeEventDependency();
const [chartObj, setChartObj] = React.useState<echarts.ECharts | undefined>();
const selectedPoints = React.useRef<number[]>([]);
const chartCharacter = useChartCharacter();
const title = useTitle(graph.title ?? '');
const { t, i18n } = useTranslation('memory');
const locale = i18n.language?.slice(0, 2);
const theme = useTheme();
React.useLayoutEffect(() => {
const element = graphRef.current;
if (!element) {
return () => {};
}
element.oncontextmenu = (): boolean => { return false; };
const chartWidth = graphRef.current.clientWidth - 200;
const myChart = echarts.init(element, isDark ? 'dark' : 'customed', { locale });
_showGraph(myChart, selectedPoints, props, theme, chartWidth);
setChartObj(myChart);
return () => {
myChart.dispose();
};
}, [graph, isDark, i18n, rangeFlagData]);
React.useEffect(() => {
if (!graphRef.current) {
return;
}
echarts.getInstanceByDom(graphRef.current)?.resize();
}, [resizeEventDependency]);
React.useEffect(() => {
_handleEvents(chartObj, props, selectedPoints, graph, t);
}, [graph, record, chartObj]);
return (
<div>
{graph.title !== undefined && graph.title?.length !== 0
? <ChartDesc>{title}{chartCharacter}</ChartDesc>
: null
}
<div ref={graphRef} style={{ width: 'calc(100vw - 80px)', height: '400px' }}></div>
</div>
);
};