文件层级:
DataFilterDisplay
static
app.js
style.css
templates
data.html
index.html
plot.html
uplpads
a.xlsx
b.xlsx
app.py
data_tool.py
app.js
document.addEventListener('DOMContentLoaded', function () {
const uploadBox = document.getElementById('uploadBox');
const fileInput = document.getElementById('fileInput');
const fileList = document.getElementById('fileList');
const welcomeView = document.getElementById('welcomeView');
const dataView = document.getElementById('dataView');
const currentFile = document.getElementById('currentFile');
const backBtn = document.getElementById('backBtn');
const generateBtn = document.getElementById('generateBtn');
const chartContainer = document.getElementById('chartContainer');
const xColumnSelector = document.getElementById('xColumnSelector');
const yColumnSelector = document.getElementById('yColumnSelector');
let currentFileName = null;
let selectedXColumn = null;
let selectedYColumns = [];
// 初始化文件列表
loadFileList();
// 绑定事件
uploadBox.addEventListener('dragover', handleDragOver);
uploadBox.addEventListener('drop', handleDrop);
uploadBox.addEventListener('click', () => fileInput.click());
fileInput.addEventListener('change', handleFileSelect);
backBtn.addEventListener('click', resetView);
generateBtn.addEventListener('click', generateChart);
function handleDragOver(e) {
e.preventDefault();
e.stopPropagation();
uploadBox.querySelector('.upload-area').style.borderColor =
var (
--primary
)
;
}
function handleDrop(e) {
e.preventDefault();
e.stopPropagation();
uploadBox.querySelector('.upload-area').style.borderColor = '#ccc';
if (e.dataTransfer.files.length) {
fileInput.files = e.dataTransfer.files;
handleFileSelect({target: fileInput});
}
}
function handleFileSelect(e) {
const file = e.target.files[0];
if (!file) return;
const formData = new FormData();
formData.append('file', file);
fetch('/upload', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.error) throw new Error(data.error);
loadFileList();
loadFileData(data.filename);
})
.catch(error => {
alert('上传失败: ' + error.message);
});
}
function loadFileList() {
fetch('/get_files')
.then(response => response.json())
.then(files => {
fileList.innerHTML = files.map(file => `
<div class="file-item" data-file="${file}">
<span class="file-icon">📄</span>
<span class="file-name">${file}</span>
</div>
`).join('');
document.querySelectorAll('.file-item').forEach(item => {
item.addEventListener('click', function () {
loadFileData(this.dataset.file);
});
});
});
}
function loadFileData(filename) {
currentFileName = filename;
currentFile.textContent = filename;
fetch('/get_file_data', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({filename: filename})
})
.then(response => response.json())
.then(data => {
if (data.error) throw new Error(data.error);
renderColumnSelectors(data.columns);
renderTablePreview(data.preview);
welcomeView.style.display = 'none';
dataView.style.display = 'block';
})
.catch(error => {
alert('加载失败: ' + error.message);
});
}
function renderColumnSelectors(columns) {
xColumnSelector.innerHTML = '';
yColumnSelector.innerHTML = '';
selectedXColumn = null;
selectedYColumns = [];
columns.forEach(col => {
// X轴选择器(单选)
const xItem = document.createElement('div');
xItem.className = 'column-item';
xItem.textContent = col;
xItem.dataset.column = col;
xItem.addEventListener('click', function () {
document.querySelectorAll('#xColumnSelector .column-item').forEach(item => {
item.classList.remove('selected');
});
this.classList.add('selected');
selectedXColumn = col;
});
xColumnSelector.appendChild(xItem);
// Y轴选择器(多选)
const yItem = document.createElement('div');
yItem.className = 'column-item';
yItem.textContent = col;
yItem.dataset.column = col;
yItem.addEventListener('click', function () {
this.classList.toggle('selected');
if (this.classList.contains('selected')) {
selectedYColumns.push(col);
} else {
selectedYColumns = selectedYColumns.filter(c => c !== col);
}
});
yColumnSelector.appendChild(yItem);
});
}
function renderTablePreview(data) {
if (!data || data.length === 0) return;
const headers = Object.keys(data[0]);
const headerRow = document.createElement('tr');
headers.forEach(header => {
const th = document.createElement('th');
th.textContent = header;
headerRow.appendChild(th);
});
document.getElementById('tableHeader').innerHTML = '';
document.getElementById('tableHeader').appendChild(headerRow);
const tbody = document.getElementById('tableBody');
tbody.innerHTML = '';
data.forEach(row => {
const tr = document.createElement('tr');
headers.forEach(header => {
const td = document.createElement('td');
td.textContent = row[header] || '';
tr.appendChild(td);
});
tbody.appendChild(tr);
});
}
function generateChart() {
if (!selectedXColumn || selectedYColumns.length === 0) {
alert('请选择X轴和至少一个Y轴列');
return;
}
fetch('/generate_chart', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
filename: currentFileName,
x_column: selectedXColumn,
y_columns: selectedYColumns
})
})
.then(response => response.json())
.then(data => {
if (data.error) throw new Error(data.error);
chartContainer.innerHTML = `<img src="data:image/png;base64,${data.chart}">`;
chartContainer.scrollIntoView({behavior: 'smooth'});
})
.catch(error => {
alert('图表生成失败: ' + error.message);
});
}
function resetView() {
welcomeView.style.display = 'block';
dataView.style.display = 'none';
chartContainer.innerHTML = '';
currentFileName = null;
}
});
style.css
body {
background-color: #f8f9fa;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.card {
border-radius: 12px;
border: none;
transition: transform 0.3s, box-shadow 0.3s;
height: 100%;
}
.card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
}
.card-header {
border-radius: 12px 12px 0 0 !important;
padding: 15px 20px;
}
.file-item {
background-color: #f8f9fa;
transition: background-color 0.2s;
}
.file-item:hover {
background-color: #e9ecef;
}
.form-check-input:checked {
background-color: #0d6efd;
border-color: #0d6efd;
}
.btn {
border-radius: 8px;
padding: 10px 20px;
font-weight: 500;
transition: all 0.3s;
}
#generate-btn {
background-color: #ffc107;
border-color: #ffc107;
color: #212529;
}
#generate-btn:hover {
background-color: #e0a800;
border-color: #e0a800;
transform: scale(1.02);
}
#analyze-btn {
transition: all 0.3s;
}
#chart-container {
min-height: 350px;
display: flex;
align-items: center;
justify-content: center;
background-color: #f8f9fa;
border-radius: 8px;
}
.placeholder-content {
color: #6c757d;
}
.chart-image {
animation: fadeIn 0.5s;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.form-select:disabled {
background-color: #e9ecef;
cursor: not-allowed;
}
.badge {
font-size: 0.8em;
padding: 5px 8px;
}
data.html
body {
background-color: #f8f9fa;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.card {
border-radius: 12px;
border: none;
transition: transform 0.3s, box-shadow 0.3s;
height: 100%;
}
.card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
}
.card-header {
border-radius: 12px 12px 0 0 !important;
padding: 15px 20px;
}
.file-item {
background-color: #f8f9fa;
transition: background-color 0.2s;
}
.file-item:hover {
background-color: #e9ecef;
}
.form-check-input:checked {
background-color: #0d6efd;
border-color: #0d6efd;
}
.btn {
border-radius: 8px;
padding: 10px 20px;
font-weight: 500;
transition: all 0.3s;
}
#generate-btn {
background-color: #ffc107;
border-color: #ffc107;
color: #212529;
}
#generate-btn:hover {
background-color: #e0a800;
border-color: #e0a800;
transform: scale(1.02);
}
#analyze-btn {
transition: all 0.3s;
}
#chart-container {
min-height: 350px;
display: flex;
align-items: center;
justify-content: center;
background-color: #f8f9fa;
border-radius: 8px;
}
.placeholder-content {
color: #6c757d;
}
.chart-image {
animation: fadeIn 0.5s;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.form-select:disabled {
background-color: #e9ecef;
cursor: not-allowed;
}
.badge {
font-size: 0.8em;
padding: 5px 8px;
}
index.html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Excel数据可视化工具</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
:root {
--primary: #4e73df;
--secondary: #858796;
--success: #1cc88a;
--warning: #f6c23e;
--danger: #e74a3b;
--light: #f8f9fc;
--dark: #5a5c69;
}
body {
background: linear-gradient(135deg, #f5f7fa 0%, #e4e7f1 100%);
font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
min-height: 100vh;
padding-bottom: 50px;
}
.navbar {
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
background: linear-gradient(90deg, var(--primary) 0%, #2a4cb3 100%);
}
.card {
border-radius: 10px;
border: none;
box-shadow: 0 0.15rem 1.75rem 0 rgba(58, 59, 69, 0.15);
transition: all 0.3s;
overflow: hidden;
margin-bottom: 25px;
background-color: white;
}
.card:hover {
transform: translateY(-5px);
box-shadow: 0 0.5rem 2rem rgba(58, 59, 69, 0.2);
}
.card-header {
background: linear-gradient(90deg, var(--primary) 0%, #3a5bdc 100%);
color: white;
font-weight: 600;
padding: 15px 20px;
border-bottom: none;
border-radius: 10px 10px 0 0 !important;
}
.btn-primary {
background: linear-gradient(90deg, var(--primary) 0%, #3a5bdc 100%);
border: none;
transition: all 0.3s;
}
.btn-primary:hover {
background: linear-gradient(90deg, #3a5bdc 0%, #2a4cb3 100%);
transform: translateY(-2px);
}
.btn-success {
background: linear-gradient(90deg, var(--success) 0%, #17a673 100%);
border: none;
}
.btn-warning {
background: linear-gradient(90deg, var(--warning) 0%, #e0a800 100%);
border: none;
color: #212529;
font-weight: 600;
}
.file-item {
background-color: #f8f9fc;
border-radius: 8px;
padding: 10px 15px;
margin-bottom: 10px;
display: flex;
align-items: center;
transition: all 0.2s;
border: 1px solid #e3e6f0;
}
.file-item:hover {
background-color: #eef2f9;
border-color: #d1d3e2;
}
.file-info {
flex-grow: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.file-name {
font-weight: 500;
color: #4e73df;
}
.file-size {
font-size: 0.85em;
color: #858796;
}
#chart-container {
min-height: 400px;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #f8f9fc 0%, #eef2f9 100%);
border-radius: 10px;
border: 1px dashed #d1d3e2;
}
.chart-placeholder {
text-align: center;
color: #858796;
padding: 30px;
}
.chart-placeholder i {
font-size: 4rem;
margin-bottom: 15px;
color: #d1d3e2;
}
.filter-section {
background-color: #f8f9fc;
border-radius: 8px;
padding: 15px;
margin-bottom: 15px;
}
.filter-title {
display: flex;
align-items: center;
margin-bottom: 10px;
font-weight: 600;
color: var(--dark);
}
.filter-badge {
width: 28px;
height: 28px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-right: 10px;
font-weight: bold;
}
.column-item {
display: inline-block;
background-color: #eef2f9;
border-radius: 20px;
padding: 5px 15px;
margin: 0 5px 10px 0;
cursor: pointer;
transition: all 0.2s;
border: 2px solid transparent;
}
.column-item:hover {
background-color: #dbe5ff;
}
.column-item.selected {
background-color: var(--primary);
color: white;
border-color: #3a5bdc;
}
.footer {
text-align: center;
padding: 20px;
color: #6e707e;
font-size: 0.9rem;
margin-top: 30px;
}
.upload-area {
border: 2px dashed #d1d3e2;
border-radius: 10px;
padding: 25px;
text-align: center;
background-color: #f8f9fc;
transition: all 0.3s;
margin-bottom: 20px;
cursor: pointer;
}
.upload-area:hover {
border-color: var(--primary);
background-color: #eef2f9;
}
.upload-icon {
font-size: 3rem;
color: #d1d3e2;
margin-bottom: 15px;
}
.step-indicator {
display: flex;
justify-content: space-between;
margin-bottom: 25px;
position: relative;
}
.step-indicator::before {
content: '';
position: absolute;
top: 20px;
left: 0;
right: 0;
height: 2px;
background-color: #e3e6f0;
z-index: 1;
}
.step {
text-align: center;
z-index: 2;
position: relative;
flex: 1;
}
.step-circle {
width: 40px;
height: 40px;
border-radius: 50%;
background-color: #e3e6f0;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 10px;
font-weight: bold;
color: #6e707e;
border: 2px solid white;
}
.step.active .step-circle {
background-color: var(--primary);
color: white;
}
.step-label {
font-size: 0.85rem;
color: #858796;
}
.step.active .step-label {
color: var(--primary);
font-weight: 500;
}
</style>
</head>
<body>
<!-- 导航栏 -->
<nav class="navbar navbar-expand-lg navbar-dark">
<div class="container">
<a class="navbar-brand" href="#">
<i class="fas fa-chart-line me-2"></i>
Excel数据可视化工具
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
</div>
</nav>
<div class="container py-4">
<!-- 步骤指示器 -->
<div class="step-indicator">
<div class="step active">
<div class="step-circle">1</div>
<div class="step-label">上传文件</div>
</div>
<div class="step">
<div class="step-circle">2</div>
<div class="step-label">选择数据</div>
</div>
<div class="step">
<div class="step-circle">3</div>
<div class="step-label">筛选数据</div>
</div>
<div class="step">
<div class="step-circle">4</div>
<div class="step-label">生成图表</div>
</div>
</div>
<div class="row">
<!-- 左侧面板:文件管理 -->
<div class="col-lg-4">
<div class="card">
<div class="card-header">
<i class="fas fa-file-excel me-2"></i>文件管理
</div>
<div class="card-body">
<!-- 文件上传区域 -->
<div class="upload-area" id="drop-area">
<div class="upload-icon">
<i class="fas fa-cloud-upload-alt"></i>
</div>
<h5>拖放Excel文件到此处</h5>
<p class="text-muted small mb-3">或点击选择文件 (支持 .xlsx, .xls)</p>
<form id="upload-form" method="post" enctype="multipart/form-data">
<div class="mb-3">
<input class="form-control d-none" type="file" name="files" id="file-input" multiple accept=".xlsx,.xls">
<button type="button" class="btn btn-primary" id="browse-btn">
<i class="fas fa-folder-open me-2"></i>浏览文件
</button>
</div>
<button type="submit" class="btn btn-success w-100">
<i class="fas fa-upload me-2"></i>上传文件
</button>
</form>
</div>
<!-- 已上传文件列表 -->
<div class="mt-4">
<h6 class="fw-bold mb-3">
<i class="fas fa-list me-2"></i>已上传文件
<span class="badge bg-primary float-end" id="file-count">{{ files|length }}</span>
</h6>
<div id="file-list">
{% for file in files %}
<div class="file-item">
<div class="form-check">
<input type="checkbox" class="form-check-input file-checkbox" value="{{ file.name }}" id="file-{{ loop.index }}">
</div>
<div class="file-info">
<div class="file-name">{{ file.name }}</div>
<div class="file-size">{{ file.size }}</div>
</div>
</div>
{% else %}
<div class="text-center py-4 text-muted">
<i class="fas fa-inbox fa-2x mb-2"></i>
<p>暂无上传文件</p>
</div>
{% endfor %}
</div>
<button id="analyze-btn" class="btn btn-primary w-100 mt-3" disabled>
<i class="fas fa-search me-2"></i>分析选中的文件
</button>
</div>
</div>
</div>
</div>
<!-- 中间面板:数据选择和筛选 -->
<div class="col-lg-4">
<div class="card">
<div class="card-header">
<i class="fas fa-sliders-h me-2"></i>数据选择和筛选
</div>
<div class="card-body">
<!-- 数据列选择 -->
<div class="mb-4">
<h6 class="fw-bold mb-3">
<i class="fas fa-columns me-2"></i>选择要对比数据列
</h6>
<p class="text-muted small">选择需要对比的数据列(可多选)</p>
<div id="columns-container" class="d-flex flex-wrap gap-2 mb-3"></div>
<div class="alert alert-info d-flex align-items-center">
<i class="fas fa-info-circle me-2"></i>
<span>已选择 <span id="selected-columns-count">0</span> 个数据列</span>
</div>
</div>
<!-- 数据筛选器 -->
<div class="mb-4">
<h6 class="fw-bold mb-3">
<i class="fas fa-filter me-2"></i>数据筛选
</h6>
<!-- 筛选器1 -->
<div class="filter-section">
<div class="filter-title">
<div class="filter-badge bg-primary">1</div>
<span>第一筛选条件必填项(选择一个ADCU版本)</span>
</div>
<div class="mb-3">
<label class="form-label">选择筛选列</label>
<select id="filter-col1" class="form-select mb-2" disabled>
<option value="">-- 请选择列 --</option>
</select>
</div>
<div class="mb-3">
<label class="form-label">选择筛选值</label>
<select id="filter-val1" class="form-select" disabled>
<option value="">-- 请选择值 --</option>
</select>
</div>
</div>
<!-- 筛选器2 -->
<div class="filter-section">
<div class="filter-title">
<div class="filter-badge bg-success">2</div>
<span>第二筛选条件</span>
</div>
<div class="mb-3">
<label class="form-label">选择筛选列</label>
<select id="filter-col2" class="form-select mb-2" disabled>
<option value="">-- 请选择列 --</option>
</select>
</div>
<div class="mb-3">
<label class="form-label">选择筛选值</label>
<select id="filter-val2" class="form-select" disabled>
<option value="">-- 请选择值 --</option>
</select>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 右侧面板:图表展示 -->
<div class="col-lg-4">
<div class="card">
<div class="card-header">
<i class="fas fa-chart-bar me-2"></i>图表展示
</div>
<div class="card-body">
<div id="chart-container">
<div class="chart-placeholder">
<i class="fas fa-chart-bar"></i>
<h5>图表将在这里显示</h5>
<p class="text-muted small mt-2">请选择文件并配置数据后生成图表</p>
</div>
</div>
<div class="d-grid mt-3">
<button id="generate-btn" class="btn btn-warning w-100" disabled>
<i class="fas fa-chart-bar me-2"></i>生成数据图表
</button>
</div>
</div>
</div>
</div>
</div>
<!-- 页脚 -->
<div class="footer">
<p>© 2025 Excel数据可视化工具 | 基于Python Flask构建 | 支持Excel数据分析和可视化</p>
</div>
</div>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
$(document).ready(function() {
// 初始化步骤指示器
updateStepIndicator(1);
// 文件上传区域交互
$('#browse-btn').click(function() {
$('#file-input').click();
});
// 拖放功能
const dropArea = $('#drop-area');
dropArea.on('dragover', function(e) {
e.preventDefault();
dropArea.css('border-color', '#4e73df');
dropArea.css('background-color', '#eef2f9');
});
dropArea.on('dragleave', function(e) {
e.preventDefault();
dropArea.css('border-color', '#d1d3e2');
dropArea.css('background-color', '#f8f9fc');
});
dropArea.on('drop', function(e) {
e.preventDefault();
dropArea.css('border-color', '#d1d3e2');
dropArea.css('background-color', '#f8f9fc');
const files = e.originalEvent.dataTransfer.files;
if (files.length > 0) {
// 更新文件输入
const fileInput = document.getElementById('file-input');
fileInput.files = files;
// 显示文件数量
$('#browse-btn').html(`<i class="fas fa-folder-open me-2"></i>已选择 ${files.length} 个文件`);
}
});
// 文件上传处理
$('#upload-form').on('submit', function(e) {
e.preventDefault();
// 显示上传状态
const originalBtnText = $('button[type="submit"]').html();
$('button[type="submit"]').html('<i class="fas fa-spinner fa-spin me-2"></i>上传中...').prop('disabled', true);
var formData = new FormData(this);
$.ajax({
url: '/upload',
type: 'POST',
data: formData,
contentType: false,
processData: false,
success: function() {
location.reload();
},
error: function() {
alert('文件上传失败,请重试');
$('button[type="submit"]').html(originalBtnText).prop('disabled', false);
}
});
});
// 文件选择处理
$(document).on('change', '.file-checkbox', function() {
const anyChecked = $('.file-checkbox:checked').length > 0;
$('#analyze-btn').prop('disabled', !anyChecked);
// 更新步骤
if (anyChecked) updateStepIndicator(2);
else updateStepIndicator(1);
});
// 分析按钮点击处理
$('#analyze-btn').click(function() {
const selectedFiles = getSelectedFiles();
if (selectedFiles.length === 0) {
alert('请至少选择一个文件');
return;
}
// 显示分析状态
const originalBtnText = $(this).html();
$(this).html('<i class="fas fa-spinner fa-spin me-2"></i>分析中...').prop('disabled', true);
$.ajax({
url: '/get_common_columns',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({
selected_files: selectedFiles
}),
success: function(response) {
displayColumns(response.columns);
populateFilterColumns(response.columns);
$('#generate-btn').prop('disabled', false);
// 更新步骤
updateStepIndicator(3);
},
error: function(xhr) {
alert('获取共有列失败: ' + xhr.responseText);
},
complete: function() {
$('#analyze-btn').html(originalBtnText).prop('disabled', false);
}
});
});
// 显示共有列
function displayColumns(columns) {
$('#columns-container').empty();
if (columns && columns.length > 0) {
columns.forEach(function(col) {
$('#columns-container').append(`
<div class="column-item" data-column="${col}">
${col}
</div>
`);
});
// 添加列选择事件
$('.column-item').click(function() {
$(this).toggleClass('selected');
updateSelectedColumnsCount();
});
} else {
$('#columns-container').html('<div class="alert alert-warning">没有找到共有列</div>');
}
}
// 更新已选列计数
function updateSelectedColumnsCount() {
const count = $('.column-item.selected').length;
$('#selected-columns-count').text(count);
// 更新步骤
if (count > 0) updateStepIndicator(4);
else updateStepIndicator(3);
}
// 填充筛选列下拉框
function populateFilterColumns(columns) {
$('#filter-col1, #filter-col2').empty().append('<option value="">-- 请选择列 --</option>');
if (columns && columns.length > 0) {
columns.forEach(function(col) {
$('#filter-col1').append(`<option value="${col}">${col}</option>`);
$('#filter-col2').append(`<option value="${col}">${col}</option>`);
});
$('#filter-col1, #filter-col2').prop('disabled', false);
}
}
// 筛选列1改变时加载唯一值
$('#filter-col1').change(function() {
const selectedFiles = getSelectedFiles();
const col = $(this).val();
if (col && selectedFiles.length > 0) {
loadUniqueValues(col, '#filter-val1');
} else {
$('#filter-val1').empty().append('<option value="">-- 请选择值 --</option>').prop('disabled', true);
}
});
// 筛选列2改变时加载唯一值
$('#filter-col2').change(function() {
const selectedFiles = getSelectedFiles();
const col = $(this).val();
if (col && selectedFiles.length > 0) {
loadUniqueValues(col, '#filter-val2');
} else {
$('#filter-val2').empty().append('<option value="">-- 请选择值 --</option>').prop('disabled', true);
}
});
// 加载唯一值
function loadUniqueValues(column, targetSelector) {
const selectedFiles = getSelectedFiles();
// 显示加载状态
$(targetSelector).html('<option value="">加载中...</option>').prop('disabled', true);
$.ajax({
url: '/get_unique_values',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({
selected_files: selectedFiles,
column_name: column
}),
success: function(response) {
$(targetSelector).empty().append('<option value="">-- 请选择值 --</option>');
if (response.values && response.values.length > 0) {
response.values.forEach(function(val) {
$(targetSelector).append(`<option value="${val}">${val}</option>`);
});
$(targetSelector).prop('disabled', false);
} else {
$(targetSelector).append('<option value="">没有可用值</option>').prop('disabled', true);
}
},
error: function() {
alert('获取唯一值失败');
}
});
}
// 获取选中的文件
function getSelectedFiles() {
const selectedFiles = [];
$('.file-checkbox:checked').each(function() {
selectedFiles.push($(this).val());
});
return selectedFiles;
}
// 生成图表按钮点击处理
$('#generate-btn').click(function() {
const selectedFiles = getSelectedFiles();
const selectedColumns = [];
$('.column-item.selected').each(function() {
selectedColumns.push($(this).data('column'));
});
if (selectedColumns.length === 0) {
alert('请至少选择一个X轴数据列');
return;
}
const filterCol1 = $('#filter-col1').val();
const filterVal1 = $('#filter-val1').val();
const filterCol2 = $('#filter-col2').val();
const filterVal2 = $('#filter-val2').val();
// 显示加载状态
const originalBtnText = $(this).html();
$(this).html('<i class="fas fa-spinner fa-spin me-2"></i>生成中...').prop('disabled', true);
$.ajax({
url: '/generate_chart',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({
selected_files: selectedFiles,
x_columns: selectedColumns,
filter_col1: filterCol1,
filter_val1: filterVal1,
filter_col2: filterCol2,
filter_val2: filterVal2
}),
success: function(response) {
if (response.chart_url) {
$('#chart-container').html(`
<div class="chart-image">
<img src="${response.chart_url}?t=${new Date().getTime()}">`);
} else {
alert('图表生成失败');
}
},
error: function(xhr) {
if (xhr.status === 400) {
alert(xhr.responseJSON.error);
} else {
alert('图表生成失败');
}
},
complete: function() {
$('#generate-btn').html(originalBtnText).prop('disabled', false);
}
});
});
// 更新步骤指示器
function updateStepIndicator(step) {
$('.step').removeClass('active');
$('.step:nth-child(' + step + ')').addClass('active');
}
});
// 下载图表函数
function downloadChart() {
const link = document.createElement('a');
link.href = $('.chart-image img').attr('src');
link.download = 'data_chart.png';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
</script>
</body>
</html>
plot.html
<!DOCTYPE html>
<html>
<head>
<title>数据可视化</title>
</head>
<body>
<h1>柱状图展示</h1>
<img src="data:image/png;base64,{{ plot_url }}" alt="柱状图">
<p><a href="{{ url_for('show_data', filename=filename) }}">返回数据</a></p>
</body>
</html>
app.py
import os
import openpyxl
import numpy as np
import pandas as pd
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from flask import Flask, render_template, request, redirect, url_for, send_from_directory
from werkzeug.utils import secure_filename
from pathlib import Path
# 固定X轴的标签
filter_label = ["状态异常", "纵向异常", "横向异常", "变道异常", "进出匝道异常", "HMI异常", "避让障碍物异常",
"过路口通行异常", "红绿灯异常"]
# 固定测试功能
test_function = ["ACC", "HNOA", "ICC"]
# 版本号列表
# ADCU_list = ["3.3.5B18"]
app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = 'uploads'
app.config['ALLOWED_EXTENSIONS'] = {'xlsx', 'xls'}
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16MB
# 确保上传目录存在
os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
def concat_with_plus(string_list):
result = string_list[0]
for s in string_list[1:]:
result += "+" + s
return result
def get_visible_sheets(file_path):
"""
获取Excel文件中所有可见的工作表
"""
visible_sheets = []
with pd.ExcelFile(file_path) as xl:
if isinstance(xl.book, openpyxl.workbook.workbook.Workbook):
for sheet in xl.book.worksheets:
if sheet.sheet_state == 'visible':
visible_sheets.append(sheet.title)
else:
visible_sheets = xl.sheet_names # 如果不是openpyxl格式则返回所有sheet
return visible_sheets
def load_and_process(file_path, x_col, value_cols, filter_col1, filter_val1, filter_col2, filter_val2):
"""
过滤Excel中所需内容
"""
visible_sheets = get_visible_sheets(file_path)
# 拿到可见工作表
df = pd.read_excel(file_path, sheet_name=visible_sheets[0])
df = df[df["模块"] == "行车"]
# 筛选X轴的标签
# actual_subset = list(x_col)
actual_subset = filter_label
# 过滤空值并合并重复项
try:
df = df.dropna(subset=actual_subset)
except KeyError as e:
print(f"无效列名: {e}")
# 过滤指定问题标签下的数据
df = df[df["问题标签"].isin(filter_label)]
print("filter_col1", filter_col1, type(filter_col1))
print("filter_val1", filter_val1, type(filter_val1))
# 过滤指定测试功能下的数据
df = df[df[filter_col1].isin(filter_val1)]
print("filter_col2", filter_col2, type(filter_col2))
print("filter_val2", filter_val2, type(filter_val2))
df = df[df[filter_col2].isin(filter_val2)]
grouped = df.groupby(x_col)[value_cols].count()
print("grouped", grouped)
return grouped
def allowed_file(filename):
# 检查上传文件
return '.' in filename and
filename.rsplit('.', 1)[1].lower() in app.config['ALLOWED_EXTENSIONS']
def get_common_columns(filenames):
# 获取公共列名
common_columns = None
for filename in filenames:
file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
visible_sheets = get_visible_sheets(file_path)
try:
df = pd.read_excel(file_path, sheet_name=visible_sheets[0])
if common_columns is None:
common_columns = set(df.columns)
else:
common_columns = common_columns.intersection(set(df.columns))
except Exception as e:
print(f"Error reading file {filename}: {e}")
continue
return list(common_columns) if common_columns else []
def get_unique_values(column_name, filenames):
# 获取excel表中唯一值
unique_values = set()
for filename in filenames:
file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
visible_sheets = get_visible_sheets(file_path)
try:
df = pd.read_excel(file_path, sheet_name=visible_sheets[0])
if column_name in df.columns:
# 处理非字符串类型
col_data = df[column_name].dropna().astype(str)
unique_values.update(col_data.unique())
except Exception as e:
print(f"Error reading file {filename}: {e}")
continue
return sorted(unique_values)
def generate_histogram(data):
# 构造配置参数
files = {}
for filename in data['selected_files']:
files[Path(filename).stem] = os.path.join(app.config['UPLOAD_FOLDER'], filename)
try:
x_column = '问题标签' # X轴列名
value_columns = data['x_columns'] # 需要对比的列
# 处理数据
results = {}
for ver, path in files.items():
print("ver---path----", ver, path)
# 应用筛选条件
if data["filter_col1"] and data["filter_val1"] and data["filter_col2"] and data["filter_val2"]:
print(11111111111)
filter_col1 = data["filter_col1"]
filter_val1 = [data["filter_val1"]]
filter_col2 = data["filter_col2"]
filter_val2 = [data["filter_val2"]]
results[ver] = load_and_process(path, x_column, value_columns, filter_col1, filter_val1, filter_col2,
filter_val2)
elif data["filter_col1"] and data["filter_val1"]:
print(22222222222)
print("filter_val2", data["filter_val2"])
print("filter_col2", data["filter_col2"])
filter_col1 = data["filter_col1"]
filter_val1 = [data["filter_val1"]]
filter_col2 = "测试功能"
filter_val2 = ["ACC", "HNOA", "ICC"]
results[ver] = load_and_process(path, x_column, value_columns, filter_col1, filter_val1, filter_col2,
filter_val2)
# print("first_result", results)
# 统一X轴类别
all_categories = sorted(set(results['P181'].index).union(set(results['E245'].index)))
print("all_categories", all_categories)
# 创建图形
plt.rcParams['font.family'] = 'SimHei'
fig, ax = plt.subplots(figsize=(15, 8))
colors = plt.cm.tab20.colors
bar_width = 0.35
x = np.arange(len(all_categories))
print("second_result", results)
# 计算总数用于百分比
total_counts = {ver: data.sum().values for ver, data in results.items()}
filter_val1 = data["filter_val1"]
test_str = concat_with_plus(test_function)
filter_val2 = data["filter_val2"] if data["filter_val2"] else test_str
# 绘制对比条形图
for i, col in enumerate(value_columns):
for j, (ver, data) in enumerate(results.items()):
pos = x + (i - len(value_columns) / 2 + 0.5) * bar_width / len(value_columns)
counts = data.reindex(all_categories)[col].fillna(0)
print("counts", counts)
bars = ax.bar(pos + j * bar_width, counts,
width=bar_width / len(value_columns),
color=colors[i * 2 + j],
label=f'{ver}-{col}-{filter_val1}')
# 添加数值和百分比标签
for bar in bars:
height = bar.get_height()
if height > 0:
percent = height / total_counts[ver] * 100
ax.text(bar.get_x() + bar.get_width() / 2., height,
f'{int(height)}
{percent[0]:.1f}%',
ha='center', va='bottom', fontsize=8)
# 图表装饰
ax.set_xticks(x + bar_width / 2)
ax.set_xticklabels(all_categories, rotation=45, ha='right')
ax.set_xlabel(x_column)
ax.set_ylabel('数量')
ax.set_title(filter_val2 + '-问题标签分布')
ax.legend(bbox_to_anchor=(1.05, 1))
plt.tight_layout()
# 保存图表
chart_path = os.path.join('static', 'chart.png')
plt.savefig(chart_path, dpi=300)
plt.close()
return chart_path
except Exception as e:
print("eeeee", e)
return e
# def generate_histogram(filenames, x_columns, filter_col1, filter_val1, filter_col2, filter_val2):
# # 收集所有数据生成直方图
# all_data = []
# for filename in filenames:
# file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
# try:
# df = pd.read_excel(file_path)
#
# # 应用筛选条件
# if filter_col1 and filter_val1 and filter_col1 in df.columns:
# df = df[df[filter_col1].astype(str) == filter_val1]
# if filter_col2 and filter_val2 and filter_col2 in df.columns:
# df = df[df[filter_col2].astype(str) == filter_val2]
#
# all_data.append(df)
# except Exception as e:
# print(f"Error reading file {filename}: {e}")
# continue
#
# if not all_data:
# return None
#
# combined_df = pd.concat(all_data)
#
# # 如果选择了多个X轴列,组合它们
# if x_columns:
# if len(x_columns) > 1:
# combined_df['Combined_X'] = combined_df[x_columns].apply(
# lambda x: ' | '.join(x.dropna().astype(str)), axis=1
# )
# x_col = 'Combined_X'
# else:
# x_col = x_columns[0]
#
# # 创建直方图
# plt.rcParams['font.family'] = 'SimHei'
# plt.figure(figsize=(12, 8))
# counts = combined_df[x_col].value_counts()
#
# if len(counts) > 20: # 如果类别太多,只取前20
# counts = counts.head(20)
#
# ax = counts.plot(kind='bar', color='#4e73df')
# plt.title('数据分布直方图', fontsize=16)
# plt.xlabel(x_col, fontsize=14)
# plt.ylabel('数量', fontsize=14)
# plt.xticks(rotation=45, ha='right', fontsize=12)
# plt.grid(axis='y', linestyle='--', alpha=0.7)
#
# # 添加数据标签
# for i, v in enumerate(counts):
# ax.text(i, v + 0.5, str(v), ha='center', fontsize=10)
#
# plt.tight_layout()
#
# # 保存图表
# chart_path = os.path.join('static', 'chart.png')
# plt.savefig(chart_path)
# plt.close()
#
# return chart_path
#
# return None
@app.route('/')
def index():
# 获取已上传文件列表
files = []
for filename in os.listdir(app.config['UPLOAD_FOLDER']):
if allowed_file(filename):
file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
file_size = os.path.getsize(file_path) // 1024 # KB
files.append({
'name': filename,
'size': f"{file_size} KB",
'upload_time': os.path.getctime(file_path)
})
# 按上传时间排序(最新在前)
files.sort(key=lambda x: x['upload_time'], reverse=True)
return render_template('index.html', files=files)
@app.route('/upload', methods=['POST'])
def upload_file():
if 'files' not in request.files:
return redirect(request.url)
files = request.files.getlist('files')
for file in files:
if file.filename == '':
continue
if file and allowed_file(file.filename):
filename = secure_filename(file.filename)
file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
return redirect(url_for('index'))
@app.route('/get_common_columns', methods=['POST'])
def get_common_columns_route():
data = request.get_json()
selected_files = data.get('selected_files', [])
common_columns = get_common_columns(selected_files)
return {'columns': common_columns}
@app.route('/get_unique_values', methods=['POST'])
def get_unique_values_route():
data = request.get_json()
print("unique_data", data)
selected_files = data.get('selected_files', [])
column_name = data.get('column_name', '')
unique_values = get_unique_values(column_name, selected_files)
return {'values': unique_values}
@app.route('/generate_chart', methods=['POST'])
def generate_chart():
data = request.get_json()
print("generate_data", data)
chart_path = generate_histogram(data)
if chart_path:
return {'chart_url': url_for('static', filename='chart.png')}
else:
return {'error': '无法生成图表,请检查数据'}, 400
if __name__ == '__main__':
app.run(host="0.0.0.0", port=5000, debug=True)
data_tool.py
import openpyxl
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
# 固定X轴的标签
filter_label = ["状态异常", "纵向异常", "横向异常", "变道异常", "进出匝道异常", "HMI异常", "避让障碍物异常",
"过路口通行异常", "红绿灯异常"]
# 固定测试功能
# test_function = ["ACC", "HNOA", "ICC"]
test_function = ["HNOA"]
# 版本号列表
ADCU_list = ["3.3.5B18"]
def concat_with_plus(string_list):
result = string_list[0]
for s in string_list[1:]:
result += "+" + s
return result
def get_visible_sheets(file_path):
"""
获取Excel文件中所有可见的工作表
"""
visible_sheets = []
with pd.ExcelFile(file_path) as xl:
if isinstance(xl.book, openpyxl.workbook.workbook.Workbook):
for sheet in xl.book.worksheets:
if sheet.sheet_state == 'visible':
visible_sheets.append(sheet.title)
else:
visible_sheets = xl.sheet_names # 如果不是openpyxl格式则返回所有sheet
return visible_sheets
# plt.rcParams['font.family'] = 'SimHei'
def load_and_process(file_path, x_col, value_cols):
"""
过滤Excel中所需内容
"""
visible_sheets = get_visible_sheets(file_path)
# 拿到可见工作表
df = pd.read_excel(file_path, sheet_name=visible_sheets[0])
df = df[df["模块"] == "行车"]
# 筛选X轴的标签
# actual_subset = list(x_col)
actual_subset = filter_label
# 过滤空值并合并重复项
try:
df = df.dropna(subset=actual_subset)
except KeyError as e:
print(f"无效列名: {e}")
# 过滤指定问题标签下的数据
df = df[df["问题标签"].isin(filter_label)]
print("first_df", df)
# 过滤指定测试功能下的数据
df = df[df["ADCU版本"].isin(ADCU_list)]
df = df[df["测试功能"].isin(test_function)]
print("second_df", df)
print("value_cols", value_cols)
grouped = df.groupby(x_col)[value_cols].count()
print("grouped", grouped)
return grouped
# 配置参数
files = {
'P181': r'E:GeelyDataFilterDisplayuploadsP181.xlsx',
'E245': r'E:GeelyDataFilterDisplayuploadsE245.xlsx'
}
x_column = '问题标签' # X轴列名
value_columns = ['ADCU版本'] # 需要对比的列
# 处理数据
results = {}
for ver, path in files.items():
results[ver] = load_and_process(path, x_column, value_columns)
# 统一X轴类别
all_categories = sorted(set(results['P181'].index).union(set(results['E245'].index)))
# 创建图形
plt.rcParams['font.family'] = 'SimHei'
fig, ax = plt.subplots(figsize=(15, 8))
colors = plt.cm.tab20.colors
bar_width = 0.35
x = np.arange(len(all_categories))
# 计算总数用于百分比
total_counts = {ver: data.sum().values for ver, data in results.items()}
# 绘制对比条形图
for i, col in enumerate(value_columns):
for j, (ver, data) in enumerate(results.items()):
pos = x + (i - len(value_columns) / 2 + 0.5) * bar_width / len(value_columns)
counts = data.reindex(all_categories)[col].fillna(0)
print("counts", counts)
bars = ax.bar(pos + j * bar_width, counts,
width=bar_width / len(value_columns),
color=colors[i * 2 + j],
label=f'{ver}-{col}-{ADCU_list[0]}')
# 添加数值标签
# ax.bar_label(bars, padding=3, fmt='%.0f')
# 添加数值和百分比标签
print("bars", bars)
for bar in bars:
height = bar.get_height()
if height > 0:
percent = height / total_counts[ver] * 100
ax.text(bar.get_x() + bar.get_width() / 2., height,
f'{int(height)}
{percent[0]:.1f}%',
ha='center', va='bottom', fontsize=8)
# 图表装饰
ax.set_xticks(x + bar_width / 2)
ax.set_xticklabels(all_categories, rotation=45, ha='right')
ax.set_xlabel(x_column)
ax.set_ylabel('数量')
test_str = concat_with_plus(test_function)
ax.set_title(test_str + '-问题标签分布')
ax.legend(bbox_to_anchor=(1.05, 1))
plt.tight_layout()
plt.savefig('comparison_result.png', dpi=300)
plt.show()
© 版权声明
文章版权归作者所有,未经允许请勿转载。
相关文章
暂无评论...





