Skip to content

Commit 5c8541c

Browse files
committed
[pinpoint-apm#12240] Heatmap > outlink button
1 parent ecfc7c4 commit 5c8541c

File tree

13 files changed

+568
-290
lines changed

13 files changed

+568
-290
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { ScatterOrHeatmapFullScreenPage, withInitialFetch } from '@pinpoint-fe/ui';
2+
import { getLayoutWithSideNavigation } from '@pinpoint-fe/web/src/components/Layout/LayoutWithSideNavigation';
3+
4+
export default withInitialFetch((props) => {
5+
return getLayoutWithSideNavigation(<ScatterOrHeatmapFullScreenPage {...props} />);
6+
});

web-frontend/src/main/v3/apps/web/src/routes/index.tsx

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { createBrowserRouter, redirect } from 'react-router-dom';
33
import ServerMap from '@pinpoint-fe/web/src/pages/ServerMap';
44
import Realtime from '@pinpoint-fe/web/src/pages/ServerMap/Realtime';
55
import ScatterFullScreen from '@pinpoint-fe/web/src/pages/ScatterFullScreen';
6+
import ScatterOrHeatmapFullScreen from '@pinpoint-fe/web/src/pages/ScatterOrHeatmapFullScreen';
67
import {
78
serverMapRouteLoader,
89
realtimeLoader,
@@ -17,6 +18,8 @@ import {
1718
openTelemetryRouteLoader,
1819
scatterFullScreenLoader,
1920
scatterFullScreenRealtimeLoader,
21+
scatterOrHeatmapFullScreenLoader,
22+
scatterOrHeatmapFullScreenRealtimeLoader,
2023
} from '@pinpoint-fe/ui/src/loader';
2124

2225
import FilteredMap from '@pinpoint-fe/web/src/pages/FilteredMap';
@@ -87,13 +90,23 @@ const router = createBrowserRouter(
8790
},
8891
{
8992
path: `${APP_PATH.SCATTER_FULL_SCREEN}/:application?`,
90-
element: <ScatterFullScreen />,
91-
loader: scatterFullScreenLoader,
93+
element: <ScatterOrHeatmapFullScreen />,
94+
loader: scatterOrHeatmapFullScreenLoader,
9295
},
9396
{
9497
path: `${APP_PATH.SCATTER_FULL_SCREEN_REALTIME}/:application?`,
95-
element: <ScatterFullScreen />,
96-
loader: scatterFullScreenRealtimeLoader,
98+
element: <ScatterOrHeatmapFullScreen />,
99+
loader: scatterOrHeatmapFullScreenRealtimeLoader,
100+
},
101+
{
102+
path: `${APP_PATH.HEATMAP_FULL_SCREEN}/:application?`,
103+
element: <ScatterOrHeatmapFullScreen />,
104+
loader: scatterOrHeatmapFullScreenLoader,
105+
},
106+
{
107+
path: `${APP_PATH.HEATMAP_FULL_SCREEN_REALTIME}/:application?`,
108+
element: <ScatterOrHeatmapFullScreen />,
109+
loader: scatterOrHeatmapFullScreenRealtimeLoader,
97110
},
98111
{
99112
path: `${APP_PATH.ERROR_ANALYSIS}/:application?`,
Lines changed: 15 additions & 277 deletions
Original file line numberDiff line numberDiff line change
@@ -1,279 +1,17 @@
11
import React from 'react';
2-
import ReactEChartsCore from 'echarts-for-react/lib/core';
3-
import * as echarts from 'echarts/core';
4-
import { CanvasRenderer } from 'echarts/renderers';
5-
import { HeatmapChart as HeatmapChartEcharts } from 'echarts/charts';
6-
import {
7-
GridComponent,
8-
TooltipComponent,
9-
VisualMapComponent,
10-
GraphicComponent,
11-
} from 'echarts/components';
12-
import { EChartsOption } from 'echarts';
13-
import { mockData } from './mockData';
14-
import { colors } from '@pinpoint-fe/ui/src/constants';
15-
import { capitalize } from 'lodash';
16-
import { defaultTickFormatter } from '@pinpoint-fe/ui/src/components/ReChart';
17-
import { HeatmapSettingType } from './HeatmapSetting';
18-
19-
echarts.use([
20-
CanvasRenderer, // 캔버스 렌더링
21-
HeatmapChartEcharts, // 히트맵 차트
22-
GridComponent, // 그리드
23-
TooltipComponent, // 툴팁
24-
VisualMapComponent, // 색상 범례
25-
GraphicComponent, // 그래픽
26-
]);
27-
28-
echarts.use([HeatmapChartEcharts, CanvasRenderer]);
29-
30-
export const HeatmapColor = {
31-
success: '#34b994',
32-
failed: '#eb4748',
33-
};
34-
35-
type HeatmapChartProps = {
36-
setting: HeatmapSettingType;
2+
import { HeatmapFetcher, HeatmapFetcherProps } from './HeatmapFetcher';
3+
import { ErrorBoundary, ChartSkeleton } from '../..';
4+
5+
export interface HeatmapChartProps extends HeatmapFetcherProps {
6+
realtime?: boolean;
7+
}
8+
9+
export const HeatmapChart = ({ realtime = false, ...props }: HeatmapChartProps) => {
10+
return (
11+
<ErrorBoundary>
12+
<React.Suspense fallback={<ChartSkeleton />}>
13+
{realtime ? <div>Realtime heatmap</div> : <HeatmapFetcher {...props} />}
14+
</React.Suspense>
15+
</ErrorBoundary>
16+
);
3717
};
38-
39-
const HeatmapChart = React.forwardRef(
40-
({ setting }: HeatmapChartProps, ref: React.Ref<ReactEChartsCore>) => {
41-
const containerRef = React.useRef<HTMLDivElement>(null);
42-
const [containerSize, setContainerSize] = React.useState({
43-
width: 0,
44-
height: 0,
45-
});
46-
const chartRef = React.useRef<ReactEChartsCore>(null);
47-
const successData: [string, string, number][] = [];
48-
const failedData: [string, string, number][] = [];
49-
let maxFailedCount = 0;
50-
let maxSuccessCount = 0;
51-
52-
React.useEffect(() => {
53-
const wrapperElement = containerRef.current;
54-
if (!wrapperElement) return;
55-
const resizeObserver = new ResizeObserver(() => {
56-
setContainerSize({
57-
width: wrapperElement.clientWidth,
58-
height: wrapperElement.clientHeight,
59-
});
60-
});
61-
resizeObserver.observe(wrapperElement);
62-
63-
return () => {
64-
resizeObserver.disconnect();
65-
};
66-
}, []);
67-
68-
const { matrixData } = mockData;
69-
matrixData.forEach((row) => {
70-
row.cellData.forEach((cell) => {
71-
successData.push([String(row.timestamp), String(cell.elapsedTime), cell.successCount]);
72-
failedData.push([String(row.timestamp), String(cell.elapsedTime), cell.failCount]);
73-
74-
maxSuccessCount = Math.max(maxSuccessCount, cell.successCount);
75-
maxFailedCount = Math.max(maxFailedCount, cell.failCount);
76-
});
77-
});
78-
79-
const totalSuccessCount = setting.yMax;
80-
const totalFailedCount = setting.yMax;
81-
82-
const xAxisData = matrixData.map((row) => String(row.timestamp));
83-
const yAxisData = matrixData[0].cellData.map((cell) => String(cell.elapsedTime));
84-
85-
// console.log('successData', successData);
86-
// console.log('failedData', failedData);
87-
88-
const option: EChartsOption = {
89-
tooltip: {
90-
borderColor: colors.gray[300],
91-
textStyle: {
92-
fontFamily: 'inherit',
93-
fontSize: 8,
94-
},
95-
formatter: (params: any) => {
96-
const { data } = params;
97-
const [timestamp, elapsedTime, failedCount] = data;
98-
const date = new Date(timestamp);
99-
const successCount =
100-
successData.find(
101-
(item: [string, string, number]) => item[0] === timestamp && item[1] === elapsedTime,
102-
)?.[2] || 'N/A';
103-
104-
return `
105-
<div style="display: flex; flex-direction: column; gap: 5px; padding: 2px;">
106-
<div style="margin-bottom: 5px;"><strong>${defaultTickFormatter(date.getTime())}</strong></div>
107-
${['success', 'failed']
108-
.map((type) => {
109-
const count = type === 'success' ? successCount : failedCount;
110-
const color = type === 'success' ? HeatmapColor.success : HeatmapColor.failed;
111-
112-
return `
113-
<div style="display: flex; justify-content: space-between; gap: 5px;">
114-
<div style="display: flex; gap: 6px; align-items: center;">
115-
<div style="width: 8px; height: 8px; background: ${color}"></div>${capitalize(type)}
116-
</div>
117-
<div>${Number(count).toLocaleString()}</div>
118-
</div>
119-
`;
120-
})
121-
.join('')}
122-
</div>
123-
`;
124-
},
125-
},
126-
grid: {
127-
left: setting.yMax.toString().length * 10,
128-
right: '10px',
129-
top: '2%',
130-
bottom: '20%',
131-
},
132-
xAxis: {
133-
type: 'category',
134-
data: xAxisData.sort((a, b) => new Date(a).getTime() - new Date(b).getTime()),
135-
axisLabel: {
136-
interval: 'auto',
137-
showMaxLabel: true,
138-
showMinLabel: true,
139-
formatter: (value: string) => {
140-
const date = new Date(value);
141-
return defaultTickFormatter(date.getTime());
142-
},
143-
},
144-
},
145-
yAxis: {
146-
type: 'category',
147-
data: yAxisData.filter(
148-
(yValue) => Number(yValue) >= setting.yMin && Number(yValue) <= setting.yMax,
149-
),
150-
axisLabel: {
151-
interval: (index: number, value: string) => {
152-
if (yAxisData.length <= 5) {
153-
return true;
154-
}
155-
156-
const step = (yAxisData.length - 1) / 4;
157-
const positions = [
158-
0,
159-
...Array.from({ length: 3 }, (_, i) => Math.round((i + 1) * step)),
160-
yAxisData.length - 1,
161-
];
162-
163-
return positions.includes(index);
164-
},
165-
formatter: (value: string) => {
166-
try {
167-
return Number(value).toLocaleString();
168-
} catch (err) {
169-
return value;
170-
}
171-
},
172-
},
173-
},
174-
visualMap: [
175-
{
176-
min: 0,
177-
max: maxSuccessCount,
178-
calculable: true,
179-
seriesIndex: 0,
180-
orient: 'horizontal',
181-
itemHeight: (containerSize.width || 100) * 0.3,
182-
right: '45%',
183-
bottom: '4%',
184-
hoverLink: false,
185-
formatter: (value) => {
186-
if (value === setting.yMax) {
187-
return '';
188-
}
189-
return Math.floor(Number(value)).toLocaleString();
190-
},
191-
inRange: {
192-
color: ['#ffffff', HeatmapColor.success],
193-
},
194-
},
195-
{
196-
min: 0,
197-
max: maxFailedCount,
198-
calculable: true,
199-
seriesIndex: 1,
200-
orient: 'horizontal',
201-
itemHeight: (containerSize.width || 100) * 0.3,
202-
left: '55%',
203-
bottom: '4%',
204-
hoverLink: false,
205-
formatter: (value) => {
206-
return Math.floor(Number(value)).toLocaleString();
207-
},
208-
inRange: {
209-
color: ['#ffffff', HeatmapColor.failed],
210-
},
211-
},
212-
],
213-
graphic: [
214-
{
215-
type: 'text',
216-
bottom: '0%',
217-
left: 'center',
218-
style: {
219-
text: `Success {boldSuccess|${Math.floor(totalSuccessCount).toLocaleString()}} Failed {boldFailed|${Math.floor(totalFailedCount).toLocaleString()}}`,
220-
fontSize: 15,
221-
fill: colors.gray[500],
222-
rich: {
223-
boldSuccess: {
224-
fontWeight: 'bold',
225-
fill: HeatmapColor.success,
226-
},
227-
boldFailed: {
228-
fontWeight: 'bold',
229-
fill: HeatmapColor.failed,
230-
},
231-
},
232-
},
233-
},
234-
],
235-
series: [
236-
{
237-
name: 'success',
238-
type: 'heatmap',
239-
data: successData,
240-
},
241-
{
242-
name: 'failed',
243-
type: 'heatmap',
244-
data: failedData,
245-
itemStyle: {
246-
opacity: 0.5,
247-
},
248-
emphasis: {
249-
itemStyle: {
250-
borderColor: '#333',
251-
borderWidth: 1,
252-
},
253-
},
254-
},
255-
],
256-
};
257-
258-
return (
259-
<div ref={containerRef} className="relative w-full h-full">
260-
<ReactEChartsCore
261-
ref={ref}
262-
echarts={echarts}
263-
option={option}
264-
style={{ height: '100%', width: '100%', minHeight: 500 }}
265-
onEvents={{
266-
// mouseover: (params: any) => {
267-
// console.log('mouseover', params);
268-
// },
269-
click: (params: any) => {
270-
console.log('click', params);
271-
},
272-
}}
273-
/>
274-
</div>
275-
);
276-
},
277-
);
278-
279-
export default HeatmapChart;

0 commit comments

Comments
 (0)