feat: 帕累托分析
parent
5a57c904e3
commit
7f69902d2f
@ -0,0 +1,125 @@
|
||||
import React from 'react';
|
||||
import { DualAxes } from '@ant-design/charts';
|
||||
import { fixTo2Decimals, fixTo4Decimals, fixTo1Decimals, groupBy } from '../utils/commons';
|
||||
|
||||
const ParetoChart = ({ data, xField, yField, thresholds = { A: 80, B: 90 }, title = '帕累托分析', showCategory = true, showThresholds = true, yFieldAlias }) => {
|
||||
// 1. 数据清洗
|
||||
const cleanedData = data
|
||||
.filter((d) => d[yField])
|
||||
.sort((a, b) => b[yField] - a[yField]);
|
||||
|
||||
// 2. 累计占比 & 分类
|
||||
const total = cleanedData.reduce((sum, d) => sum + d[yField], 0);
|
||||
let cumulative = 0;
|
||||
const barData = cleanedData.map((d) => {
|
||||
cumulative += d[yField];
|
||||
const ratio = (cumulative / total) * 100;
|
||||
let category = 'C';
|
||||
if (ratio <= thresholds.A) category = 'A';
|
||||
else if (ratio <= thresholds.B) category = 'B';
|
||||
return { [xField]: d[xField], [yField]: d[yField], Category: category };
|
||||
});
|
||||
|
||||
// Fixed the groupBy usage and barColor assignment
|
||||
const barColor = (() => {
|
||||
const grouped = groupBy(barData, 'Category');
|
||||
const ret = new Map();
|
||||
for (const [category, items] of Object.entries(grouped)) {
|
||||
ret.set(
|
||||
category,
|
||||
items.map((e) => e[xField])
|
||||
);
|
||||
}
|
||||
return ret;
|
||||
})();
|
||||
|
||||
const lineData = barData.map((d, idx) => {
|
||||
const cumulativeValue = barData.slice(0, idx + 1).reduce((sum, x) => sum + x[yField], 0);
|
||||
return { [xField]: d[xField], Cumulative: fixTo2Decimals((cumulativeValue / total) * 100 )};
|
||||
});
|
||||
|
||||
// 3. DualAxes 配置
|
||||
const config = {
|
||||
data: [barData, lineData],
|
||||
xField,
|
||||
yField: [yField, 'Cumulative'],
|
||||
meta: {
|
||||
[yField]: {
|
||||
alias: yFieldAlias || yField,
|
||||
formatter: (v) => v > 1_0000 ? `${fixTo1Decimals((v || 0) / 10000)} 万` : v,
|
||||
},
|
||||
Cumulative: {
|
||||
alias: '累计百分比',
|
||||
formatter: (val) => `${val}%`,
|
||||
},
|
||||
},
|
||||
yAxis: {
|
||||
// 格式化左坐标轴
|
||||
Cumulative: {
|
||||
// min: 0,
|
||||
label: {
|
||||
formatter: (val) => `${val}%`,
|
||||
},
|
||||
},
|
||||
},
|
||||
geometryOptions: [
|
||||
{
|
||||
geometry: 'column',
|
||||
colorField: 'Category',
|
||||
color: showCategory
|
||||
? (x) => {
|
||||
const xText = x[xField];
|
||||
let thisC = null;
|
||||
for (const [Category, Xs] of barColor) {
|
||||
if (Xs.includes(xText)) {
|
||||
thisC = Category;
|
||||
}
|
||||
}
|
||||
if (thisC === 'A') return '#52c41a';
|
||||
if (thisC === 'B') return '#faad14';
|
||||
return '#5B8FF9';
|
||||
}
|
||||
: '#5B8FF9',
|
||||
},
|
||||
{
|
||||
geometry: 'line',
|
||||
color: '#ff4d4f',
|
||||
// smooth: true,
|
||||
lineStyle: {
|
||||
lineWidth: 2,
|
||||
},
|
||||
},
|
||||
],
|
||||
annotations: showThresholds
|
||||
? {
|
||||
Cumulative: [
|
||||
{
|
||||
type: 'line',
|
||||
start: ['min', thresholds.A],
|
||||
end: ['max', thresholds.A],
|
||||
style: { stroke: '#52c41a', lineDash: [4, 4], lineWidth: 2 },
|
||||
// text: { content: `${thresholds.A}% 阈值`, position: 'end' },
|
||||
},
|
||||
{
|
||||
type: 'line',
|
||||
start: ['min', thresholds.B],
|
||||
end: ['max', thresholds.B],
|
||||
style: { stroke: '#faad14', lineDash: [4, 4], lineWidth: 2 },
|
||||
// text: { content: `${thresholds.B}% 阈值`, position: 'end' },
|
||||
},
|
||||
],
|
||||
}
|
||||
: {},
|
||||
tooltip: { shared: true },
|
||||
title: { visible: true, text: title },
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<h3>{title}</h3>
|
||||
<DualAxes {...config} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ParetoChart;
|
||||
Loading…
Reference in New Issue