Flask项目:模糊筛选表格(DataFilterDisplay)

内容分享2周前发布
1 0 0

文件层级:
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>&copy; 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()



© 版权声明

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
none
暂无评论...