Skip to content

Commit 5a8639e

Browse files
authored
feat: Add dataset tags distribution graph (#314)
1 parent f06cf91 commit 5a8639e

File tree

1 file changed

+214
-5
lines changed

1 file changed

+214
-5
lines changed

frontend/src/pages/DataManagement/Detail/components/DataQuality.tsx

Lines changed: 214 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
// typescript
22
// File: `frontend/src/pages/DataManagement/Detail/components/DataQuality.tsx`
3-
import React from "react";
3+
import React, { useMemo } from "react";
44
// Run `npm install antd lucide-react` if your editor reports "Module is not installed"
5-
import { Card } from "antd";
6-
import { AlertTriangle } from "lucide-react";
5+
import { Card, Table, Progress } from "antd";
6+
import { AlertTriangle, Tags, BarChart3 } from "lucide-react";
77
import DevelopmentInProgress from "@/components/DevelopmentInProgress";
88
import { Dataset } from "@/pages/DataManagement/dataset.model.ts";
99

@@ -73,8 +73,214 @@ function getMockMetrics(datasetType: DatasetType, stats: FileStats) {
7373
];
7474
}
7575

76+
// 数据集标签分布统计组件
77+
interface LabelDistributionProps {
78+
distribution?: Record<string, Record<string, number>>;
79+
}
80+
81+
function LabelDistributionStats({ distribution }: LabelDistributionProps) {
82+
// 将 distribution 数据转换为表格格式
83+
const { tableData, totalLabels } = useMemo(() => {
84+
if (!distribution) return { tableData: [], totalLabels: 0 };
85+
86+
const data: Array<{
87+
category: string;
88+
label: string;
89+
count: number;
90+
percentage: number;
91+
}> = [];
92+
93+
let total = 0;
94+
95+
// 遍历 distribution 对象
96+
Object.entries(distribution).forEach(([category, labels]) => {
97+
if (typeof labels === 'object' && labels !== null) {
98+
Object.entries(labels).forEach(([label, count]) => {
99+
const numCount = typeof count === 'number' ? count : 0;
100+
total += numCount;
101+
data.push({
102+
category,
103+
label,
104+
count: numCount,
105+
percentage: 0, // 稍后计算
106+
});
107+
});
108+
}
109+
});
110+
111+
// 计算百分比
112+
data.forEach(item => {
113+
item.percentage = total > 0 ? (item.count / total) * 100 : 0;
114+
});
115+
116+
// 按 count 降序排序
117+
data.sort((a, b) => b.count - a.count);
118+
119+
return { tableData: data, totalLabels: total };
120+
}, [distribution]);
121+
122+
const columns = [
123+
{
124+
title: '类别',
125+
dataIndex: 'category',
126+
key: 'category',
127+
width: 120,
128+
render: (text: string) => (
129+
<span className="font-medium text-gray-700">{text || '未分类'}</span>
130+
),
131+
},
132+
{
133+
title: '标签名称',
134+
dataIndex: 'label',
135+
key: 'label',
136+
render: (text: string) => <span>{text}</span>,
137+
},
138+
{
139+
title: '数量',
140+
dataIndex: 'count',
141+
key: 'count',
142+
width: 100,
143+
sorter: (a: any, b: any) => a.count - b.count,
144+
render: (count: number) => (
145+
<span className="font-semibold">{count}</span>
146+
),
147+
},
148+
{
149+
title: '占比',
150+
dataIndex: 'percentage',
151+
key: 'percentage',
152+
width: 200,
153+
sorter: (a: any, b: any) => a.percentage - b.percentage,
154+
render: (percentage: number, record: any) => (
155+
<div className="flex items-center gap-3">
156+
<Progress
157+
percent={parseFloat(percentage.toFixed(1))}
158+
size="small"
159+
showInfo={true}
160+
strokeColor={{
161+
'0%': '#108ee9',
162+
'100%': '#87d068',
163+
}}
164+
/>
165+
</div>
166+
),
167+
},
168+
];
169+
170+
// 按类别分组的视图数据
171+
const categoryGroups = useMemo(() => {
172+
if (!tableData.length) return {};
173+
174+
return tableData.reduce((acc, item) => {
175+
if (!acc[item.category]) {
176+
acc[item.category] = [];
177+
}
178+
acc[item.category].push(item);
179+
return acc;
180+
}, {} as Record<string, typeof tableData>);
181+
}, [tableData]);
182+
183+
if (!distribution || Object.keys(distribution).length === 0) {
184+
return (
185+
<Card className="bg-gray-50">
186+
<div className="text-center py-8 text-gray-400">
187+
<Tags className="w-12 h-12 mx-auto mb-3 opacity-50" />
188+
<p>暂无标签分布数据</p>
189+
</div>
190+
</Card>
191+
);
192+
}
193+
194+
return (
195+
<div className="space-y-4">
196+
{/* 统计概览 */}
197+
<Card className="bg-gradient-to-r from-blue-50 to-indigo-50 border-blue-200">
198+
<div className="flex items-center justify-between">
199+
<div className="flex items-center gap-3">
200+
<div className="p-2 bg-blue-500 rounded-lg">
201+
<Tags className="w-5 h-5 text-white" />
202+
</div>
203+
<div>
204+
<h3 className="font-semibold text-gray-800">数据集标签统计</h3>
205+
<p className="text-sm text-gray-600">
206+
{Object.keys(categoryGroups).length} 个类别,{totalLabels} 个标签样本
207+
</p>
208+
</div>
209+
</div>
210+
</div>
211+
</Card>
212+
213+
{/* 表格视图 */}
214+
<Card
215+
title={
216+
<div className="flex items-center gap-2">
217+
<BarChart3 className="w-4 h-4" />
218+
<span>标签分布明细</span>
219+
</div>
220+
}
221+
>
222+
<Table
223+
columns={columns}
224+
dataSource={tableData}
225+
rowKey={(record) => `${record.category}-${record.label}`}
226+
pagination={{
227+
pageSize: 10,
228+
showTotal: (total) => `共 ${total} 条`,
229+
showSizeChanger: true,
230+
}}
231+
size="small"
232+
/>
233+
</Card>
234+
235+
{/* 分类卡片视图 */}
236+
<div className="grid md:grid-cols-2 gap-4">
237+
{Object.entries(categoryGroups).map(([category, items]) => {
238+
const categoryTotal = items.reduce((sum, item) => sum + item.count, 0);
239+
const topLabels = items.slice(0, 5); // 只显示前5个
240+
241+
return (
242+
<Card
243+
key={category}
244+
title={<span className="font-semibold">{category}</span>}
245+
size="small"
246+
>
247+
<div className="space-y-3">
248+
<div className="text-sm text-gray-600">
249+
总计: <span className="font-semibold">{categoryTotal}</span> 个标签
250+
</div>
251+
{topLabels.map((item) => (
252+
<div key={item.label} className="space-y-1">
253+
<div className="flex justify-between text-sm">
254+
<span className="truncate flex-1" title={item.label}>
255+
{item.label}
256+
</span>
257+
<span className="font-medium ml-2">{item.count}</span>
258+
</div>
259+
<div className="w-full bg-gray-200 rounded-full h-2">
260+
<div
261+
className="bg-gradient-to-r from-blue-500 to-indigo-500 h-2 rounded-full transition-all duration-300"
262+
style={{
263+
width: `${(item.count / categoryTotal) * 100}%`,
264+
}}
265+
/>
266+
</div>
267+
</div>
268+
))}
269+
{items.length > 5 && (
270+
<div className="text-xs text-gray-500 text-center pt-2">
271+
还有 {items.length - 5} 个标签...
272+
</div>
273+
)}
274+
</div>
275+
</Card>
276+
);
277+
})}
278+
</div>
279+
</div>
280+
);
281+
}
282+
76283
export default function DataQuality(props: Props = {}) {
77-
return <DevelopmentInProgress showHome={false} />
78284
const { dataset, datasetType: propDatasetType, fileStats: propFileStats } = props;
79285

80286
// Prefer dataset fields when available, then explicit props, then sensible defaults
@@ -170,7 +376,10 @@ export default function DataQuality(props: Props = {}) {
170376
}, [datasetType, finalFileStats, completeSource]);
171377

172378
return (
173-
<div className="mt-0">
379+
<div className="mt-0 space-y-6">
380+
{/* 数据集标签统计 */}
381+
<LabelDistributionStats distribution={(dataset as any)?.distribution} />
382+
174383
<div className="grid md:grid-cols-2 gap-6">
175384
<Card title="质量分布">
176385
{metrics.map((item, index) => (

0 commit comments

Comments
 (0)