feat: WIP Overview Hold Summary 改為柏拉圖視覺化

將 Hold Summary 從單一表格改為兩張柏拉圖呈現品質異常與非品質異常 Hold 分佈,
便於快速辨識主要原因並支援 80/20 法則分析。

- 新增 ECharts 柏拉圖:Y軸(左)為 QTY 柱狀圖、Y軸(右)為累計百分比折線
- 每張圖下方保留摘要表格顯示 Hold Reason、Lots、QTY、累計%
- 保留點擊 drill-down 至 Hold Detail 頁面功能
- 響應式設計:大螢幕並排、小螢幕堆疊

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
beabigegg
2026-02-02 19:03:02 +08:00
parent ef83060109
commit bd3591703b
7 changed files with 649 additions and 21 deletions

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-02-02

View File

@@ -0,0 +1,73 @@
## Context
WIP Overview 頁面目前以表格呈現 Hold Summary資料來自 `/api/wip/overview/hold` API回傳包含 `holdType: 'quality' | 'non-quality'` 分類。現有實作使用 `renderHold()` 函數動態產生表格 HTML。
專案已整合 ECharts 圖表庫(`/static/js/echarts.min.js`),在 `resource_history.html` 有完整使用範例。
## Goals / Non-Goals
**Goals:**
- 以柏拉圖呈現 Hold 分佈,快速辨識主要原因
- 分離品質/非品質異常為兩張獨立圖表
- 保留 drill-down 至 Hold Detail 頁面功能
- 在圖表下方提供摘要表格顯示精確數據
**Non-Goals:**
- 不修改後端 API資料結構已足夠
- 不新增其他圖表類型
- 不改變 Hold Detail 頁面
## Decisions
### 1. 圖表庫選擇:使用 ECharts
**決定**:沿用專案已整合的 ECharts
**理由**
- 已在 `resource_history.html` 驗證可行
- 無需額外依賴
- 支援 bar + line 組合圖(柏拉圖標準呈現)
### 2. 資料分組策略:前端過濾
**決定**:在前端使用 `filter()``holdType` 分組
**理由**
- API 已回傳 `holdType` 欄位
- 避免增加後端 API 複雜度
- 資料量小(通常 < 50 前端處理無效能疑慮
### 3. 累計百分比計算:以 QTY 為基準
**決定**柏拉圖 Y 軸顯示 QTY累計線以 QTY 累計百分比計算
**理由**
- QTY 更能反映實際影響規模
- 柱狀圖與累計線使用相同基準QTY圖表邏輯一致
- Lots 數量僅在下方摘要表格作為參考資訊呈現
### 4. Drill-down 實作ECharts click 事件
**決定**使用 ECharts `on('click')` 事件導向 Hold Detail 頁面
**理由**
- 與現有表格連結行為一致
- 使用者可點擊柱狀圖或摘要表格連結
### 5. 版面配置:兩欄並排
**決定**品質異常與非品質異常柏拉圖並排顯示
**理由**
- 便於同時比較兩類 Hold 分佈
- 螢幕空間利用率高
- 響應式設計在小螢幕改為垂直堆疊
## Risks / Trade-offs
| 風險 | 緩解措施 |
|------|----------|
| X 軸標籤過長被截斷 | 設定 `axisLabel.rotate: 45` 並限制最大字元數 |
| 某分類無資料時顯示空白 | 顯示目前無資料提示訊息 |
| 響應式設計複雜度 | 使用 CSS Grid 配合 media query |
| 累計線在少量資料時跳躍明顯 | 可接受這是柏拉圖正常特性 |

View File

@@ -0,0 +1,36 @@
## Why
WIP Overview 頁面的 Hold Summary 目前以表格呈現,難以快速辨識主要的 Hold 原因分佈。使用柏拉圖Pareto圖可以更直觀地顯示哪些 Hold 原因佔據最多比例,並透過累計百分比線幫助識別 80/20 法則中的關鍵因素。
## What Changes
- 將 Hold Summary 從單一表格改為**兩張柏拉圖**
- 品質異常 Hold (Quality)
- 非品質異常 Hold (Non-Quality)
- 每張柏拉圖包含:
- Y 軸QTY 數量(柱狀圖)
- Y 軸(右):累計百分比(折線圖)
- X 軸Hold Reason 名稱
- 每張圖下方保留**摘要表格**顯示Hold Reason、Lots、QTY、累計%(以 QTY 為基準)
- 保留 **Drill-down 功能**:點擊柏拉圖柱狀或表格連結可跳轉至 Hold Detail 頁面
- **移除**原本的單一表格呈現方式
## Capabilities
### New Capabilities
- `hold-pareto-chart`: 在 WIP Overview 頁面以柏拉圖呈現 Hold 資料分佈,支援品質/非品質分類與互動式 drill-down
### Modified Capabilities
_(無需修改現有 spec - API 已提供完整資料,僅前端視覺化變更)_
## Impact
- **前端**`src/mes_dashboard/templates/wip_overview.html`
- 加入 ECharts 圖表庫引用
- 新增兩組 chart container 與摘要表格
- 新增 `renderQualityPareto()`, `renderNonQualityPareto()` 函數
- 移除原本的 `renderHold()` 表格邏輯
- **後端**無變更API `/api/wip/overview/hold` 已提供 `holdType` 分類)
- **依賴**ECharts 已整合於專案(`/static/js/echarts.min.js`

View File

@@ -0,0 +1,65 @@
## ADDED Requirements
### Requirement: Hold Summary 柏拉圖視覺化
系統 SHALL 在 WIP Overview 頁面以兩張柏拉圖呈現 Hold 資料分佈,分別為品質異常 Hold 與非品質異常 Hold。
#### Scenario: 顯示品質異常柏拉圖
- **WHEN** 頁面載入 Hold 資料且存在品質異常 Hold
- **THEN** 系統顯示品質異常柏拉圖X 軸為 Hold ReasonY 軸(左)為 QTY 柱狀圖Y 軸(右)為累計百分比折線圖
#### Scenario: 顯示非品質異常柏拉圖
- **WHEN** 頁面載入 Hold 資料且存在非品質異常 Hold
- **THEN** 系統顯示非品質異常柏拉圖X 軸為 Hold ReasonY 軸(左)為 QTY 柱狀圖Y 軸(右)為累計百分比折線圖
#### Scenario: 無資料時顯示提示
- **WHEN** 某分類(品質或非品質)無 Hold 資料
- **THEN** 該分類圖表區域顯示「目前無資料」提示訊息
### Requirement: 柏拉圖排序與累計計算
系統 SHALL 按 QTY 降序排列 Hold Reason並以 QTY 計算累計百分比。
#### Scenario: QTY 降序排列
- **WHEN** 渲染柏拉圖
- **THEN** Hold Reason 按 QTY 由高至低排列於 X 軸
#### Scenario: 累計百分比計算
- **WHEN** 渲染柏拉圖累計線
- **THEN** 累計百分比以各 Reason 的 QTY 佔該分類總 QTY 比例累加計算
### Requirement: 柏拉圖摘要表格
系統 SHALL 在每張柏拉圖下方顯示摘要表格,包含 Hold Reason、Lots、QTY、累計%。
#### Scenario: 摘要表格欄位
- **WHEN** 渲染摘要表格
- **THEN** 表格包含欄位Hold Reason可點擊、Lots 數量、QTY 數量、累計百分比
#### Scenario: 摘要表格排序
- **WHEN** 渲染摘要表格
- **THEN** 表格資料順序與柏拉圖一致QTY 降序)
### Requirement: Hold Reason Drill-down
系統 SHALL 支援點擊柏拉圖柱狀或摘要表格連結跳轉至 Hold Detail 頁面。
#### Scenario: 點擊柏拉圖柱狀
- **WHEN** 使用者點擊柏拉圖中的某一柱狀
- **THEN** 導向 `/hold-detail?reason={encoded_reason}` 頁面
#### Scenario: 點擊摘要表格連結
- **WHEN** 使用者點擊摘要表格中的 Hold Reason 連結
- **THEN** 導向 `/hold-detail?reason={encoded_reason}` 頁面
### Requirement: 響應式版面配置
系統 SHALL 支援響應式設計,大螢幕兩欄並排,小螢幕垂直堆疊。
#### Scenario: 大螢幕並排顯示
- **WHEN** 螢幕寬度 >= 1200px
- **THEN** 品質異常與非品質異常柏拉圖並排顯示
#### Scenario: 小螢幕堆疊顯示
- **WHEN** 螢幕寬度 < 1200px
- **THEN** 品質異常與非品質異常柏拉圖垂直堆疊顯示

View File

@@ -0,0 +1,33 @@
## 1. 準備工作
- [x] 1.1 在 wip_overview.html 加入 ECharts 腳本引用
- [x] 1.2 新增柏拉圖容器 HTML 結構(品質/非品質兩組 chart + 表格)
## 2. 資料處理函數
- [x] 2.1 實作 `splitHoldByType()` 函數,將 Hold 資料按 holdType 分為品質/非品質兩組
- [x] 2.2 實作 `prepareParetoData()` 函數,按 QTY 降序排列並計算累計百分比
## 3. 柏拉圖渲染
- [x] 3.1 實作 `initParetoCharts()` 函數,初始化兩個 ECharts 實例
- [x] 3.2 實作 `renderParetoChart()` 函數配置柏拉圖選項bar + line 組合圖)
- [x] 3.3 加入 ECharts click 事件處理,實作 drill-down 導向 Hold Detail 頁面
- [x] 3.4 處理無資料情況,顯示「目前無資料」提示
## 4. 摘要表格
- [x] 4.1 實作 `renderParetoTable()` 函數渲染摘要表格Reason、Lots、QTY、累計%
- [x] 4.2 表格 Hold Reason 欄位加入 drill-down 連結
## 5. 樣式與響應式
- [x] 5.1 新增柏拉圖區塊 CSS 樣式
- [x] 5.2 實作響應式設計:大螢幕並排、小螢幕堆疊
- [x] 5.3 加入 window resize 事件處理,重新調整圖表尺寸
## 6. 整合與清理
- [x] 6.1 修改 `loadHold()` 呼叫新的渲染函數
- [x] 6.2 移除原本的 `renderHold()` 表格函數及相關 HTML/CSS
- [x] 6.3 測試驗證資料載入、圖表渲染、drill-down 功能、響應式切換

View File

@@ -0,0 +1,63 @@
### Requirement: Hold Summary 柏拉圖視覺化
系統 SHALL 在 WIP Overview 頁面以兩張柏拉圖呈現 Hold 資料分佈,分別為品質異常 Hold 與非品質異常 Hold。
#### Scenario: 顯示品質異常柏拉圖
- **WHEN** 頁面載入 Hold 資料且存在品質異常 Hold
- **THEN** 系統顯示品質異常柏拉圖X 軸為 Hold ReasonY 軸(左)為 QTY 柱狀圖Y 軸(右)為累計百分比折線圖
#### Scenario: 顯示非品質異常柏拉圖
- **WHEN** 頁面載入 Hold 資料且存在非品質異常 Hold
- **THEN** 系統顯示非品質異常柏拉圖X 軸為 Hold ReasonY 軸(左)為 QTY 柱狀圖Y 軸(右)為累計百分比折線圖
#### Scenario: 無資料時顯示提示
- **WHEN** 某分類(品質或非品質)無 Hold 資料
- **THEN** 該分類圖表區域顯示「目前無資料」提示訊息
### Requirement: 柏拉圖排序與累計計算
系統 SHALL 按 QTY 降序排列 Hold Reason並以 QTY 計算累計百分比。
#### Scenario: QTY 降序排列
- **WHEN** 渲染柏拉圖
- **THEN** Hold Reason 按 QTY 由高至低排列於 X 軸
#### Scenario: 累計百分比計算
- **WHEN** 渲染柏拉圖累計線
- **THEN** 累計百分比以各 Reason 的 QTY 佔該分類總 QTY 比例累加計算
### Requirement: 柏拉圖摘要表格
系統 SHALL 在每張柏拉圖下方顯示摘要表格,包含 Hold Reason、Lots、QTY、累計%。
#### Scenario: 摘要表格欄位
- **WHEN** 渲染摘要表格
- **THEN** 表格包含欄位Hold Reason可點擊、Lots 數量、QTY 數量、累計百分比
#### Scenario: 摘要表格排序
- **WHEN** 渲染摘要表格
- **THEN** 表格資料順序與柏拉圖一致QTY 降序)
### Requirement: Hold Reason Drill-down
系統 SHALL 支援點擊柏拉圖柱狀或摘要表格連結跳轉至 Hold Detail 頁面。
#### Scenario: 點擊柏拉圖柱狀
- **WHEN** 使用者點擊柏拉圖中的某一柱狀
- **THEN** 導向 `/hold-detail?reason={encoded_reason}` 頁面
#### Scenario: 點擊摘要表格連結
- **WHEN** 使用者點擊摘要表格中的 Hold Reason 連結
- **THEN** 導向 `/hold-detail?reason={encoded_reason}` 頁面
### Requirement: 響應式版面配置
系統 SHALL 支援響應式設計,大螢幕兩欄並排,小螢幕垂直堆疊。
#### Scenario: 大螢幕並排顯示
- **WHEN** 螢幕寬度 >= 1200px
- **THEN** 品質異常與非品質異常柏拉圖並排顯示
#### Scenario: 小螢幕堆疊顯示
- **WHEN** 螢幕寬度 < 1200px
- **THEN** 品質異常與非品質異常柏拉圖垂直堆疊顯示

View File

@@ -3,6 +3,7 @@
{% block title %}WIP Overview Dashboard{% endblock %}
{% block head_extra %}
<script src="/static/js/echarts.min.js"></script>
<style>
:root {
--bg: #f5f7fa;
@@ -436,8 +437,8 @@
/* Content Grid */
.content-grid {
display: grid;
grid-template-columns: 3fr 1fr;
display: flex;
flex-direction: column;
gap: 16px;
}
@@ -612,6 +613,139 @@
color: #9A3412;
}
/* Pareto Chart Styles */
.pareto-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
.pareto-section {
background: var(--card-bg);
border-radius: 10px;
box-shadow: var(--shadow);
overflow: hidden;
}
.pareto-header {
padding: 12px 16px;
border-bottom: 1px solid var(--border);
background: #fafbfc;
}
.pareto-header.quality {
background: linear-gradient(135deg, #fee2e2 0%, #fecaca 100%);
border-bottom-color: #fca5a5;
}
.pareto-header.non-quality {
background: linear-gradient(135deg, #ffedd5 0%, #fed7aa 100%);
border-bottom-color: #fdba74;
}
.pareto-title {
font-size: 14px;
font-weight: 600;
color: var(--text);
display: flex;
align-items: center;
gap: 8px;
}
.pareto-header.quality .pareto-title {
color: #991B1B;
}
.pareto-header.non-quality .pareto-title {
color: #9A3412;
}
.pareto-title .badge {
font-size: 11px;
padding: 2px 8px;
border-radius: 10px;
background: rgba(0,0,0,0.1);
}
.pareto-body {
padding: 12px;
}
.pareto-chart {
width: 100%;
height: 280px;
}
.pareto-no-data {
display: flex;
align-items: center;
justify-content: center;
height: 280px;
color: var(--muted);
font-size: 14px;
}
.pareto-table {
width: 100%;
border-collapse: collapse;
font-size: 12px;
margin-top: 12px;
}
.pareto-table th,
.pareto-table td {
padding: 8px 10px;
text-align: left;
border-bottom: 1px solid #e5e7eb;
}
.pareto-table th {
background: #f9fafb;
font-weight: 600;
font-size: 11px;
color: var(--muted);
text-transform: uppercase;
}
.pareto-table td:nth-child(2),
.pareto-table td:nth-child(3),
.pareto-table td:nth-child(4) {
text-align: right;
}
.pareto-table th:nth-child(2),
.pareto-table th:nth-child(3),
.pareto-table th:nth-child(4) {
text-align: right;
}
.pareto-table tbody tr:hover {
background: #f3f4f6;
}
.pareto-table .reason-link {
color: var(--primary);
text-decoration: none;
cursor: pointer;
}
.pareto-table .reason-link:hover {
text-decoration: underline;
color: var(--primary-dark);
}
.pareto-table .cumulative {
color: var(--muted);
font-size: 11px;
}
/* Responsive: stack on smaller screens */
@media (max-width: 1200px) {
.pareto-grid {
grid-template-columns: 1fr;
}
}
/* Loading */
.loading-overlay {
position: fixed;
@@ -784,14 +918,34 @@
</div>
</div>
<!-- Hold Summary -->
<div class="card">
<div class="card-header">
<div class="card-title">Hold Summary</div>
<!-- Hold Summary - Pareto Charts -->
<div class="pareto-grid" id="holdParetoContainer">
<!-- Quality Hold Pareto -->
<div class="pareto-section">
<div class="pareto-header quality">
<div class="pareto-title">
品質異常 Hold
<span class="badge" id="qualityHoldCount">0 項</span>
</div>
</div>
<div class="pareto-body">
<div id="qualityParetoChart" class="pareto-chart"></div>
<div id="qualityParetoNoData" class="pareto-no-data" style="display: none;">目前無資料</div>
<div id="qualityParetoTable"></div>
</div>
</div>
<div class="card-body" style="max-height: 500px; overflow: auto;">
<div id="holdContainer">
<div class="placeholder">Loading...</div>
<!-- Non-Quality Hold Pareto -->
<div class="pareto-section">
<div class="pareto-header non-quality">
<div class="pareto-title">
非品質異常 Hold
<span class="badge" id="nonQualityHoldCount">0 項</span>
</div>
</div>
<div class="pareto-body">
<div id="nonQualityParetoChart" class="pareto-chart"></div>
<div id="nonQualityParetoNoData" class="pareto-no-data" style="display: none;">目前無資料</div>
<div id="nonQualityParetoTable"></div>
</div>
</div>
</div>
@@ -1305,31 +1459,184 @@
container.innerHTML = html;
}
function renderHold(data) {
const container = document.getElementById('holdContainer');
// ============================================================
// Pareto Chart Functions
// ============================================================
let paretoCharts = {
quality: null,
nonQuality: null
};
if (!data || !data.items || data.items.length === 0) {
container.innerHTML = '<div class="placeholder">No hold lots</div>';
// Task 2.1: Split hold data by type
function splitHoldByType(data) {
if (!data || !data.items) {
return { quality: [], nonQuality: [] };
}
const quality = data.items.filter(item => item.holdType === 'quality');
const nonQuality = data.items.filter(item => item.holdType !== 'quality');
return { quality, nonQuality };
}
// Task 2.2: Prepare Pareto data (sort by QTY desc, calculate cumulative %)
function prepareParetoData(items) {
if (!items || items.length === 0) {
return { reasons: [], qtys: [], lots: [], cumulative: [], totalQty: 0 };
}
// Sort by QTY descending
const sorted = [...items].sort((a, b) => (b.qty || 0) - (a.qty || 0));
const reasons = sorted.map(item => item.reason || '未知');
const qtys = sorted.map(item => item.qty || 0);
const lots = sorted.map(item => item.lots || 0);
const totalQty = qtys.reduce((sum, q) => sum + q, 0);
// Calculate cumulative percentage
const cumulative = [];
let runningSum = 0;
qtys.forEach(qty => {
runningSum += qty;
cumulative.push(totalQty > 0 ? Math.round((runningSum / totalQty) * 100) : 0);
});
return { reasons, qtys, lots, cumulative, totalQty, items: sorted };
}
// Task 3.1: Initialize Pareto charts
function initParetoCharts() {
const qualityEl = document.getElementById('qualityParetoChart');
const nonQualityEl = document.getElementById('nonQualityParetoChart');
if (qualityEl && !paretoCharts.quality) {
paretoCharts.quality = echarts.init(qualityEl);
}
if (nonQualityEl && !paretoCharts.nonQuality) {
paretoCharts.nonQuality = echarts.init(nonQualityEl);
}
}
// Task 3.2: Render Pareto chart with ECharts
function renderParetoChart(chart, paretoData, colorTheme) {
if (!chart) return;
const barColor = colorTheme === 'quality' ? '#ef4444' : '#f97316';
const lineColor = colorTheme === 'quality' ? '#991B1B' : '#9A3412';
const option = {
tooltip: {
trigger: 'axis',
axisPointer: { type: 'cross' },
formatter: function(params) {
const reason = params[0].name;
const qty = params[0].value;
const cumPct = params[1] ? params[1].value : 0;
return `<strong>${reason}</strong><br/>QTY: ${formatNumber(qty)}<br/>累計: ${cumPct}%`;
}
},
grid: {
left: '3%',
right: '4%',
bottom: '15%',
top: '10%',
containLabel: true
},
xAxis: {
type: 'category',
data: paretoData.reasons,
axisLabel: {
rotate: 30,
interval: 0,
fontSize: 10,
formatter: function(value) {
return value.length > 12 ? value.slice(0, 12) + '...' : value;
}
},
axisTick: { alignWithLabel: true }
},
yAxis: [
{
type: 'value',
name: 'QTY',
position: 'left',
axisLabel: {
formatter: function(val) {
return val >= 1000 ? (val / 1000).toFixed(0) + 'k' : val;
}
}
},
{
type: 'value',
name: '累計%',
position: 'right',
min: 0,
max: 100,
axisLabel: { formatter: '{value}%' }
}
],
series: [
{
name: 'QTY',
type: 'bar',
data: paretoData.qtys,
itemStyle: { color: barColor },
emphasis: {
itemStyle: { color: barColor, opacity: 0.8 }
}
},
{
name: '累計%',
type: 'line',
yAxisIndex: 1,
data: paretoData.cumulative,
symbol: 'circle',
symbolSize: 6,
lineStyle: { color: lineColor, width: 2 },
itemStyle: { color: lineColor }
}
]
};
chart.setOption(option);
// Task 3.3: Add click event for drill-down
chart.off('click'); // Remove existing handlers
chart.on('click', function(params) {
if (params.componentType === 'series' && params.seriesType === 'bar') {
const reason = paretoData.reasons[params.dataIndex];
if (reason && reason !== '未知') {
window.location.href = `/hold-detail?reason=${encodeURIComponent(reason)}`;
}
}
});
}
// Task 4.1 & 4.2: Render Pareto table with drill-down links
function renderParetoTable(containerId, paretoData) {
const container = document.getElementById(containerId);
if (!container) return;
if (!paretoData.items || paretoData.items.length === 0) {
container.innerHTML = '';
return;
}
let html = '<table class="hold-table"><thead><tr>';
let html = '<table class="pareto-table"><thead><tr>';
html += '<th>Hold Reason</th>';
html += '<th>Lots</th>';
html += '<th>QTY</th>';
html += '<th>累計%</th>';
html += '</tr></thead><tbody>';
data.items.forEach(item => {
const badgeClass = item.holdType === 'quality' ? 'quality' : 'non-quality';
const badgeText = item.holdType === 'quality' ? '品質' : '非品質';
const reasonText = item.reason || '-';
paretoData.items.forEach((item, idx) => {
const reason = item.reason || '未知';
const reasonLink = item.reason
? `<a href="/hold-detail?reason=${encodeURIComponent(item.reason)}" class="hold-reason-link">${reasonText}</a>`
: reasonText;
? `<a href="/hold-detail?reason=${encodeURIComponent(item.reason)}" class="reason-link">${reason}</a>`
: reason;
html += '<tr>';
html += `<td><span class="hold-type-badge ${badgeClass}">${badgeText}</span>${reasonLink}</td>`;
html += `<td>${reasonLink}</td>`;
html += `<td>${formatNumber(item.lots)}</td>`;
html += `<td>${formatNumber(item.qty)}</td>`;
html += `<td class="cumulative">${paretoData.cumulative[idx]}%</td>`;
html += '</tr>';
});
@@ -1337,6 +1644,55 @@
container.innerHTML = html;
}
// Task 3.4: Handle no data state
function showParetoNoData(type, show) {
const chartEl = document.getElementById(`${type}ParetoChart`);
const noDataEl = document.getElementById(`${type}ParetoNoData`);
if (chartEl) chartEl.style.display = show ? 'none' : 'block';
if (noDataEl) noDataEl.style.display = show ? 'flex' : 'none';
}
// Main render function for Hold data
function renderHold(data) {
initParetoCharts();
const { quality, nonQuality } = splitHoldByType(data);
const qualityData = prepareParetoData(quality);
const nonQualityData = prepareParetoData(nonQuality);
// Update counts in header
document.getElementById('qualityHoldCount').textContent = `${quality.length}`;
document.getElementById('nonQualityHoldCount').textContent = `${nonQuality.length}`;
// Quality Pareto
if (quality.length > 0) {
showParetoNoData('quality', false);
renderParetoChart(paretoCharts.quality, qualityData, 'quality');
renderParetoTable('qualityParetoTable', qualityData);
} else {
showParetoNoData('quality', true);
if (paretoCharts.quality) paretoCharts.quality.clear();
document.getElementById('qualityParetoTable').innerHTML = '';
}
// Non-Quality Pareto
if (nonQuality.length > 0) {
showParetoNoData('nonQuality', false);
renderParetoChart(paretoCharts.nonQuality, nonQualityData, 'non-quality');
renderParetoTable('nonQualityParetoTable', nonQualityData);
} else {
showParetoNoData('nonQuality', true);
if (paretoCharts.nonQuality) paretoCharts.nonQuality.clear();
document.getElementById('nonQualityParetoTable').innerHTML = '';
}
}
// Task 5.3: Window resize handler for charts
window.addEventListener('resize', function() {
if (paretoCharts.quality) paretoCharts.quality.resize();
if (paretoCharts.nonQuality) paretoCharts.nonQuality.resize();
});
// ============================================================
// Navigation
// ============================================================