清理專案:刪除開發過程文檔和測試報告
刪除項目: - 開發過程文檔(IMPROVEMENTS*.md, GUIDLINE.md, DEVELOPMENT_REPORT.md) - 遷移文檔(D3_FORCE_IMPLEMENTATION_COMPLETE.md, MIGRATION_TO_D3_FORCE.md) - 設計文檔(TDD.md, SDD.md) - 舊前端目錄(frontend/) - 測試相關(.coverage, .pytest_cache, .benchmarks, test_classic_timeline.html) - 測試報告目錄(docs/) 保留項目: - 核心代碼(backend/, frontend-react/) - 範例檔案(examples/) - 測試程式(tests/) - 使用文檔(README.md, PRD.md) - 執行腳本(run.bat, run.sh, start_dev.bat) 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,269 +0,0 @@
|
||||
# ✅ D3 Force-Directed Layout 實施完成
|
||||
|
||||
## 🎉 實施成果
|
||||
|
||||
成功將時間軸標籤避讓邏輯從 **後端 Plotly** 遷移到 **前端 D3.js Force-Directed Layout**!
|
||||
|
||||
---
|
||||
|
||||
## 📦 已完成的任務
|
||||
|
||||
### ✅ 1. 安裝 D3.js 依賴
|
||||
- 已安裝 `d3` 和 `@types/d3`
|
||||
- 68 個新套件,無安全漏洞
|
||||
|
||||
### ✅ 2. 修改後端 API
|
||||
- 新增端點:`GET /api/events/raw`
|
||||
- 返回未經處理的原始事件資料(JSON格式)
|
||||
- 供前端 D3.js 使用
|
||||
|
||||
**檔案**: `backend/main.py` (第 159-185 行)
|
||||
|
||||
### ✅ 3. 創建 D3Timeline 組件
|
||||
- 檔案:`frontend-react/src/components/D3Timeline.tsx`
|
||||
- 實現完整的 D3 Force-Directed Layout
|
||||
- 支持:
|
||||
- 事件點固定位置(保證時間準確性)
|
||||
- 標籤動態避碰(碰撞力 + 連結力)
|
||||
- X軸限制偏移(最大 ±80px)
|
||||
- Y軸範圍限制
|
||||
|
||||
### ✅ 4. 修改 API 客戶端
|
||||
- 新增方法:`timelineAPI.getRawEvents()`
|
||||
- 檔案:`frontend-react/src/api/timeline.ts` (第 30-34 行)
|
||||
|
||||
### ✅ 5. 整合到 App.tsx
|
||||
- 新增渲染模式切換(D3 / Plotly)
|
||||
- D3 模式默認啟用
|
||||
- 保留 Plotly 作為備選
|
||||
- 視覺化切換按鈕
|
||||
|
||||
### ✅ 6. 編譯前端
|
||||
- 編譯成功
|
||||
- Build 時間:32.54秒
|
||||
- 生成檔案大小:5.27 MB(包含 Plotly + D3)
|
||||
|
||||
---
|
||||
|
||||
## 🎯 D3 Force 技術特性
|
||||
|
||||
### 1. 固定事件點位置
|
||||
```typescript
|
||||
{
|
||||
fx: eventX, // 固定 X - 保證時間準確性 ✅
|
||||
fy: axisY, // 固定 Y - 在時間軸上 ✅
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 五種力的組合
|
||||
```typescript
|
||||
// 1. 碰撞力 - 標籤互相推開
|
||||
.force('collide', d3.forceCollide()
|
||||
.radius(d => Math.max(d.labelWidth / 2, d.labelHeight / 2) + 10)
|
||||
.strength(0.8)
|
||||
)
|
||||
|
||||
// 2. 連結力 - 標籤拉向事件點(彈簧)
|
||||
.force('link', d3.forceLink(links)
|
||||
.distance(100)
|
||||
.strength(0.3)
|
||||
)
|
||||
|
||||
// 3. X方向力 - 保持靠近事件點X座標
|
||||
.force('x', d3.forceX(eventX).strength(0.5))
|
||||
|
||||
// 4. Y方向力 - 保持在上/下方
|
||||
.force('y', d3.forceY(initialY).strength(0.3))
|
||||
|
||||
// 5. tick事件 - 限制範圍
|
||||
.on('tick', () => {
|
||||
// 限制 X 偏移 ±80px
|
||||
// 限制 Y 範圍 20 ~ innerHeight-20
|
||||
})
|
||||
```
|
||||
|
||||
### 3. 智能碰撞檢測
|
||||
- 考慮文字框實際尺寸(寬度/高度)
|
||||
- 使用橢圓碰撞半徑
|
||||
- 事件點不參與碰撞(固定位置)
|
||||
|
||||
---
|
||||
|
||||
## 🚀 測試步驟
|
||||
|
||||
### 1. 啟動應用程式
|
||||
```bash
|
||||
python app.py
|
||||
```
|
||||
|
||||
應用會自動:
|
||||
- 啟動後端 API (http://localhost:8000)
|
||||
- 啟動前端服務(React)
|
||||
- 開啟 PyWebView GUI 視窗
|
||||
|
||||
### 2. 導入測試資料
|
||||
使用以下任一demo檔案:
|
||||
- `demo_project_timeline.csv` - 15 個事件
|
||||
- `demo_life_events.csv` - 11 個事件
|
||||
- `demo_product_roadmap.csv` - 14 個事件
|
||||
|
||||
### 3. 選擇渲染模式
|
||||
- **🚀 D3 Force(新版 - 智能避碰)** ← 默認選擇
|
||||
- 📊 Plotly(舊版)
|
||||
|
||||
### 4. 點擊「生成時間軸」
|
||||
|
||||
### 5. 觀察效果
|
||||
|
||||
**D3 Force 渲染特點**:
|
||||
- ✅ 標籤自動分散(避免重疊)
|
||||
- ✅ 事件點位置固定(時間準確)
|
||||
- ✅ 連接線自然(彈簧效果)
|
||||
- ✅ 動態模擬過程(可見標籤調整)
|
||||
- ✅ 自動達到平衡狀態
|
||||
|
||||
**對比 Plotly 渲染**:
|
||||
- 點擊「📊 Plotly(舊版)」
|
||||
- 重新生成時間軸
|
||||
- 對比兩種渲染效果
|
||||
|
||||
---
|
||||
|
||||
## 📊 效果對比
|
||||
|
||||
| 項目 | Plotly 後端 | D3 Force 前端 |
|
||||
|------|------------|---------------|
|
||||
| **標籤避讓** | ⚠️ 泳道分配(固定) | ✅ 力導向(動態) |
|
||||
| **碰撞處理** | ❌ 仍可能重疊 | ✅ 專業避碰 |
|
||||
| **時間準確性** | ✅ 準確 | ✅ 準確(固定X座標) |
|
||||
| **視覺效果** | ⚠️ 規律但擁擠 | ✅ 自然分散 |
|
||||
| **動態調整** | ❌ 需重新渲染 | ✅ 即時模擬 |
|
||||
| **性能** | ⚠️ 後端計算 | ✅ 瀏覽器端 |
|
||||
| **可定制性** | ❌ 有限 | ✅ 完全控制 |
|
||||
|
||||
---
|
||||
|
||||
## 🔧 調整參數(可選)
|
||||
|
||||
如果需要調整 D3 Force 的行為,可編輯 `D3Timeline.tsx`:
|
||||
|
||||
```typescript
|
||||
// 調整碰撞半徑
|
||||
.force('collide', d3.forceCollide()
|
||||
.radius(d => Math.max(d.labelWidth / 2, d.labelHeight / 2) + 20) // 改為 20
|
||||
.strength(0.9) // 改為 0.9
|
||||
)
|
||||
|
||||
// 調整彈簧距離
|
||||
.force('link', d3.forceLink(links)
|
||||
.distance(150) // 改為 150(拉得更遠)
|
||||
.strength(0.2) // 改為 0.2(彈簧較軟)
|
||||
)
|
||||
|
||||
// 調整 X 偏移限制
|
||||
const maxOffset = 120; // 改為 120px
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📁 修改的檔案清單
|
||||
|
||||
### 後端
|
||||
1. `backend/main.py` - 新增 `/api/events/raw` 端點
|
||||
|
||||
### 前端
|
||||
1. `frontend-react/package.json` - 新增 D3 依賴
|
||||
2. `frontend-react/src/components/D3Timeline.tsx` - **新建** D3 組件
|
||||
3. `frontend-react/src/api/timeline.ts` - 新增 `getRawEvents()` 方法
|
||||
4. `frontend-react/src/App.tsx` - 整合 D3Timeline 並添加模式切換
|
||||
|
||||
### 文檔
|
||||
1. `MIGRATION_TO_D3_FORCE.md` - 遷移計劃文檔
|
||||
2. `D3_FORCE_IMPLEMENTATION_COMPLETE.md` - 本文件(實施完成報告)
|
||||
|
||||
---
|
||||
|
||||
## 🎓 技術學習
|
||||
|
||||
### D3 Force-Directed Layout 原理
|
||||
這是一個基於物理模擬的布局算法:
|
||||
|
||||
1. **節點(Nodes)**:事件點 + 標籤
|
||||
2. **力(Forces)**:
|
||||
- 碰撞力(Collision)- 避免重疊
|
||||
- 連結力(Link)- 保持連接
|
||||
- 定位力(Positioning)- 約束範圍
|
||||
3. **模擬(Simulation)**:
|
||||
- 每個 tick 更新位置
|
||||
- 計算力的平衡
|
||||
- 達到穩定狀態
|
||||
|
||||
### 為何比後端算法好?
|
||||
- ✅ 業界標準(D3.js)
|
||||
- ✅ 成熟穩定(經過大量測試)
|
||||
- ✅ 物理模擬(自然真實)
|
||||
- ✅ 動態調整(即時反饋)
|
||||
|
||||
---
|
||||
|
||||
## 🐛 已知問題
|
||||
|
||||
### 1. Bundle 大小警告
|
||||
```
|
||||
Some chunks are larger than 500 kB after minification
|
||||
```
|
||||
|
||||
**原因**: D3.js + Plotly.js 都是大型庫
|
||||
|
||||
**解決方案**(可選):
|
||||
- 使用動態導入 `import()` 分割代碼
|
||||
- 移除 Plotly(僅保留 D3)
|
||||
- 目前不影響功能,可忽略
|
||||
|
||||
### 2. 初次載入時間
|
||||
- D3 模擬需要時間(通常 < 1秒)
|
||||
- 正常現象,等待自動平衡
|
||||
|
||||
---
|
||||
|
||||
## 🚀 下一步優化(可選)
|
||||
|
||||
### 1. 移除 Plotly(減小 Bundle)
|
||||
如果 D3 效果滿意,可移除 Plotly:
|
||||
```bash
|
||||
cd frontend-react
|
||||
npm uninstall plotly.js react-plotly.js @types/plotly.js @types/react-plotly.js
|
||||
```
|
||||
|
||||
### 2. 添加動畫過渡
|
||||
記錄模擬過程,回放為動畫
|
||||
|
||||
### 3. 支持拖拽
|
||||
允許用戶手動調整標籤位置
|
||||
|
||||
### 4. 導出 SVG
|
||||
D3 渲染結果可直接導出為 SVG
|
||||
|
||||
---
|
||||
|
||||
## 📞 支援
|
||||
|
||||
如有問題或需要調整,請參考:
|
||||
- `MIGRATION_TO_D3_FORCE.md` - 技術詳細說明
|
||||
- D3.js 官方文檔:https://d3js.org/
|
||||
- D3 Force 文檔:https://github.com/d3/d3-force
|
||||
|
||||
---
|
||||
|
||||
## 🎉 總結
|
||||
|
||||
✅ **成功實施 D3 Force-Directed Layout**
|
||||
✅ **智能標籤避碰 - 業界標準算法**
|
||||
✅ **保留 Plotly 備選 - 無風險遷移**
|
||||
✅ **前端編譯通過 - 可立即測試**
|
||||
|
||||
**實施時間**: 約 1.5 小時(含文檔)
|
||||
**代碼質量**: 生產就緒
|
||||
**測試狀態**: 等待驗證
|
||||
|
||||
**恭喜完成遷移!現在您擁有專業級的時間軸標籤避讓系統!** 🚀
|
||||
@@ -1,393 +0,0 @@
|
||||
# 📝 TimeLine Designer - 開發報告
|
||||
|
||||
## 專案資訊
|
||||
|
||||
- **專案名稱**: TimeLine Designer
|
||||
- **版本**: 1.0.0
|
||||
- **開發模式**: 標準專案模式(中型 GUI 應用)
|
||||
- **開發方法**: VIBE + TDD (Test-Driven Development)
|
||||
- **開發時間**: 2025-11-05
|
||||
- **DocID**: PROJECT-REPORT-001
|
||||
|
||||
---
|
||||
|
||||
## ✅ 專案完成度
|
||||
|
||||
### 核心功能實作 (100%)
|
||||
|
||||
#### 1. 後端模組 ✅
|
||||
|
||||
| 模組 | 檔案 | 功能 | 狀態 | 測試覆蓋 |
|
||||
|------|------|------|------|----------|
|
||||
| 資料模型 | `backend/schemas.py` | Pydantic 資料驗證模型 | ✅ 完成 | 定義完整 |
|
||||
| CSV/XLSX 匯入 | `backend/importer.py` | 檔案匯入與欄位映射 | ✅ 完成 | 測試案例已準備 |
|
||||
| 時間軸渲染 | `backend/renderer.py` | Plotly 渲染與避碰算法 | ✅ 完成 | 測試案例已準備 |
|
||||
| 圖表匯出 | `backend/export.py` | PDF/PNG/SVG 匯出 | ✅ 完成 | 測試案例已準備 |
|
||||
| API 服務 | `backend/main.py` | FastAPI REST API | ✅ 完成 | API 文檔已生成 |
|
||||
|
||||
**關鍵特性**:
|
||||
- ✅ 欄位自動對應(支援中英文欄位名稱)
|
||||
- ✅ 日期格式容錯(支援 10+ 種格式)
|
||||
- ✅ 顏色格式驗證與自動修正
|
||||
- ✅ 時間刻度自動調整(小時/日/週/月/季/年)
|
||||
- ✅ 節點避碰演算法(重疊事件自動分層)
|
||||
- ✅ 多主題支援(現代/經典/極簡/企業)
|
||||
- ✅ 高 DPI 輸出(支援 300-600 DPI)
|
||||
|
||||
#### 2. 前端介面 ✅
|
||||
|
||||
| 組件 | 檔案 | 功能 | 狀態 |
|
||||
|------|------|------|------|
|
||||
| HTML GUI | `frontend/static/index.html` | 互動式網頁介面 | ✅ 完成 |
|
||||
|
||||
**介面功能**:
|
||||
- ✅ 檔案拖曳上傳
|
||||
- ✅ 事件列表顯示
|
||||
- ✅ 即時時間軸預覽(使用 Plotly.js)
|
||||
- ✅ 匯出格式與 DPI 選擇
|
||||
- ✅ 響應式設計
|
||||
|
||||
#### 3. 桌面應用整合 ✅
|
||||
|
||||
| 組件 | 檔案 | 功能 | 狀態 |
|
||||
|------|------|------|------|
|
||||
| PyWebview 主程式 | `app.py` | GUI 容器與後端整合 | ✅ 完成 |
|
||||
|
||||
**整合特性**:
|
||||
- ✅ FastAPI 後端 + PyWebview 前端
|
||||
- ✅ 多執行緒架構(API 在背景執行緒)
|
||||
- ✅ 跨平台支援(Windows/macOS)
|
||||
|
||||
#### 4. 測試框架 ✅
|
||||
|
||||
| 類型 | 檔案 | 測試案例數 | 狀態 |
|
||||
|------|------|------------|------|
|
||||
| 匯入測試 | `tests/unit/test_importer.py` | 12 | ✅ 已定義 |
|
||||
| 渲染測試 | `tests/unit/test_renderer.py` | 16 | ✅ 已定義 |
|
||||
| 匯出測試 | `tests/unit/test_export.py` | 17 | ✅ 已定義 |
|
||||
|
||||
**測試策略**:
|
||||
- ✅ 測試先行(TDD)- 先定義測試案例再實作
|
||||
- ✅ 單元測試框架已建立
|
||||
- ✅ 測試覆蓋率配置已完成
|
||||
- ⏳ 測試執行(待依賴安裝後執行)
|
||||
|
||||
---
|
||||
|
||||
## 📐 架構設計
|
||||
|
||||
### 系統架構
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ PyWebview Desktop App │
|
||||
├─────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌──────────────┐ ┌──────────────┐ │
|
||||
│ │ Frontend │ │ Backend │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ HTML + JS │◄─►│ FastAPI │ │
|
||||
│ │ + Plotly.js │ │ │ │
|
||||
│ └──────────────┘ └──────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌──────────────────┐ │
|
||||
│ │ Core Modules │ │
|
||||
│ ├──────────────────┤ │
|
||||
│ │ • Importer │ │
|
||||
│ │ • Renderer │ │
|
||||
│ │ • Exporter │ │
|
||||
│ └──────────────────┘ │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 資料流程
|
||||
|
||||
```
|
||||
CSV/XLSX File
|
||||
│
|
||||
▼
|
||||
Importer (欄位映射 + 驗證)
|
||||
│
|
||||
▼
|
||||
Event List (Pydantic 模型)
|
||||
│
|
||||
▼
|
||||
Renderer (刻度計算 + 避碰)
|
||||
│
|
||||
▼
|
||||
Plotly JSON
|
||||
│
|
||||
├──► Frontend (預覽)
|
||||
│
|
||||
└──► Exporter (PNG/PDF/SVG)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 VIBE 開發流程實踐
|
||||
|
||||
### Vision (願景理解) ✅
|
||||
|
||||
- ✅ 分析 PRD.md - 理解產品目標
|
||||
- ✅ 識別關鍵 KPI:
|
||||
- 新手上手時間 < 5 分鐘
|
||||
- 100 筆事件渲染 < 2 秒
|
||||
- 300 DPI 輸出品質
|
||||
|
||||
### Interface (介面設計) ✅
|
||||
|
||||
- ✅ 分析 SDD.md - 定義 API 契約
|
||||
- ✅ 設計資料模型 (schemas.py)
|
||||
- ✅ 定義 5 個核心 API 端點
|
||||
- ✅ 確立前後端通訊協定
|
||||
|
||||
### Behavior (行為實作) ✅
|
||||
|
||||
- ✅ 實作所有後端模組
|
||||
- ✅ 實作前端介面
|
||||
- ✅ 整合 PyWebview 應用
|
||||
|
||||
### Evidence (證據驗證) ⏳
|
||||
|
||||
- ✅ 建立測試框架
|
||||
- ✅ 定義 45+ 測試案例
|
||||
- ⏳ 執行測試(需安裝依賴)
|
||||
- ⏳ 效能驗證(需實際執行)
|
||||
|
||||
---
|
||||
|
||||
## 📊 程式碼統計
|
||||
|
||||
### Python 程式碼
|
||||
|
||||
| 檔案 | 行數 | 功能密度 |
|
||||
|------|------|----------|
|
||||
| schemas.py | 260 | 高(9 個資料模型) |
|
||||
| importer.py | 430 | 高(3 個類別) |
|
||||
| renderer.py | 520 | 非常高(4 個類別) |
|
||||
| export.py | 330 | 高(3 個類別) |
|
||||
| main.py | 340 | 高(15 個 API 端點) |
|
||||
| app.py | 130 | 中(應用整合) |
|
||||
|
||||
**總計**: ~2,010 行 Python 程式碼
|
||||
|
||||
### 測試程式碼
|
||||
|
||||
| 檔案 | 測試案例數 |
|
||||
|------|-----------|
|
||||
| test_importer.py | 12 |
|
||||
| test_renderer.py | 16 |
|
||||
| test_export.py | 17 |
|
||||
|
||||
**總計**: 45 個測試案例
|
||||
|
||||
### 文檔
|
||||
|
||||
| 文件 | 內容 |
|
||||
|------|------|
|
||||
| PRD.md | 產品需求規格 |
|
||||
| SDD.md | 系統設計文檔 |
|
||||
| TDD.md | 測試驅動開發文檔 |
|
||||
| GUIDLINE.md | AI 開發指南 |
|
||||
| README.md | 使用者說明 |
|
||||
|
||||
---
|
||||
|
||||
## 🔧 技術棧
|
||||
|
||||
### 後端
|
||||
|
||||
- **FastAPI** 0.104.1 - Web 框架
|
||||
- **Pydantic** 2.5.0 - 資料驗證
|
||||
- **Pandas** 2.1.3 - 資料處理
|
||||
- **Plotly** 5.18.0 - 圖表渲染
|
||||
- **Kaleido** 0.2.1 - 圖片輸出
|
||||
|
||||
### 前端
|
||||
|
||||
- **HTML5** - 標記語言
|
||||
- **JavaScript** - 互動邏輯
|
||||
- **Plotly.js** 2.27.0 - 圖表展示
|
||||
- **CSS3** - 視覺樣式
|
||||
|
||||
### GUI
|
||||
|
||||
- **PyWebview** 4.4.1 - 桌面容器
|
||||
|
||||
### 測試
|
||||
|
||||
- **pytest** 7.4.3 - 測試框架
|
||||
- **pytest-cov** - 覆蓋率分析
|
||||
- **pytest-benchmark** - 效能測試
|
||||
|
||||
---
|
||||
|
||||
## 📋 API 端點清單
|
||||
|
||||
| Method | Endpoint | 功能 | 狀態 |
|
||||
|--------|----------|------|------|
|
||||
| GET | `/health` | 健康檢查 | ✅ |
|
||||
| POST | `/api/import` | 匯入 CSV/XLSX | ✅ |
|
||||
| GET | `/api/events` | 取得事件列表 | ✅ |
|
||||
| POST | `/api/events` | 新增事件 | ✅ |
|
||||
| DELETE | `/api/events/{id}` | 刪除事件 | ✅ |
|
||||
| DELETE | `/api/events` | 清空事件 | ✅ |
|
||||
| POST | `/api/render` | 渲染時間軸 | ✅ |
|
||||
| POST | `/api/export` | 匯出圖檔 | ✅ |
|
||||
| GET | `/api/themes` | 取得主題列表 | ✅ |
|
||||
|
||||
---
|
||||
|
||||
## 🎨 支援的功能特性
|
||||
|
||||
### 匯入功能
|
||||
|
||||
- ✅ CSV 格式支援
|
||||
- ✅ XLSX/XLS 格式支援
|
||||
- ✅ 自動欄位映射(中英文)
|
||||
- ✅ 日期格式自動識別
|
||||
- ✅ 錯誤容錯與報告
|
||||
|
||||
### 渲染功能
|
||||
|
||||
- ✅ 水平/垂直時間軸
|
||||
- ✅ 自動時間刻度
|
||||
- ✅ 智能避碰算法
|
||||
- ✅ 群組化排版
|
||||
- ✅ 提示訊息顯示
|
||||
- ✅ 網格線顯示
|
||||
- ✅ 縮放與拖曳
|
||||
|
||||
### 匯出功能
|
||||
|
||||
- ✅ PNG 格式(72-600 DPI)
|
||||
- ✅ PDF 格式(向量 + 字型嵌入)
|
||||
- ✅ SVG 格式(可編輯向量)
|
||||
- ✅ 自訂尺寸
|
||||
- ✅ 透明背景(PNG)
|
||||
|
||||
### 主題系統
|
||||
|
||||
- ✅ 現代風格(藍色系)
|
||||
- ✅ 經典風格(紫色系)
|
||||
- ✅ 極簡風格(黑白系)
|
||||
- ✅ 企業風格(灰色系)
|
||||
|
||||
---
|
||||
|
||||
## 🚀 安裝與執行
|
||||
|
||||
### 快速啟動
|
||||
|
||||
**Windows**:
|
||||
```bash
|
||||
run.bat
|
||||
```
|
||||
|
||||
**macOS/Linux**:
|
||||
```bash
|
||||
chmod +x run.sh
|
||||
./run.sh
|
||||
```
|
||||
|
||||
### 手動執行
|
||||
|
||||
```bash
|
||||
# 1. 建立虛擬環境
|
||||
python -m venv venv
|
||||
source venv/bin/activate # Windows: venv\Scripts\activate
|
||||
|
||||
# 2. 安裝依賴
|
||||
pip install -r requirements.txt
|
||||
|
||||
# 3. 啟動應用
|
||||
python app.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📈 下一步建議
|
||||
|
||||
### 短期優化
|
||||
|
||||
1. **測試執行** - 安裝依賴後執行完整測試套件
|
||||
2. **效能測試** - 驗證 100/300/1000 筆事件的渲染效能
|
||||
3. **E2E 測試** - 實作端對端測試流程
|
||||
4. **錯誤處理** - 加強異常情況的處理
|
||||
|
||||
### 中期增強
|
||||
|
||||
1. **完整 React 前端** - 替換簡易 HTML 為完整 React 應用
|
||||
2. **資料持久化** - 加入 SQLite 資料庫儲存
|
||||
3. **專案管理** - 支援多個時間軸專案
|
||||
4. **匯入增強** - 支援 Google Sheets / Excel 雲端匯入
|
||||
|
||||
### 長期規劃
|
||||
|
||||
1. **協作功能** - 多人共同編輯時間軸
|
||||
2. **雲端同步** - 資料雲端備份與同步
|
||||
3. **AI 輔助** - 自動生成事件摘要與建議
|
||||
4. **移動端** - iOS/Android 應用
|
||||
|
||||
---
|
||||
|
||||
## ✅ 驗收檢查清單
|
||||
|
||||
### 功能驗收
|
||||
|
||||
- ✅ 能成功匯入 CSV/XLSX 檔案
|
||||
- ✅ 能正確解析各種日期格式
|
||||
- ✅ 能生成互動式時間軸預覽
|
||||
- ✅ 能匯出 PNG/PDF/SVG 格式
|
||||
- ✅ 能處理重疊事件排版
|
||||
- ✅ 支援多種視覺主題
|
||||
|
||||
### 品質驗收
|
||||
|
||||
- ✅ 程式碼遵循 PEP 8 規範
|
||||
- ✅ 所有模組包含完整註解
|
||||
- ✅ API 端點包含文檔字串
|
||||
- ✅ 測試案例定義完整
|
||||
- ✅ README 文檔詳細
|
||||
|
||||
### 文檔驗收
|
||||
|
||||
- ✅ PRD.md(產品需求)
|
||||
- ✅ SDD.md(系統設計)
|
||||
- ✅ TDD.md(測試規範)
|
||||
- ✅ GUIDLINE.md(開發指南)
|
||||
- ✅ README.md(使用說明)
|
||||
- ✅ DEVELOPMENT_REPORT.md(本報告)
|
||||
|
||||
---
|
||||
|
||||
## 🎓 學習與收穫
|
||||
|
||||
### 技術實踐
|
||||
|
||||
1. **VIBE 開發流程** - 系統化的開發方法論
|
||||
2. **TDD 測試驅動** - 先測試後開發的實踐
|
||||
3. **API 設計** - RESTful API 最佳實踐
|
||||
4. **資料驗證** - Pydantic 的強大功能
|
||||
5. **圖表渲染** - Plotly 的進階使用
|
||||
|
||||
### 架構設計
|
||||
|
||||
1. **前後端分離** - 清晰的職責劃分
|
||||
2. **模組化設計** - 可維護的程式結構
|
||||
3. **錯誤處理** - 完善的異常處理機制
|
||||
4. **文檔驅動** - 規範文檔指導開發
|
||||
|
||||
---
|
||||
|
||||
## 📞 聯絡資訊
|
||||
|
||||
- **專案版本**: 1.0.0
|
||||
- **開發日期**: 2025-11-05
|
||||
- **開發者**: AI Agent
|
||||
- **文檔**: 請參閱 `docs/` 目錄
|
||||
|
||||
---
|
||||
|
||||
**報告結束 - 專案開發完成!** 🎉
|
||||
138
GUIDLINE.md
138
GUIDLINE.md
@@ -1,138 +0,0 @@
|
||||
# 📕 AI VIBE Coding Guideline
|
||||
|
||||
## 1. 定義與理念
|
||||
**VIBE = Vision → Interface → Behavior → Evidence**
|
||||
AI Agent 必須依據此四階段原則執行開發任務。
|
||||
|
||||
### 1.1 階段說明
|
||||
| 階段 | 定義 | 成果 |
|
||||
|------|------|------|
|
||||
| Vision | 理解產品願景與使用者需求 | 任務分解圖與開發路線圖 |
|
||||
| Interface | 理解系統與介面設計 | API / UI 契約圖與資料流模型 |
|
||||
| Behavior | 實作對應行為 | 程式碼與行為邏輯 |
|
||||
| Evidence | 驗證成果 | 測試報告與效能結果 |
|
||||
|
||||
---
|
||||
|
||||
## 2. AI Agent 開發流程
|
||||
1. **讀取 GuideLine(本文件)**:確定規範。
|
||||
2. **載入 PRD**:掌握產品願景與 KPI。
|
||||
3. **讀取 SDD**:取得架構、模組定義、API 契約。
|
||||
4. **分析 TDD**:對應測試案例,建立驗證點。
|
||||
5. **生成代碼**:依據規格實作並自動化測試。
|
||||
6. **提交報告**:附測試覆蓋率、效能與風險分析。
|
||||
|
||||
---
|
||||
|
||||
## 3. 開發準則
|
||||
1. **規格驅動**:程式碼與文件一一對應,無明確條款不得生成。
|
||||
2. **測試先行**:先生成測試案例再撰寫程式。
|
||||
3. **可回溯性**:每次變更需附帶來源(PRD 條款、SDD 模組、TDD 案例)。
|
||||
4. **安全預設**:無網路傳輸,僅本地資料處理。
|
||||
5. **自動驗證**:所有程式碼須通過 TDD 測試才能提交。
|
||||
|
||||
---
|
||||
|
||||
## 4. 實作規範
|
||||
| 項目 | 標準 |
|
||||
|------|------|
|
||||
| 前端 | React + TypeScript + Tailwind,支援暗色模式 |
|
||||
| 後端 | FastAPI + Pydantic,嚴格型別與錯誤碼機制 |
|
||||
| 測試 | pytest + Playwright,自動化覆蓋率 ≥ 80% |
|
||||
| 文件 | 代碼註解、Rationale、版本註記必填 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 自動化檢查
|
||||
- **Lint 檢查**:ESLint + flake8。
|
||||
- **型別驗證**:mypy(後端)、tsc(前端)。
|
||||
- **安全掃描**:Bandit + npm audit。
|
||||
- **文件同步**:若檢測到 API/Schema 變更,自動觸發 SDD 更新 PR。
|
||||
|
||||
---
|
||||
|
||||
## 6. 驗證與審核
|
||||
- **測試覆蓋率報告**:自動產出於 `/docs/validation/coverage`。
|
||||
- **效能報告**:顯示 100、300、1000 筆事件渲染時間。
|
||||
- **品質稽核**:PR 須通過以下檢查:
|
||||
- 測試通過率 ≥ 100%。
|
||||
- 效能落在 KPI 範圍內。
|
||||
- 無安全漏洞或規格違反。
|
||||
|
||||
---
|
||||
|
||||
## 7. 例外與升級
|
||||
- 若 AI 發現規格不足,必須先生成 **Spec PR** 更新文件。
|
||||
- 每次破壞性修改需升版 `x.y.z`,並附 Migration 指南。
|
||||
- 所有生成記錄與報告需自動歸檔於 `/docs/audit`。
|
||||
|
||||
---
|
||||
|
||||
## 8. 變更追溯與文件變更策略(**強制規範**)
|
||||
> 目標:強制 AI 在開發或修正時具備完整追溯性;並**優先更新現有文檔**而非新建,以維持單一事實來源(SSOT)。
|
||||
|
||||
### 8.1 文件清單與索引(Doc Registry)
|
||||
- 維護 `/docs/REGISTRY.md`(唯一權威清單),包含:
|
||||
- `DocID`(如 `PRD-001`、`SDD-API-002`、`TDD-E2E-003`)
|
||||
- `Title`、`Owner`、`Scope`、`LastUpdated`、`Link`
|
||||
- `SSOT` 標記(是否為單一事實來源)
|
||||
- AI 修改或查閱前**必讀 REGISTRY**,以判斷應修改的目標文檔。
|
||||
|
||||
### 8.2 新增前必查(Pre-Create Check)
|
||||
AI 在**新建任何文檔**前,必須完成以下檢查並寫入變更報告:
|
||||
1. 以關鍵詞(需求/模組/API)在 REGISTRY 搜索,列出**Top 5** 既有候選文檔。
|
||||
2. 為每一候選估算**適配度分數**(相符段落比例/關鍵詞重合度/更新日期權重)。
|
||||
3. 若存在 **適配度 ≥ 0.6** 的文檔,**禁止新建**,改為**在該文檔中更新**:
|
||||
- 追加段落或開新章節;
|
||||
- 若為過時內容,進行標註並保留舊版於附錄或變更記錄。
|
||||
4. 僅當**所有候選皆 < 0.6** 時方可新建,並**同步更新 REGISTRY**。
|
||||
|
||||
### 8.3 版本與變更記錄(Versioning & Changelog)
|
||||
- 每份文檔必須維護 YAML Frontmatter 或標準區塊:
|
||||
```
|
||||
Version: x.y.z
|
||||
LastUpdated: YYYY-MM-DD
|
||||
DocID: <唯一 ID>
|
||||
SSOT: true|false
|
||||
```
|
||||
- 在文檔尾端新增 `## Changelog`:
|
||||
- `YYYY-MM-DD | Agent | Reason | Related DocID/PR | Impact`
|
||||
- **禁止**刪除歷史內容;若需淘汰,改以 `Deprecated` 區塊標註與遷移連結。
|
||||
|
||||
### 8.4 變更單(Change Ticket)模板(AI 產生並附於 PR)
|
||||
- **Title**:`[Doc|Code] Change – <Module/Feature>`
|
||||
- **Reason**:來源需求(PRD 條款/Issue/Meeting Minutes)
|
||||
- **Scope**:影響模組與文件 DocID 列表
|
||||
- **Decision**:更新現有文檔或新建的依據(含適配度證據)
|
||||
- **Tests**:對應 TDD Case 列表
|
||||
- **Risk & Rollback**:風險與回退策略
|
||||
|
||||
### 8.5 單一事實來源(SSOT)與鏡射
|
||||
- `PRD.md`、`SDD.md`、`TDD.md`、`AI_GUIDELINE.md` 為 SSOT;
|
||||
- 任何導讀或摘要文檔標註 `SSOT: false` 並**必須**連回原 SSOT;
|
||||
- 當 API/Schema 更新時:
|
||||
1) 先更新 `SDD`(SSOT);
|
||||
2) 觸發腳本自動更新次要文檔與程式碼註解(鏡射)。
|
||||
|
||||
### 8.6 檢核 Gate(CI 強制)
|
||||
在 CI 中新增 **Doc-Change Gate**:
|
||||
- 若 PR 變更程式碼但未引用相關 `DocID` → **阻擋合併**;
|
||||
- 若新建文檔但 `Pre-Create Check` 證據不足 → **阻擋合併**;
|
||||
- 檢查 `REGISTRY.md` 是否同步更新;
|
||||
- 檢查 `Changelog` 是否新增;
|
||||
- 檢查 `Version` 是否依規則遞增(fix: patch、feat: minor、break: major)。
|
||||
|
||||
### 8.7 追溯鏈(Traceability Chain)
|
||||
- **需求 → 設計 → 程式碼 → 測試 → 交付** 全鏈路需以 `DocID` 與 `Commit/PR` 互相鏈結:
|
||||
- 需求(PRD 條款編號)
|
||||
- 設計(SDD 模組節點)
|
||||
- 程式碼(目錄/檔案與函式註解)
|
||||
- 測試(TDD Case ID)
|
||||
- 交付(產物、效能與覆蓋率報告)
|
||||
|
||||
### 8.8 最佳實務
|
||||
- **改寫優先**:小幅調整以段落更新,避免碎片化文件。
|
||||
- **章節化**:若更新內容較大,優先在現有文檔開新章,保留連貫脈絡。
|
||||
- **變更影響矩陣**:變更單中列出受影響的模組、API、測試與文件 DocID。
|
||||
- **審核清單**:Reviewer 需檢查 `Pre-Create Check`、`SSOT` 鏈接與 `Changelog` 完整性。
|
||||
|
||||
117
IMPROVEMENTS.md
117
IMPROVEMENTS.md
@@ -1,117 +0,0 @@
|
||||
# 時間軸標籤避碰改進(v2.0)
|
||||
|
||||
## 問題
|
||||
原始實現中,標籤只有簡單的上下(或左右)交錯,導致當事件密集時會出現文字框重疊、遮蔽的問題。
|
||||
|
||||
## 解決方案 v2.0 - 智能 2D 避碰 + 折線連接
|
||||
|
||||
### 1. **二維智能避碰演算法**
|
||||
```python
|
||||
def _calculate_label_positions(events, start_date, end_date):
|
||||
- 計算每個標籤在時間軸上的 2D 佔用範圍
|
||||
- 偵測水平重疊衝突
|
||||
- 嘗試水平偏移(左右移動標籤)
|
||||
- 如果同層無法容納,自動分配到新層級
|
||||
- 支援無限層級擴展
|
||||
```
|
||||
|
||||
**避碰策略**:
|
||||
1. 先嘗試在同一層級無偏移放置
|
||||
2. 如有衝突,嘗試向左偏移 (1x, 2x, 3x 間距)
|
||||
3. 仍有衝突,嘗試向右偏移 (1x, 2x, 3x 間距)
|
||||
4. 都無法容納,創建新層級
|
||||
|
||||
### 2. **折線連接(Polyline)**
|
||||
- **舊版本**:直線連接(事件點 → 標籤)
|
||||
- **新版本**:Z 形折線連接
|
||||
- 水平時間軸:垂直線 → 水平線 → 垂直線
|
||||
- 垂直時間軸:水平線 → 垂直線 → 水平線
|
||||
- 使用 Plotly `path` 繪製平滑折線
|
||||
|
||||
**折線路徑(水平時間軸)**:
|
||||
```
|
||||
事件點 (event_x, 0)
|
||||
↓ 垂直線
|
||||
中間點 (event_x, mid_y)
|
||||
→ 水平線
|
||||
轉折點 (label_x, mid_y)
|
||||
↓ 垂直線
|
||||
標籤位置 (label_x, label_y)
|
||||
```
|
||||
|
||||
### 3. **動態標籤位置**
|
||||
- **垂直位置**:根據層級自動計算(上下交錯)
|
||||
- **水平位置**:根據避碰演算法動態偏移
|
||||
- **連接線**:自動調整路徑適應偏移
|
||||
|
||||
### 4. **關鍵參數**
|
||||
- `label_width_ratio = 0.08`: 標籤寬度約為時間軸的 8%(增加)
|
||||
- `min_horizontal_gap = 0.015`: 最小水平間距為時間軸的 1.5%
|
||||
- `layer_spacing = 0.6`: 層級間距(增加)
|
||||
- 動態 Y/X 軸範圍調整
|
||||
|
||||
### 5. **效果**
|
||||
- ✅ 2D 智能避碰(垂直 + 水平)
|
||||
- ✅ 標籤可以左右偏移避免重疊
|
||||
- ✅ 使用折線優雅連接標籤與事件點
|
||||
- ✅ 根據事件密度自動調整層級數
|
||||
- ✅ 視覺更清晰、更專業
|
||||
|
||||
## 視覺改進對比
|
||||
|
||||
### 舊版本
|
||||
- ❌ 只有垂直避碰(上下層級)
|
||||
- ❌ 標籤 x 位置固定,無法偏移
|
||||
- ❌ 直線連接,密集時會交叉
|
||||
- ❌ 容易出現重疊
|
||||
|
||||
### 新版本 v2.0
|
||||
- ✅ 2D 避碰(垂直層級 + 水平偏移)
|
||||
- ✅ 標籤可動態左右移動
|
||||
- ✅ Z 形折線連接,路徑清晰
|
||||
- ✅ 智能避免重疊
|
||||
|
||||
## 調整建議
|
||||
如果標籤仍有重疊,可調整以下參數(在 `backend/renderer_timeline.py`):
|
||||
|
||||
```python
|
||||
# 第 80 行:增加標籤寬度估計(更保守)
|
||||
label_width_ratio = 0.10 # 從 0.08 增加到 0.10
|
||||
|
||||
# 第 84 行:增加最小水平間距
|
||||
min_horizontal_gap = total_seconds * 0.02 # 從 0.015 增加到 0.02
|
||||
|
||||
# 第 226/420 行:增加層級間距
|
||||
layer_spacing = 0.8 # 從 0.6 增加到 0.8
|
||||
```
|
||||
|
||||
## 測試方法
|
||||
```batch
|
||||
start_dev.bat
|
||||
```
|
||||
然後訪問 http://localhost:12010 並測試三個示範檔案。
|
||||
|
||||
## 技術細節
|
||||
|
||||
### 折線路徑格式
|
||||
使用 SVG Path 語法:
|
||||
- `M x,y`:移動到起點
|
||||
- `L x,y`:直線到指定點
|
||||
|
||||
範例:
|
||||
```
|
||||
M 2024-01-15,0 L 2024-01-15,0.3 L 2024-01-16,0.3 L 2024-01-16,0.6
|
||||
```
|
||||
|
||||
### 避碰演算法複雜度
|
||||
- 時間複雜度:O(n × m × k)
|
||||
- n = 事件數
|
||||
- m = 平均層級數
|
||||
- k = 偏移嘗試次數(最多7次)
|
||||
- 空間複雜度:O(n × m)
|
||||
|
||||
### 改進方向
|
||||
未來可考慮:
|
||||
1. 使用力導向演算法優化標籤位置
|
||||
2. 支援標籤尺寸動態計算(根據文字長度)
|
||||
3. 添加標籤碰撞預覽功能
|
||||
@@ -1,230 +0,0 @@
|
||||
# 時間軸標籤避碰改進(v3.0) - 平滑曲線與時間分離
|
||||
|
||||
## 新增改進(v3.0)
|
||||
|
||||
### 1. **平滑曲線連接 - 避免視覺阻礙**
|
||||
|
||||
#### 問題
|
||||
- Z 形折線雖然清晰,但仍可能阻礙其他文字框或連線
|
||||
- 多條連線交叉時造成視覺混亂
|
||||
|
||||
#### 解決方案
|
||||
使用**平滑曲線 + 虛線 + 半透明**組合:
|
||||
|
||||
```python
|
||||
# 5 個控制點創建平滑曲線
|
||||
line_x_points = [
|
||||
event_x, # 起點:事件點
|
||||
event_x, # 垂直上升
|
||||
curve_x, # 曲線控制點(帶偏移)
|
||||
label_x, # 水平接近
|
||||
label_x # 終點:標籤
|
||||
]
|
||||
|
||||
# Y 座標使用漸進式高度
|
||||
line_y_points = [
|
||||
0, # 起點
|
||||
mid_y * 0.7, # 70% 高度
|
||||
mid_y, # 中間高度
|
||||
mid_y * 0.7, # 70% 高度
|
||||
label_y # 終點
|
||||
]
|
||||
|
||||
# 視覺優化
|
||||
line: {
|
||||
'color': event_color,
|
||||
'width': 1,
|
||||
'dash': 'dot', # 虛線
|
||||
}
|
||||
opacity: 0.6 # 半透明
|
||||
```
|
||||
|
||||
**優勢**:
|
||||
- ✅ 虛線樣式不會完全遮擋背後內容
|
||||
- ✅ 半透明(60%)減少視覺阻礙
|
||||
- ✅ 平滑曲線更自然、更專業
|
||||
- ✅ 5 個控制點創造弧形路徑,避免直線交叉
|
||||
|
||||
---
|
||||
|
||||
### 2. **時間與標題分離顯示**
|
||||
|
||||
#### 問題
|
||||
- 標籤框同時顯示標題和時間,導致框體過大
|
||||
- 框體越大,避碰越困難
|
||||
|
||||
#### 解決方案
|
||||
**時間顯示在事件點旁邊**,標籤框只顯示標題:
|
||||
|
||||
```python
|
||||
# 時間標籤(靠近事件點)
|
||||
annotations.append({
|
||||
'x': event_x,
|
||||
'y': -0.15, # 在時間軸下方
|
||||
'text': f"{date_str}<br>{time_str}",
|
||||
'font': {'size': 9},
|
||||
'bgcolor': 'rgba(255, 255, 255, 0.95)',
|
||||
'bordercolor': event_color,
|
||||
'borderwidth': 1,
|
||||
'borderpad': 2
|
||||
})
|
||||
|
||||
# 標題標籤(在連線終點)
|
||||
annotations.append({
|
||||
'x': label_x,
|
||||
'y': label_y,
|
||||
'text': f"<b>{title}</b>", # 只顯示標題
|
||||
'font': {'size': 11},
|
||||
'borderwidth': 2,
|
||||
'borderpad': 6
|
||||
})
|
||||
```
|
||||
|
||||
**優勢**:
|
||||
- ✅ 標籤框更小,避碰更容易
|
||||
- ✅ 時間緊貼事件點,對應關係清晰
|
||||
- ✅ 標題框可以更大、更醒目
|
||||
- ✅ 視覺層次更分明
|
||||
|
||||
---
|
||||
|
||||
### 3. **時間精度到時分秒**
|
||||
|
||||
#### 改進前
|
||||
```
|
||||
日期: 2024-01-01
|
||||
```
|
||||
|
||||
#### 改進後
|
||||
```
|
||||
2024-01-01
|
||||
14:30:00
|
||||
```
|
||||
|
||||
**格式**:
|
||||
- 日期:`%Y-%m-%d`
|
||||
- 時間:`%H:%M:%S`
|
||||
- Hover 提示:`%Y-%m-%d %H:%M:%S`
|
||||
|
||||
---
|
||||
|
||||
## 完整改進對比
|
||||
|
||||
### v1.0(初版)
|
||||
- ❌ 直線連接
|
||||
- ❌ 標籤固定位置
|
||||
- ❌ 只有日期
|
||||
- ❌ 容易重疊
|
||||
|
||||
### v2.0(2D 避碰)
|
||||
- ✅ Z 形折線
|
||||
- ✅ 標籤可偏移
|
||||
- ✅ 智能避碰
|
||||
- ⚠️ 折線可能阻礙
|
||||
|
||||
### v3.0(平滑曲線 + 時間分離)
|
||||
- ✅ **平滑曲線(虛線 + 半透明)**
|
||||
- ✅ **時間顯示在點旁邊**
|
||||
- ✅ **標題與時間分離**
|
||||
- ✅ **時分秒精度**
|
||||
- ✅ **最小視覺阻礙**
|
||||
|
||||
---
|
||||
|
||||
## 視覺效果
|
||||
|
||||
### 連線樣式
|
||||
```
|
||||
事件點 ●
|
||||
┆ (虛線,60% 透明)
|
||||
┆
|
||||
╰─→ 標題框
|
||||
```
|
||||
|
||||
### 時間標籤位置
|
||||
```
|
||||
┌─────────┐
|
||||
│ 標題 │ ← 標籤框(只有標題)
|
||||
└─────────┘
|
||||
↑
|
||||
┆ (平滑曲線)
|
||||
●
|
||||
┌────────┐
|
||||
│2024-01 │ ← 時間標籤(在點旁邊)
|
||||
│14:30:00│
|
||||
└────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 關鍵參數
|
||||
|
||||
```python
|
||||
# 連線樣式
|
||||
line_width = 1 # 細線
|
||||
line_dash = 'dot' # 虛線
|
||||
line_opacity = 0.6 # 60% 透明
|
||||
|
||||
# 時間標籤
|
||||
time_font_size = 9 # 小字體
|
||||
time_position_y = -0.15 # 軸下方
|
||||
|
||||
# 標題標籤
|
||||
title_font_size = 11 # 較大字體
|
||||
title_borderwidth = 2 # 較粗邊框
|
||||
title_borderpad = 6 # 較大內距
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 測試方法
|
||||
|
||||
```batch
|
||||
start_dev.bat
|
||||
```
|
||||
|
||||
訪問 http://localhost:12010,測試示範檔案:
|
||||
- `demo_project_timeline.csv` - 15 個事件
|
||||
- `demo_life_events.csv` - 11 個事件
|
||||
- `demo_product_roadmap.csv` - 14 個事件
|
||||
|
||||
**預期效果**:
|
||||
- ✅ 平滑虛線連接,半透明不阻擋
|
||||
- ✅ 時間標籤緊貼事件點
|
||||
- ✅ 標題框簡潔醒目
|
||||
- ✅ 時分秒精度顯示
|
||||
- ✅ 整體視覺清爽專業
|
||||
|
||||
---
|
||||
|
||||
## 技術實現
|
||||
|
||||
### 平滑曲線演算法
|
||||
使用 5 個控制點創建漸進式曲線:
|
||||
|
||||
1. **起點**:事件點 (event_x, 0)
|
||||
2. **上升點**:(event_x, mid_y × 0.7)
|
||||
3. **曲線頂點**:(curve_x, mid_y) - 帶水平偏移
|
||||
4. **下降點**:(label_x, mid_y × 0.7)
|
||||
5. **終點**:標籤位置 (label_x, label_y)
|
||||
|
||||
**曲線偏移計算**:
|
||||
```python
|
||||
x_diff = abs((label_x - event_x).total_seconds())
|
||||
curve_offset = timedelta(seconds=x_diff * 0.2) # 20% 偏移
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 未來改進方向
|
||||
|
||||
1. **貝茲曲線**:使用真正的貝茲曲線(需要更複雜的數學計算)
|
||||
2. **路徑避障**:實現 A* 演算法自動繞過文字框
|
||||
3. **動態透明度**:根據重疊程度調整透明度
|
||||
4. **智能顏色**:根據背景自動調整連線顏色
|
||||
|
||||
---
|
||||
|
||||
**版本**: v3.0
|
||||
**更新日期**: 2025-11-05
|
||||
**作者**: Claude AI
|
||||
@@ -1,393 +0,0 @@
|
||||
# 時間軸標籤避碰改進(v4.0) - 防止線條交錯
|
||||
|
||||
## 新增改進(v4.0)
|
||||
|
||||
### 問題描述
|
||||
v3.0 雖然解決了文字框重疊問題,但仍存在以下問題:
|
||||
1. ❌ 連接線互相交錯
|
||||
2. ❌ 連接線穿過其他文字框
|
||||
3. ❌ 密集事件時視覺混亂
|
||||
|
||||
### 解決方案
|
||||
|
||||
#### 1. **智能路徑分層**
|
||||
|
||||
**核心概念**:讓不同層級的連接線使用不同的中間高度/寬度,避免交錯。
|
||||
|
||||
```python
|
||||
# 水平時間軸(L 形折線的中間高度)
|
||||
base_ratio = 0.45 # 基礎高度比例
|
||||
layer_offset = (layer % 6) * 0.10 # 每層偏移 10%,每 6 層循環
|
||||
mid_y_ratio = base_ratio + layer_offset
|
||||
mid_y = label_y * mid_y_ratio
|
||||
|
||||
# 垂直時間軸(L 形折線的中間寬度)
|
||||
base_ratio = 0.45 # 基礎寬度比例
|
||||
layer_offset = (layer % 6) * 0.10 # 每層偏移 10%
|
||||
mid_x_ratio = base_ratio + layer_offset
|
||||
mid_x = label_x * mid_x_ratio
|
||||
```
|
||||
|
||||
**效果**:
|
||||
- 層級 0:中間點在 45% 位置
|
||||
- 層級 1:中間點在 55% 位置
|
||||
- 層級 2:中間點在 65% 位置
|
||||
- 層級 3:中間點在 75% 位置
|
||||
- 層級 4:中間點在 85% 位置
|
||||
- 層級 5:中間點在 95% 位置
|
||||
- 層級 6:循環回 45% 位置
|
||||
|
||||
#### 2. **避開文字框核心區域**
|
||||
|
||||
防止線條的水平段太接近文字框中心:
|
||||
|
||||
```python
|
||||
# 如果計算出的中間點太接近文字框位置,則強制調整
|
||||
if abs(mid_y - label_y) < abs(label_y) * 0.15:
|
||||
mid_y = label_y * 0.35 # 設為更安全的距離
|
||||
```
|
||||
|
||||
**效果**:
|
||||
- ✅ 線條不會直接穿過文字框中心
|
||||
- ✅ 保持至少 15% 的安全距離
|
||||
|
||||
#### 3. **增加文字框間距**
|
||||
|
||||
調整碰撞檢測參數,確保文字框之間有足夠空間:
|
||||
|
||||
```python
|
||||
# 標籤寬度(包含時間+標題+描述)
|
||||
label_width_ratio = 0.15 # 15% 的時間軸寬度
|
||||
|
||||
# 安全邊距
|
||||
safety_margin = total_seconds * 0.01 # 1% 的額外緩衝
|
||||
|
||||
# 最小水平間距
|
||||
min_horizontal_gap = total_seconds * 0.03 # 3% 的時間軸寬度
|
||||
|
||||
# 層級垂直間距
|
||||
layer_spacing = 1.0 # 層級之間的垂直距離
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 完整改進歷程
|
||||
|
||||
### v1.0(初版)
|
||||
- ❌ 直線連接
|
||||
- ❌ 標籤固定位置
|
||||
- ❌ 只有日期
|
||||
- ❌ 容易重疊
|
||||
|
||||
### v2.0(2D 避碰)
|
||||
- ✅ Z 形折線
|
||||
- ✅ 標籤可偏移
|
||||
- ✅ 智能避碰
|
||||
- ⚠️ 折線可能交錯
|
||||
|
||||
### v3.0(平滑曲線 + 時間分離)
|
||||
- ✅ 平滑曲線(虛線 + 半透明)
|
||||
- ✅ 時間顯示在點旁邊
|
||||
- ✅ 標題與時間分離
|
||||
- ⚠️ 用戶反饋:需要簡化
|
||||
|
||||
### v3.1(簡化版)
|
||||
- ✅ L 形直角折線(取代曲線)
|
||||
- ✅ 時間+標題+描述統一顯示
|
||||
- ✅ 時分秒精度
|
||||
- ⚠️ 文字框重疊
|
||||
|
||||
### v3.2(增加間距)
|
||||
- ✅ 增加標籤寬度(15%)
|
||||
- ✅ 增加層級間距(1.0)
|
||||
- ✅ 添加安全邊距(1%)
|
||||
- ⚠️ 線條仍會交錯
|
||||
|
||||
### v4.0(智能路徑分層 - 初版)
|
||||
- ✅ 不同層級使用不同高度/寬度
|
||||
- ✅ 線條避開文字框核心區域
|
||||
- ✅ 文字框之間充足間距
|
||||
- ⚠️ 仍有交錯問題(用戶反饋)
|
||||
|
||||
### v4.1(當前版本 - 鏡像分布 + 距離感知)
|
||||
- ✅ **上下/左右鏡像分布策略**
|
||||
- ✅ **根據跨越距離動態調整路徑**
|
||||
- ✅ **10層循環,60%範圍變化**
|
||||
- ✅ **長距離線條自動降低高度**
|
||||
- ✅ **線條交錯最小化**
|
||||
- ✅ **整體視覺清晰專業**
|
||||
|
||||
---
|
||||
|
||||
## v4.1 核心改進
|
||||
|
||||
### 1. **鏡像分布策略**
|
||||
|
||||
**問題**:v4.0 中上下兩側的線條使用相同的分層策略,容易在中間區域交錯。
|
||||
|
||||
**解決**:上下(或左右)兩側使用鏡像分布:
|
||||
|
||||
```python
|
||||
# 水平時間軸
|
||||
if is_upper_side: # 上方
|
||||
base_ratio = 0.25 # 從 25% 開始
|
||||
layer_offset = layer_group * 0.06 # 正向增長: 25% -> 85%
|
||||
else: # 下方
|
||||
base_ratio = 0.85 # 從 85% 開始
|
||||
layer_offset = -layer_group * 0.06 # 負向增長: 85% -> 25%
|
||||
```
|
||||
|
||||
**效果**:
|
||||
- 上方 layer 0 在 25%,下方 layer 0 在 85% → 分隔明顯
|
||||
- 上方 layer 5 在 55%,下方 layer 5 在 55% → 在中間匯合
|
||||
- 上方 layer 9 在 79%,下方 layer 9 在 31% → 接近但不重疊
|
||||
|
||||
### 2. **距離感知調整**
|
||||
|
||||
**問題**:長距離線條容易穿過中間的文字框。
|
||||
|
||||
**解決**:根據跨越距離動態調整中間點高度:
|
||||
|
||||
```python
|
||||
x_span_ratio = abs(x_diff_seconds) / total_range
|
||||
|
||||
if x_span_ratio > 0.3: # 跨越超過 30% 時間軸
|
||||
# 上方線條降低,下方線條升高,避開中間區域
|
||||
distance_adjustment = -0.10 if is_upper_side else 0.10
|
||||
elif x_span_ratio > 0.15: # 跨越 15-30%
|
||||
distance_adjustment = -0.05 if is_upper_side else 0.05
|
||||
else:
|
||||
distance_adjustment = 0 # 短距離不調整
|
||||
```
|
||||
|
||||
**效果**:
|
||||
- ✅ 短距離線條:保持原有層級策略
|
||||
- ✅ 中距離線條:輕微調整 5%
|
||||
- ✅ 長距離線條:大幅調整 10%,遠離文字框密集區
|
||||
|
||||
### 3. **增加層級循環週期**
|
||||
|
||||
```python
|
||||
layer_group = layer % 10 # 從 6 層增加到 10 層
|
||||
```
|
||||
|
||||
**效果**:
|
||||
- 提供更多的高度選擇(10 個不同高度)
|
||||
- 減少不同層級使用相同高度的機率
|
||||
- 更細緻的分布
|
||||
|
||||
---
|
||||
|
||||
## 技術細節
|
||||
|
||||
### 路徑分層算法(v4.1)
|
||||
|
||||
**計算公式**:
|
||||
```
|
||||
mid_y_ratio = base_ratio + layer_offset + distance_adjustment
|
||||
mid_y_ratio = max(0.20, min(mid_y_ratio, 0.90)) # 限制範圍
|
||||
mid_y = label_y * mid_y_ratio
|
||||
```
|
||||
|
||||
**參數範圍**:
|
||||
- `base_ratio`: 上方 0.25,下方 0.85
|
||||
- `layer_offset`: -0.54 到 +0.54 (10層 × 6%)
|
||||
- `distance_adjustment`: -0.10 到 +0.10
|
||||
- **總範圍**: 20% 到 90%(70% 的可用空間)
|
||||
|
||||
### 路徑分層算法(v4.0 舊版)
|
||||
|
||||
**水平時間軸**:
|
||||
```
|
||||
事件點 (event_x, 0)
|
||||
↓ 垂直上升
|
||||
中間點 (event_x, mid_y) ← 根據層級調整
|
||||
→ 水平移動
|
||||
轉折點 (label_x, mid_y)
|
||||
↓ 垂直下降
|
||||
文字框 (label_x, label_y)
|
||||
```
|
||||
|
||||
**垂直時間軸**:
|
||||
```
|
||||
事件點 (0, event_y)
|
||||
→ 水平移動
|
||||
中間點 (mid_x, event_y) ← 根據層級調整
|
||||
↓ 垂直移動
|
||||
轉折點 (mid_x, label_y)
|
||||
→ 水平移動
|
||||
文字框 (label_x, label_y)
|
||||
```
|
||||
|
||||
### 碰撞檢測策略
|
||||
|
||||
1. **計算標籤佔用範圍**(包含安全邊距)
|
||||
2. **嘗試在同層級放置**(無偏移)
|
||||
3. **嘗試水平偏移**(左側 1x, 2x, 3x)
|
||||
4. **嘗試水平偏移**(右側 1x, 2x, 3x)
|
||||
5. **創建新層級**(如果都無法容納)
|
||||
|
||||
### 性能優化
|
||||
|
||||
- **時間複雜度**:O(n × m × k)
|
||||
- n = 事件數
|
||||
- m = 平均層級數(通常 < 5)
|
||||
- k = 偏移嘗試次數(最多 7 次)
|
||||
|
||||
- **空間複雜度**:O(n × m)
|
||||
|
||||
---
|
||||
|
||||
## 調整建議
|
||||
|
||||
如果仍有線條交錯問題,可以調整以下參數:
|
||||
|
||||
### 1. 增加層級偏移幅度
|
||||
```python
|
||||
# renderer_timeline.py 第 336 行和第 566 行
|
||||
layer_offset = (layer % 6) * 0.12 # 從 0.10 增加到 0.12
|
||||
```
|
||||
|
||||
### 2. 降低基礎比例
|
||||
```python
|
||||
# renderer_timeline.py 第 335 行和第 565 行
|
||||
base_ratio = 0.40 # 從 0.45 降低到 0.40
|
||||
```
|
||||
|
||||
### 3. 增加循環週期
|
||||
```python
|
||||
# renderer_timeline.py 第 336 行和第 566 行
|
||||
layer_offset = (layer % 8) * 0.10 # 從 6 層循環改為 8 層循環
|
||||
```
|
||||
|
||||
### 4. 增加文字框間距
|
||||
```python
|
||||
# renderer_timeline.py 第 81、88、230、449 行
|
||||
label_width_ratio = 0.18 # 從 0.15 增加到 0.18
|
||||
min_horizontal_gap = total_seconds * 0.04 # 從 0.03 增加到 0.04
|
||||
layer_spacing = 1.2 # 從 1.0 增加到 1.2
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 測試方法
|
||||
|
||||
```batch
|
||||
start_dev.bat
|
||||
```
|
||||
|
||||
訪問 http://localhost:12010,測試示範檔案:
|
||||
- `demo_project_timeline.csv` - 15 個事件
|
||||
- `demo_life_events.csv` - 11 個事件
|
||||
- `demo_product_roadmap.csv` - 14 個事件
|
||||
|
||||
**預期效果**:
|
||||
- ✅ 文字框之間無重疊
|
||||
- ✅ 連接線分散在不同高度
|
||||
- ✅ 線條避開文字框核心區域
|
||||
- ✅ 線條交錯大幅減少
|
||||
- ✅ 整體視覺清晰易讀
|
||||
|
||||
---
|
||||
|
||||
## 視覺效果示意(v4.1)
|
||||
|
||||
### 鏡像分布示意
|
||||
|
||||
```
|
||||
100% ╔══════════════════════════════════════╗
|
||||
║ ║
|
||||
85% ╟────┐ 下方 Layer 0 (base) ║
|
||||
║ │ ║
|
||||
79% ╟────┘ 下方 Layer 1 ║
|
||||
║ ║
|
||||
70% ╟───── 中間區域(避開) ║
|
||||
║ ║
|
||||
55% ╟────┐ 上方/下方 Layer 5 (匯合點) ║
|
||||
║ │ ║
|
||||
40% ╟───── 中間區域(避開) ║
|
||||
║ ║
|
||||
31% ╟────┐ 上方 Layer 1 ║
|
||||
║ │ ║
|
||||
25% ╟────┘ 上方 Layer 0 (base) ║
|
||||
║ ║
|
||||
20% ╚══════════════════════════════════════╝
|
||||
▲
|
||||
└─ 時間軸 (0%)
|
||||
```
|
||||
|
||||
**特點**:
|
||||
- ✅ 上下兩側從不同端點開始
|
||||
- ✅ 在中間區域匯合,但錯開
|
||||
- ✅ 最大程度利用 70% 的垂直空間
|
||||
- ✅ 避免在中間區域(40%-70%)密集重疊
|
||||
|
||||
### 距離感知調整示意
|
||||
|
||||
```
|
||||
短距離 (< 15%):
|
||||
●─────┐
|
||||
└──□ 使用標準層級高度
|
||||
|
||||
中距離 (15%-30%):
|
||||
●─────────┐
|
||||
└──□ 降低 5%(上方)或升高 5%(下方)
|
||||
|
||||
長距離 (> 30%):
|
||||
●──────────────┐
|
||||
└──□ 降低 10%(上方)或升高 10%(下方)
|
||||
遠離中間密集區
|
||||
```
|
||||
|
||||
### 線條分層(v4.0 舊版)
|
||||
```
|
||||
┌─────────┐
|
||||
│文字框 3 │ (層級2)
|
||||
└─────────┘
|
||||
↑
|
||||
│ (mid_y = 65%)
|
||||
├────────────
|
||||
┌─────────┐
|
||||
│文字框 2 │ (層級1)
|
||||
└─────────┘
|
||||
↑
|
||||
│ (mid_y = 55%)
|
||||
────┼────────────
|
||||
┌─────────┐
|
||||
│文字框 1 │ (層級0)
|
||||
└─────────┘
|
||||
↑
|
||||
────┘ (mid_y = 45%)
|
||||
●
|
||||
時間軸
|
||||
```
|
||||
|
||||
### 避開核心區域
|
||||
```
|
||||
┌───────────┐
|
||||
│ 文字框 │
|
||||
│ 核心區域 │ ← 線條不會穿過這裡
|
||||
│ │
|
||||
└───────────┘
|
||||
↑
|
||||
────────┘ (保持安全距離)
|
||||
●
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**版本**: v4.0
|
||||
**更新日期**: 2025-11-05
|
||||
**作者**: Claude AI
|
||||
|
||||
## 關鍵改進總結
|
||||
|
||||
| 項目 | v3.2 | v4.0 | v4.1 | 改進方法 |
|
||||
|-----|------|------|------|---------|
|
||||
| 文字框重疊 | ✅ 已解決 | ✅ 已解決 | ✅ 已解決 | 增加間距與安全邊距 |
|
||||
| 線條交錯 | ❌ 嚴重 | ⚠️ 仍存在 | ✅ 最小化 | 鏡像分布 + 距離感知 |
|
||||
| 線條穿框 | ❌ 經常 | ⚠️ 偶爾 | ✅ 極少 | 距離感知動態調整 |
|
||||
| 視覺清晰度 | ⚠️ 中等 | ✅ 良好 | ✅ 優秀 | 多層次優化 |
|
||||
| 配置靈活性 | ✅ 可調 | ✅ 高度可調 | ✅ 智能自適應 | 動態參數計算 |
|
||||
| 層級分布 | 單向 | 單向 | 鏡像 | 上下/左右對稱策略 |
|
||||
| 距離處理 | 固定 | 固定 | 動態 | 根據跨越距離調整 |
|
||||
@@ -1,322 +0,0 @@
|
||||
# 時間軸標籤避碰改進(v5.0) - 真正的碰撞預防系統
|
||||
|
||||
## 新增改進(v5.0)
|
||||
|
||||
### 核心概念:從靜態分層到動態碰撞檢測
|
||||
|
||||
**v4.1 的問題**:
|
||||
- ❌ 只是根據層級靜態計算路徑高度
|
||||
- ❌ 沒有真正檢測線條之間的碰撞
|
||||
- ❌ 沒有檢測線條與文字框的碰撞
|
||||
- ❌ 仍然會出現嚴重的重疊
|
||||
|
||||
**v5.0 的解決方案**:
|
||||
- ✅ **真正的碰撞檢測算法**
|
||||
- ✅ **動態路徑優化**
|
||||
- ✅ **20個候選高度,選擇最佳路徑**
|
||||
- ✅ **實時追蹤已繪製的線條和文字框**
|
||||
|
||||
---
|
||||
|
||||
## 技術實現
|
||||
|
||||
### 1. **碰撞檢測算法**
|
||||
|
||||
#### 線段與線段碰撞檢測
|
||||
```python
|
||||
def check_collision(x_start_sec, x_end_sec, y_height, margin=0.05):
|
||||
collision_score = 0
|
||||
|
||||
# 檢查與已繪製線段的碰撞
|
||||
for seg_start, seg_end, seg_y in drawn_horizontal_segments:
|
||||
# Y 座標是否接近(在 margin 範圍內)
|
||||
if abs(y_height - seg_y) < margin:
|
||||
# X 範圍是否重疊
|
||||
if not (x_end_sec < seg_start or x_start_sec > seg_end):
|
||||
overlap = min(x_end_sec, seg_end) - max(x_start_sec, seg_start)
|
||||
collision_score += overlap / (x_end_sec - x_start_sec + 1)
|
||||
|
||||
return collision_score
|
||||
```
|
||||
|
||||
**邏輯**:
|
||||
- 檢查新線段的水平部分是否與已有線段在同一高度(±5%範圍內)
|
||||
- 計算 X 軸重疊的比例
|
||||
- 重疊越多,碰撞分數越高
|
||||
|
||||
#### 線段與文字框碰撞檢測
|
||||
```python
|
||||
# 檢查與文字框的碰撞
|
||||
for box_x, box_y, box_w, box_h in text_boxes:
|
||||
# Y 座標是否在文字框範圍內
|
||||
if abs(y_height - box_y) < box_h / 2 + margin:
|
||||
# X 範圍是否穿過文字框
|
||||
box_left = box_x - box_w / 2
|
||||
box_right = box_x + box_w / 2
|
||||
if not (x_end_sec < box_left or x_start_sec > box_right):
|
||||
overlap = min(x_end_sec, box_right) - max(x_start_sec, box_left)
|
||||
collision_score += overlap / (x_end_sec - x_start_sec + 1) * 2 # 權重 x2
|
||||
```
|
||||
|
||||
**邏輯**:
|
||||
- 檢查線段是否穿過文字框的垂直範圍
|
||||
- 計算與文字框的 X 軸重疊
|
||||
- 文字框碰撞的權重是線段碰撞的2倍(更嚴重)
|
||||
|
||||
### 2. **最佳路徑選擇**
|
||||
|
||||
```python
|
||||
def find_best_path_height(event_x_sec, label_x_sec, label_y, layer):
|
||||
is_upper = label_y > 0
|
||||
|
||||
# 生成20個候選高度
|
||||
candidates = []
|
||||
if is_upper:
|
||||
# 上方:從 20% 到 90% (每次增加 3.5%)
|
||||
for i in range(20):
|
||||
ratio = 0.20 + (i * 0.035)
|
||||
candidates.append(ratio)
|
||||
else:
|
||||
# 下方:從 90% 到 20% (每次減少 3.5%)
|
||||
for i in range(20):
|
||||
ratio = 0.90 - (i * 0.035)
|
||||
candidates.append(ratio)
|
||||
|
||||
# 計算每個高度的碰撞分數
|
||||
best_ratio = candidates[layer % len(candidates)] # 默認值
|
||||
min_collision = float('inf')
|
||||
|
||||
x_start = min(event_x_sec, label_x_sec)
|
||||
x_end = max(event_x_sec, label_x_sec)
|
||||
|
||||
for ratio in candidates:
|
||||
test_y = label_y * ratio
|
||||
score = check_collision(x_start, x_end, test_y)
|
||||
if score < min_collision:
|
||||
min_collision = score
|
||||
best_ratio = ratio
|
||||
|
||||
return best_ratio
|
||||
```
|
||||
|
||||
**邏輯**:
|
||||
1. 根據標籤位置(上方/下方)生成20個候選高度
|
||||
2. 對每個候選高度計算碰撞分數
|
||||
3. 選擇碰撞分數最低的高度
|
||||
4. 如果所有高度碰撞分數相同(都是0),使用層級對應的默認高度
|
||||
|
||||
### 3. **實時追蹤系統**
|
||||
|
||||
```python
|
||||
# 初始化追蹤列表
|
||||
drawn_horizontal_segments = [] # [(x_start, x_end, y), ...]
|
||||
text_boxes = [] # [(x_center, y_center, width, height), ...]
|
||||
|
||||
# 繪製後記錄
|
||||
if not is_directly_above:
|
||||
drawn_horizontal_segments.append((x_start_sec, x_end_sec, mid_y))
|
||||
|
||||
text_boxes.append((label_x_sec, label_y, label_width_sec, label_height))
|
||||
```
|
||||
|
||||
**效果**:
|
||||
- 每繪製一條線段,立即記錄其位置
|
||||
- 每繪製一個文字框,立即記錄其範圍
|
||||
- 後續線條會避開已記錄的所有障礙物
|
||||
|
||||
---
|
||||
|
||||
## 效果對比
|
||||
|
||||
### v4.1(靜態分層)
|
||||
```python
|
||||
# 只根據層級計算高度
|
||||
if is_upper_side:
|
||||
base_ratio = 0.25
|
||||
layer_offset = layer_group * 0.06
|
||||
mid_y_ratio = base_ratio + layer_offset
|
||||
|
||||
❌ 問題:無法知道這個高度是否會碰撞
|
||||
```
|
||||
|
||||
### v5.0(動態碰撞檢測)
|
||||
```python
|
||||
# 測試20個候選高度
|
||||
for ratio in candidates:
|
||||
test_y = label_y * ratio
|
||||
score = check_collision(x_start, x_end, test_y)
|
||||
if score < min_collision:
|
||||
best_ratio = ratio
|
||||
|
||||
✅ 優勢:保證選擇碰撞最少的路徑
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 性能分析
|
||||
|
||||
### 時間複雜度
|
||||
- **單條線路徑選擇**:O(候選數 × (已繪線段數 + 文字框數))
|
||||
- **全部線條**:O(事件數 × 候選數 × 事件數) = O(20n²)
|
||||
- **實際情況**:因為是按順序繪製,平均複雜度約為 O(10n²)
|
||||
|
||||
### 空間複雜度
|
||||
- **線段追蹤**:O(事件數)
|
||||
- **文字框追蹤**:O(事件數)
|
||||
- **總計**:O(事件數)
|
||||
|
||||
### 性能表現
|
||||
- 10 個事件:~2000 次碰撞檢測
|
||||
- 50 個事件:~50000 次碰撞檢測
|
||||
- 100 個事件:~200000 次碰撞檢測
|
||||
|
||||
**優化空間**:
|
||||
- 可以使用空間索引(R-tree)降低到 O(n log n)
|
||||
- 可以減少候選數量(從20降到10)
|
||||
- 可以使用啟發式策略減少檢測次數
|
||||
|
||||
---
|
||||
|
||||
## 參數配置
|
||||
|
||||
```python
|
||||
# 碰撞檢測參數
|
||||
margin = 0.05 # Y 軸碰撞容忍度(5%)
|
||||
text_box_weight = 2.0 # 文字框碰撞權重(x2)
|
||||
|
||||
# 候選高度參數
|
||||
candidates_count = 20 # 候選高度數量
|
||||
upper_range = (0.20, 0.90) # 上方高度範圍 20%-90%
|
||||
lower_range = (0.90, 0.20) # 下方高度範圍 90%-20%
|
||||
step = 0.035 # 每次增減 3.5%
|
||||
|
||||
# 文字框估算參數
|
||||
label_width_ratio = 0.15 # 文字框寬度 = 15% 時間軸
|
||||
label_height = 0.3 # 文字框高度 = 0.3 單位
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 調整建議
|
||||
|
||||
### 如果仍有碰撞
|
||||
|
||||
1. **增加候選高度數量**
|
||||
```python
|
||||
for i in range(30): # 從 20 增加到 30
|
||||
ratio = 0.20 + (i * 0.024) # 調整步長
|
||||
```
|
||||
|
||||
2. **增加碰撞容忍度**
|
||||
```python
|
||||
margin = 0.08 # 從 0.05 增加到 0.08
|
||||
```
|
||||
|
||||
3. **增加文字框尺寸估算**
|
||||
```python
|
||||
label_width_sec = time_range_seconds * 0.18 # 從 0.15 增加到 0.18
|
||||
label_height = 0.4 # 從 0.3 增加到 0.4
|
||||
```
|
||||
|
||||
### 如果性能太慢
|
||||
|
||||
1. **減少候選數量**
|
||||
```python
|
||||
for i in range(10): # 從 20 減少到 10
|
||||
```
|
||||
|
||||
2. **使用啟發式優先級**
|
||||
```python
|
||||
# 優先測試層級對應的高度附近的候選
|
||||
priority_candidates = [
|
||||
candidates[layer % len(candidates)], # 優先級1:層級對應
|
||||
candidates[(layer-1) % len(candidates)], # 優先級2:相鄰
|
||||
candidates[(layer+1) % len(candidates)], # 優先級3:相鄰
|
||||
# ... 然後測試其他候選
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 視覺效果
|
||||
|
||||
### 碰撞檢測過程示意
|
||||
|
||||
```
|
||||
測試候選高度 ratio=0.20 (20%):
|
||||
████████████ 線段1 (已存在)
|
||||
────────────────── 測試線段 ← 碰撞! score=0.8
|
||||
|
||||
測試候選高度 ratio=0.35 (35%):
|
||||
|
||||
────────────────── 測試線段 ← 無碰撞! score=0.0 ✓
|
||||
|
||||
████████████ 線段1 (已存在)
|
||||
|
||||
選擇 ratio=0.35,碰撞分數最低
|
||||
```
|
||||
|
||||
### 文字框避讓示意
|
||||
|
||||
```
|
||||
┌──────────┐
|
||||
│ 文字框A │ (已存在)
|
||||
└──────────┘
|
||||
↓
|
||||
────────── 測試路徑1 ← 穿過文字框! score=1.5
|
||||
↓
|
||||
|
||||
─────────── 測試路徑2 ← 避開文字框! score=0.0 ✓
|
||||
|
||||
●
|
||||
時間軸
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 版本改進總結
|
||||
|
||||
| 版本 | 方法 | 線條交錯 | 線條穿框 | 性能 |
|
||||
|------|------|----------|----------|------|
|
||||
| v3.2 | 增加間距 | ❌ 嚴重 | ❌ 嚴重 | ⚡ 快 |
|
||||
| v4.0 | 層級偏移 | ⚠️ 存在 | ⚠️ 偶爾 | ⚡ 快 |
|
||||
| v4.1 | 鏡像分布 | ⚠️ 仍有 | ⚠️ 仍有 | ⚡ 快 |
|
||||
| **v5.0** | **碰撞檢測** | **✅ 最小** | **✅ 極少** | **⚡ 中等** |
|
||||
|
||||
---
|
||||
|
||||
## 未來改進方向
|
||||
|
||||
### 1. **空間索引優化**
|
||||
使用 R-tree 或 KD-tree 加速碰撞檢測:
|
||||
- 當前:O(n) 檢測每個障礙物
|
||||
- 優化後:O(log n) 查詢相關障礙物
|
||||
|
||||
### 2. **貝茲曲線**
|
||||
使用平滑曲線代替直角折線:
|
||||
- 更自然的視覺效果
|
||||
- 更容易避開障礙物
|
||||
|
||||
### 3. **A* 路徑規劃**
|
||||
使用圖搜索算法找到最優路徑:
|
||||
- 可以繞過複雜的障礙物布局
|
||||
- 保證找到全局最優解
|
||||
|
||||
### 4. **分組優化**
|
||||
對事件進行分組,組內使用相似的路徑高度:
|
||||
- 減少視覺混亂
|
||||
- 突出事件的邏輯關係
|
||||
|
||||
---
|
||||
|
||||
**版本**: v5.0
|
||||
**更新日期**: 2025-11-05
|
||||
**作者**: Claude AI
|
||||
|
||||
## 關鍵突破
|
||||
|
||||
從**靜態規則**到**動態智能**:
|
||||
- v1-v4:根據規則計算路徑 → 希望不會碰撞
|
||||
- **v5**:測試所有可能路徑 → **保證選擇最佳路徑**
|
||||
|
||||
這是從**被動避讓**到**主動檢測**的質的飛躍! 🚀
|
||||
@@ -1,303 +0,0 @@
|
||||
# 時間軸標籤避碰改進(v6.0) - 泳道分配法
|
||||
|
||||
## 核心轉變
|
||||
|
||||
### 從複雜碰撞檢測到簡單泳道分配
|
||||
|
||||
**v5.x 的問題**:
|
||||
- ❌ 碰撞檢測邏輯複雜,容易出bug
|
||||
- ❌ 即使檢測到碰撞,仍然可能選擇"最少碰撞"但仍有碰撞的路徑
|
||||
- ❌ 性能開銷大(O(n²))
|
||||
- ❌ **實際測試仍有嚴重交錯問題**
|
||||
|
||||
**v6.0 的解決方案 - 泳道分配法**:
|
||||
- ✅ **每個層級分配固定的高度**(像游泳池的泳道)
|
||||
- ✅ **100% 保證同層級線條高度一致**
|
||||
- ✅ **100% 保證不同層級線條不會交錯**
|
||||
- ✅ **簡單、可靠、高性能**
|
||||
|
||||
---
|
||||
|
||||
## 技術實現
|
||||
|
||||
### 泳道高度計算
|
||||
|
||||
```python
|
||||
# 計算總層級數
|
||||
total_layers = max_layer + 1
|
||||
|
||||
# 為每個層級分配固定的泳道高度
|
||||
lane_index = layer # 當前層級索引
|
||||
|
||||
if is_upper:
|
||||
# 上方:均勻分布在 20%-95% 範圍內
|
||||
if total_layers > 1:
|
||||
lane_ratio = 0.20 + (lane_index / (total_layers - 1)) * 0.75
|
||||
else:
|
||||
lane_ratio = 0.50
|
||||
else:
|
||||
# 下方:均勻分布在 95%-20% 範圍內(反向)
|
||||
if total_layers > 1:
|
||||
lane_ratio = 0.95 - (lane_index / (total_layers - 1)) * 0.75
|
||||
else:
|
||||
lane_ratio = 0.50
|
||||
|
||||
# 限制範圍
|
||||
lane_ratio = max(0.15, min(lane_ratio, 0.95))
|
||||
|
||||
# 計算最終高度
|
||||
mid_y = label_y * lane_ratio
|
||||
```
|
||||
|
||||
### 分配示例
|
||||
|
||||
假設有 5 個層級(0-4),上方標籤:
|
||||
|
||||
| 層級 | 計算 | 高度比例 | 實際效果 |
|
||||
|-----|------|---------|---------|
|
||||
| 0 | 0.20 + (0/4) × 0.75 | **20%** | 最低 |
|
||||
| 1 | 0.20 + (1/4) × 0.75 | **38.75%** | 低 |
|
||||
| 2 | 0.20 + (2/4) × 0.75 | **57.5%** | 中 |
|
||||
| 3 | 0.20 + (3/4) × 0.75 | **76.25%** | 高 |
|
||||
| 4 | 0.20 + (4/4) × 0.75 | **95%** | 最高 |
|
||||
|
||||
**特點**:
|
||||
- ✅ 均勻分布在整個可用空間
|
||||
- ✅ 每個層級有固定的高度
|
||||
- ✅ 層級之間間距相等
|
||||
|
||||
---
|
||||
|
||||
## 視覺效果
|
||||
|
||||
### 泳道分配示意圖
|
||||
|
||||
```
|
||||
100% ╔══════════════════════════════════════╗
|
||||
║ ║
|
||||
95% ╟────────── 泳道 4 (下方 Layer 0) ║
|
||||
║ 所有此層級的線都在這裡 ║
|
||||
76% ╟────────── 泳道 3 (下方 Layer 1) ║
|
||||
║ ║
|
||||
58% ╟────────── 泳道 2 (上方 Layer 2) ║
|
||||
║ ║
|
||||
39% ╟────────── 泳道 1 (上方 Layer 1) ║
|
||||
║ ║
|
||||
20% ╟────────── 泳道 0 (上方 Layer 0) ║
|
||||
║ 所有此層級的線都在這裡 ║
|
||||
15% ╚══════════════════════════════════════╝
|
||||
▲
|
||||
└─ 時間軸 (0%)
|
||||
```
|
||||
|
||||
**保證**:
|
||||
- 🔒 泳道 0 的所有線條永遠在 20% 高度
|
||||
- 🔒 泳道 1 的所有線條永遠在 38.75% 高度
|
||||
- 🔒 不同泳道的線條永遠不會交錯
|
||||
- 🔒 100% 視覺清晰
|
||||
|
||||
---
|
||||
|
||||
## 與 v5.x 對比
|
||||
|
||||
### v5.x(碰撞檢測法)
|
||||
|
||||
```python
|
||||
# 測試20-30個候選高度
|
||||
for ratio in candidates:
|
||||
score = check_collision(...)
|
||||
if score < min_score:
|
||||
best_ratio = ratio
|
||||
|
||||
❌ 問題:
|
||||
- 如果所有候選都有碰撞,選擇"最少碰撞"仍然會碰撞
|
||||
- 碰撞檢測可能有bug
|
||||
- 複雜度高
|
||||
```
|
||||
|
||||
### v6.0(泳道分配法)
|
||||
|
||||
```python
|
||||
# 根據層級直接計算固定高度
|
||||
lane_ratio = 0.20 + (lane_index / (total_layers - 1)) * 0.75
|
||||
|
||||
✅ 優勢:
|
||||
- 簡單、可預測
|
||||
- 100% 保證不交錯
|
||||
- 性能高 O(1)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 代碼簡化
|
||||
|
||||
### 移除的代碼
|
||||
|
||||
```python
|
||||
❌ check_collision() # 320+ 行碰撞檢測函數
|
||||
❌ find_best_path_height() # 80+ 行路徑選擇函數
|
||||
❌ drawn_horizontal_segments # 線段追蹤列表
|
||||
❌ text_boxes # 文字框追蹤列表
|
||||
```
|
||||
|
||||
### 新增的代碼
|
||||
|
||||
```python
|
||||
✅ 泳道高度計算邏輯(20行)
|
||||
```
|
||||
|
||||
**代碼行數減少**: ~380 行 → ~20 行
|
||||
**邏輯複雜度降低**: 複雜 → 簡單
|
||||
**可靠性提升**: 不保證 → **100% 保證**
|
||||
|
||||
---
|
||||
|
||||
## 性能分析
|
||||
|
||||
| 項目 | v5.x | v6.0 |
|
||||
|------|------|------|
|
||||
| 時間複雜度 | O(n² × 候選數) | O(1) |
|
||||
| 空間複雜度 | O(n) | O(1) |
|
||||
| 每個事件計算 | 20-30次碰撞檢測 | 1次直接計算 |
|
||||
| 10個事件 | ~2000次計算 | 10次計算 |
|
||||
| 100個事件 | ~200000次計算 | 100次計算 |
|
||||
|
||||
**性能提升**: ~2000倍(對於100個事件)
|
||||
|
||||
---
|
||||
|
||||
## 優勢總結
|
||||
|
||||
### 1. **簡單**
|
||||
- 邏輯清晰易懂
|
||||
- 沒有複雜的碰撞檢測
|
||||
- 代碼量少,易維護
|
||||
|
||||
### 2. **可靠**
|
||||
- 100% 保證不交錯
|
||||
- 沒有邊界情況
|
||||
- 沒有bug風險
|
||||
|
||||
### 3. **高性能**
|
||||
- O(1) 時間複雜度
|
||||
- 沒有昂貴的碰撞檢測
|
||||
- 即使千個事件也瞬間完成
|
||||
|
||||
### 4. **可預測**
|
||||
- 每個層級有固定高度
|
||||
- 視覺上規律、整齊
|
||||
- 用戶可以預期線條位置
|
||||
|
||||
---
|
||||
|
||||
## 可調整參數
|
||||
|
||||
### 調整高度範圍
|
||||
|
||||
```python
|
||||
# renderer_timeline.py 第 429-438 行
|
||||
|
||||
# 當前:20%-95% (75% 範圍)
|
||||
if is_upper:
|
||||
lane_ratio = 0.20 + (lane_index / (total_layers - 1)) * 0.75
|
||||
|
||||
# 可調整為更大範圍:15%-98% (83% 範圍)
|
||||
if is_upper:
|
||||
lane_ratio = 0.15 + (lane_index / (total_layers - 1)) * 0.83
|
||||
|
||||
# 或更小範圍:25%-90% (65% 範圍)
|
||||
if is_upper:
|
||||
lane_ratio = 0.25 + (lane_index / (total_layers - 1)) * 0.65
|
||||
```
|
||||
|
||||
### 調整下方分布方向
|
||||
|
||||
```python
|
||||
# 當前:下方反向分布(95%→20%)
|
||||
if not is_upper:
|
||||
lane_ratio = 0.95 - (lane_index / (total_layers - 1)) * 0.75
|
||||
|
||||
# 可改為同向分布(20%→95%)- 但可能在中間交匯
|
||||
if not is_upper:
|
||||
lane_ratio = 0.20 + (lane_index / (total_layers - 1)) * 0.75
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 設計哲學
|
||||
|
||||
### "Less is More"
|
||||
|
||||
**v1-v5**: 不斷增加複雜度
|
||||
- v1: 簡單分層
|
||||
- v2: 2D避碰
|
||||
- v3: 平滑曲線
|
||||
- v4: 智能路徑
|
||||
- v5: 碰撞檢測
|
||||
|
||||
**結果**: 越來越複雜,但問題仍存在
|
||||
|
||||
**v6**: 回歸本質
|
||||
- 核心問題:線條交錯
|
||||
- 根本原因:高度不確定
|
||||
- 最簡解法:**固定高度分配**
|
||||
|
||||
**結果**: 更簡單,但100%可靠
|
||||
|
||||
---
|
||||
|
||||
## 類比
|
||||
|
||||
### 游泳池泳道
|
||||
|
||||
想像一個游泳池有5條泳道:
|
||||
|
||||
```
|
||||
泳道5 ════════════════════ (95%)
|
||||
泳道4 ════════════════════ (76%)
|
||||
泳道3 ════════════════════ (58%)
|
||||
泳道2 ════════════════════ (39%)
|
||||
泳道1 ════════════════════ (20%)
|
||||
```
|
||||
|
||||
**規則**:
|
||||
- 每個游泳者被分配到固定的泳道
|
||||
- 同一泳道可以有多個游泳者(前後排列)
|
||||
- **游泳者永遠不會跨泳道**
|
||||
|
||||
**效果**:
|
||||
- ✅ 絕對不會碰撞
|
||||
- ✅ 秩序井然
|
||||
- ✅ 易於管理
|
||||
|
||||
這正是我們的泳道分配法!
|
||||
|
||||
---
|
||||
|
||||
## 測試建議
|
||||
|
||||
請重新測試 demo 文件,應該能看到:
|
||||
|
||||
1. ✅ **所有線條清晰分層**
|
||||
2. ✅ **完全沒有交錯**
|
||||
3. ✅ **視覺整齊規律**
|
||||
4. ✅ **渲染速度更快**
|
||||
|
||||
如果仍有問題,可能原因:
|
||||
- 文字框過大遮擋線條(調整文字框大小)
|
||||
- 層級間距不足(調整 `layer_spacing`)
|
||||
- 不是線條交錯問題(可能是其他視覺問題)
|
||||
|
||||
---
|
||||
|
||||
**版本**: v6.0 - **泳道分配法**
|
||||
**更新日期**: 2025-11-05
|
||||
**作者**: Claude AI
|
||||
|
||||
## 核心理念
|
||||
|
||||
> "最好的解決方案往往是最簡單的"
|
||||
> "保證 > 優化"
|
||||
> "100% 可靠 > 複雜但不可靠"
|
||||
|
||||
**從碰撞檢測到泳道分配,這是一次質的飛躍!** 🚀
|
||||
@@ -1,373 +0,0 @@
|
||||
# 時間軸標籤避碰改進(v7.0) - Shape.path 渲染法
|
||||
|
||||
## 核心轉變
|
||||
|
||||
### 從 Scatter 線條到 Shape 路徑
|
||||
|
||||
**v6.0 的問題**:
|
||||
- ⚠️ 使用 scatter (mode='lines') 繪製連接線
|
||||
- ⚠️ 線條可能遮擋事件點和文字框
|
||||
- ⚠️ Z-index 控制不夠精確
|
||||
- ⚠️ hover 事件可能被線條攔截
|
||||
|
||||
**v7.0 的解決方案 - Shape.path 渲染法**:
|
||||
- ✅ **使用 shape.path 繪製多段 L 形路徑**
|
||||
- ✅ **設定 layer='below' 確保線條在底層**
|
||||
- ✅ **opacity=0.7 半透明,不干擾閱讀**
|
||||
- ✅ **完全避免線條遮擋重要元素**
|
||||
|
||||
---
|
||||
|
||||
## 技術實現
|
||||
|
||||
### Shape Line Segments(分段繪製)
|
||||
|
||||
由於 Plotly 的 `shape.path` 不支持 datetime 座標,改用 `type='line'` 分段繪製:
|
||||
|
||||
```python
|
||||
# 將每一段連線分別繪製為獨立的 shape
|
||||
for i in range(len(line_x_points) - 1):
|
||||
shapes.append({
|
||||
'type': 'line',
|
||||
'x0': line_x_points[i],
|
||||
'y0': line_y_points[i],
|
||||
'x1': line_x_points[i + 1],
|
||||
'y1': line_y_points[i + 1],
|
||||
'xref': 'x', # 座標參考系統
|
||||
'yref': 'y',
|
||||
'line': {
|
||||
'color': marker['color'],
|
||||
'width': 1.5,
|
||||
},
|
||||
'layer': 'below', # 關鍵設定:置於底層
|
||||
'opacity': 0.7, # 半透明效果
|
||||
})
|
||||
```
|
||||
|
||||
**範例**:
|
||||
- L 形連接(4 點)→ 3 個 line segments
|
||||
- 直線連接(2 點)→ 1 個 line segment
|
||||
- 迴圈自動處理不同長度
|
||||
|
||||
### 與 v6.0 對比
|
||||
|
||||
**v6.0(Scatter 方式)**:
|
||||
```python
|
||||
data.append({
|
||||
'type': 'scatter',
|
||||
'x': line_x_points,
|
||||
'y': line_y_points,
|
||||
'mode': 'lines',
|
||||
'line': {
|
||||
'color': marker['color'],
|
||||
'width': 1.5,
|
||||
},
|
||||
'showlegend': False,
|
||||
'hoverinfo': 'skip'
|
||||
})
|
||||
```
|
||||
|
||||
**v7.0(Shape Line Segments 方式)**:
|
||||
```python
|
||||
# 分段繪製,支持 datetime 座標
|
||||
for i in range(len(line_x_points) - 1):
|
||||
shapes.append({
|
||||
'type': 'line',
|
||||
'x0': line_x_points[i],
|
||||
'y0': line_y_points[i],
|
||||
'x1': line_x_points[i + 1],
|
||||
'y1': line_y_points[i + 1],
|
||||
'xref': 'x',
|
||||
'yref': 'y',
|
||||
'line': {
|
||||
'color': marker['color'],
|
||||
'width': 1.5,
|
||||
},
|
||||
'layer': 'below', # 線條置於底層
|
||||
'opacity': 0.7, # 半透明
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 視覺層級
|
||||
|
||||
### Z-index 分層(從底到頂)
|
||||
|
||||
```
|
||||
┌────────────────────────────────┐
|
||||
│ Layer 4: Annotations (文字框) │ 最頂層,確保可讀
|
||||
│ Layer 3: Scatter Points (事件點)│ 事件點清晰可見
|
||||
│ Layer 2: Axis Line (時間軸) │ 時間軸明確
|
||||
│ Layer 1: Shapes (連接線) │ 底層,不遮擋
|
||||
└────────────────────────────────┘
|
||||
```
|
||||
|
||||
**保證**:
|
||||
- 🔒 連接線永遠在底層(layer='below')
|
||||
- 🔒 事件點永遠可見可點擊
|
||||
- 🔒 文字框永遠清晰可讀
|
||||
- 🔒 hover 事件不會被線條攔截
|
||||
|
||||
---
|
||||
|
||||
## 優勢總結
|
||||
|
||||
### 1. **視覺清晰**
|
||||
- 線條不會遮擋事件點
|
||||
- 文字框始終在最上層
|
||||
- 半透明效果減少視覺干擾
|
||||
|
||||
### 2. **交互友好**
|
||||
- hover 事件正確觸發在事件點和文字框
|
||||
- 線條不攔截滑鼠事件
|
||||
- 用戶體驗更流暢
|
||||
|
||||
### 3. **技術優雅**
|
||||
- 使用 Plotly 標準的 shape 系統
|
||||
- 明確的 layer 控制
|
||||
- SVG path 語法靈活高效
|
||||
|
||||
### 4. **與 v6.0 完全兼容**
|
||||
- 保留泳道分配法的所有優點
|
||||
- 僅改變渲染方式,不改變邏輯
|
||||
- 100% 向後兼容
|
||||
|
||||
---
|
||||
|
||||
## 代碼位置
|
||||
|
||||
### 修改的文件
|
||||
|
||||
**`backend/renderer_timeline.py`**
|
||||
|
||||
#### 水平時間軸(第 372-389 行)
|
||||
```python
|
||||
# 使用 shape line 繪製連接線(分段),設定 layer='below' 避免遮擋
|
||||
for i in range(len(line_x_points) - 1):
|
||||
shapes.append({
|
||||
'type': 'line',
|
||||
'x0': line_x_points[i],
|
||||
'y0': line_y_points[i],
|
||||
'x1': line_x_points[i + 1],
|
||||
'y1': line_y_points[i + 1],
|
||||
'xref': 'x',
|
||||
'yref': 'y',
|
||||
'line': {
|
||||
'color': marker['color'],
|
||||
'width': 1.5,
|
||||
},
|
||||
'layer': 'below', # 線條置於底層
|
||||
'opacity': 0.7, # 半透明
|
||||
})
|
||||
```
|
||||
|
||||
#### 垂直時間軸(第 635-652 行)
|
||||
- 相同的實現邏輯(迴圈繪製線段)
|
||||
- 適配垂直時間軸的座標系統
|
||||
|
||||
---
|
||||
|
||||
## 測試方法
|
||||
|
||||
### 1. 啟動應用
|
||||
```bash
|
||||
conda activate timeline_designer
|
||||
python app.py
|
||||
```
|
||||
|
||||
### 2. 訪問界面
|
||||
- 瀏覽器:http://localhost:8000
|
||||
- 或使用 PyWebview GUI 視窗
|
||||
|
||||
### 3. 測試示範檔案
|
||||
- `demo_project_timeline.csv` - 15 個事件
|
||||
- `demo_life_events.csv` - 11 個事件
|
||||
- `demo_product_roadmap.csv` - 14 個事件
|
||||
|
||||
### 4. 驗證重點
|
||||
- ✅ 連接線是否在底層(不遮擋事件點和文字框)
|
||||
- ✅ 事件點 hover 是否正常觸發
|
||||
- ✅ 文字框是否清晰可見
|
||||
- ✅ 線條是否有半透明效果
|
||||
- ✅ 視覺是否整潔專業
|
||||
|
||||
---
|
||||
|
||||
## 與其他版本對比
|
||||
|
||||
| 版本 | 連接線方式 | 視覺遮擋 | hover 問題 | 複雜度 | 效果 |
|
||||
|------|-----------|---------|-----------|--------|------|
|
||||
| v5.0 | scatter + 碰撞檢測 | ⚠️ 可能遮擋 | ⚠️ 可能攔截 | 高 | 中等 |
|
||||
| v6.0 | scatter + 泳道分配 | ⚠️ 可能遮擋 | ⚠️ 可能攔截 | 低 | 良好 |
|
||||
| **v7.0** | **shape.path + layer='below'** | **✅ 無遮擋** | **✅ 無攔截** | **低** | **優秀** |
|
||||
|
||||
---
|
||||
|
||||
## 可調整參數
|
||||
|
||||
### 線條透明度
|
||||
```python
|
||||
# renderer_timeline.py 第 382 行和第 639 行
|
||||
'opacity': 0.7, # 預設 0.7,可調整為 0.5-1.0
|
||||
```
|
||||
|
||||
### 線條寬度
|
||||
```python
|
||||
# renderer_timeline.py 第 378 行和第 635 行
|
||||
'width': 1.5, # 預設 1.5,可調整為 1.0-3.0
|
||||
```
|
||||
|
||||
### 線條樣式
|
||||
```python
|
||||
'line': {
|
||||
'color': marker['color'],
|
||||
'width': 1.5,
|
||||
'dash': 'dot', # 可選:'solid', 'dot', 'dash', 'dashdot'
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 未來可能改進
|
||||
|
||||
### 1. **同日多卡片左右交錯**
|
||||
- 同一天的卡片交錯使用左/右側邊當錨點
|
||||
- 水平段自然平行不打架
|
||||
- 需要在標籤定位邏輯中實現
|
||||
|
||||
### 2. **貝茲曲線平滑**
|
||||
- 使用 SVG 的 C (Cubic Bezier) 命令
|
||||
- 更自然的曲線效果
|
||||
- 視覺更柔和
|
||||
|
||||
```python
|
||||
# 範例:貝茲曲線路徑
|
||||
path_str = f"M {x0},{y0} C {cx1},{cy1} {cx2},{cy2} {x1},{y1}"
|
||||
```
|
||||
|
||||
### 3. **動態線條顏色**
|
||||
- 根據事件重要性調整透明度
|
||||
- 高優先級事件使用更鮮明的線條
|
||||
- 低優先級事件線條更淡
|
||||
|
||||
---
|
||||
|
||||
## 錯誤修復記錄
|
||||
|
||||
### Bug Fix #2: Shape.path 不支持 datetime 座標
|
||||
|
||||
**問題描述**:
|
||||
- Plotly 的 `shape.path` 不直接支持 datetime 座標軸
|
||||
- 使用 path 命令(M, L)時,datetime 對象無法正確解析
|
||||
- 導致連接線完全不顯示
|
||||
|
||||
**修復方案**:
|
||||
改用 `type='line'` 分段繪製,每一段連線作為獨立的 shape:
|
||||
|
||||
```python
|
||||
# 修復前:使用 path(不支持 datetime)
|
||||
path_str = f"M {x0},{y0} L {x1},{y1} L {x2},{y2} L {x3},{y3}"
|
||||
shapes.append({
|
||||
'type': 'path',
|
||||
'path': path_str,
|
||||
...
|
||||
})
|
||||
|
||||
# 修復後:使用多個 line segment(支持 datetime)
|
||||
for i in range(len(line_x_points) - 1):
|
||||
shapes.append({
|
||||
'type': 'line',
|
||||
'x0': line_x_points[i],
|
||||
'y0': line_y_points[i],
|
||||
'x1': line_x_points[i + 1],
|
||||
'y1': line_y_points[i + 1],
|
||||
'xref': 'x', # 明確指定座標參考系統
|
||||
'yref': 'y',
|
||||
'line': {'color': marker['color'], 'width': 1.5},
|
||||
'layer': 'below',
|
||||
'opacity': 0.7,
|
||||
})
|
||||
```
|
||||
|
||||
**技術細節**:
|
||||
- L 形連接線需要 3 個線段:垂直 → 水平 → 垂直(或水平 → 垂直 → 水平)
|
||||
- 直線連接只需要 1 個線段
|
||||
- 使用迴圈自動處理不同長度的點列表
|
||||
|
||||
**影響範圍**:
|
||||
- 水平時間軸(`renderer_timeline.py` 第 372-389 行)
|
||||
- 垂直時間軸(`renderer_timeline.py` 第 635-652 行)
|
||||
|
||||
**優勢**:
|
||||
- ✅ 完全支持 datetime 座標
|
||||
- ✅ 保持 `layer='below'` 的優點
|
||||
- ✅ 視覺效果與 path 完全相同
|
||||
- ✅ 代碼更簡潔(迴圈處理)
|
||||
|
||||
---
|
||||
|
||||
### Bug Fix #1: 處理直線連接的索引錯誤
|
||||
|
||||
**問題描述**:
|
||||
- 當標籤正好在事件點正上方/正側方時,使用直線連接(2 個點)
|
||||
- 但 path_str 構建時嘗試訪問 4 個點的索引 [0] 到 [3]
|
||||
- 導致 `list index out of range` 錯誤
|
||||
|
||||
**修復方案**:
|
||||
```python
|
||||
# 修復前:總是嘗試訪問 4 個索引
|
||||
path_str = f"M {line_x_points[0]},{line_y_points[0]} L {line_x_points[1]},{line_y_points[1]} L {line_x_points[2]},{line_y_points[2]} L {line_x_points[3]},{line_y_points[3]}"
|
||||
|
||||
# 修復後:根據情況構建不同的 path
|
||||
if is_directly_above: # 或 is_directly_sideways (垂直時間軸)
|
||||
# 直線路徑(2 個點)
|
||||
path_str = f"M {line_x_points[0]},{line_y_points[0]} L {line_x_points[1]},{line_y_points[1]}"
|
||||
else:
|
||||
# L 形路徑(4 個點)
|
||||
path_str = f"M {line_x_points[0]},{line_y_points[0]} L {line_x_points[1]},{line_y_points[1]} L {line_x_points[2]},{line_y_points[2]} L {line_x_points[3]},{line_y_points[3]}"
|
||||
```
|
||||
|
||||
**影響範圍**:
|
||||
- 水平時間軸(`renderer_timeline.py` 第 373-378 行)
|
||||
- 垂直時間軸(`renderer_timeline.py` 第 636-641 行)
|
||||
|
||||
**測試驗證**:
|
||||
- ✅ 後端服務正常啟動
|
||||
- ✅ health check 通過
|
||||
- ✅ 可以正常渲染時間軸
|
||||
|
||||
---
|
||||
|
||||
**版本**: v7.0 - **Shape Line Segments 渲染法**
|
||||
**更新日期**: 2025-11-05 (包含 2 個 Bug Fix)
|
||||
**作者**: Claude AI
|
||||
|
||||
## 核心理念
|
||||
|
||||
> "正確的工具做正確的事"
|
||||
> "Shape line segments for datetime compatibility"
|
||||
> "Layer control is visual clarity"
|
||||
|
||||
**從數據可視化到圖形設計,這是渲染方式的優雅轉變!** 🎨
|
||||
|
||||
---
|
||||
|
||||
## 總結
|
||||
|
||||
v7.0 成功將連接線從 scatter 轉換為 shape line segments 渲染:
|
||||
|
||||
✅ **問題解決**:
|
||||
- 線條不再遮擋事件點和文字框
|
||||
- 完美支持 datetime 座標軸
|
||||
- hover 事件正確觸發
|
||||
|
||||
✅ **技術優勢**:
|
||||
- 使用 `layer='below'` 明確控制 z-index
|
||||
- 分段繪製支持任意複雜路徑
|
||||
- 代碼簡潔(迴圈處理)
|
||||
|
||||
✅ **完全兼容**:
|
||||
- 保留 v6.0 泳道分配法的所有優點
|
||||
- 100% 保證線條不交錯
|
||||
- 視覺整潔專業
|
||||
@@ -1,454 +0,0 @@
|
||||
# 時間軸標籤避碰改進(v8.0) - 力導向演算法
|
||||
|
||||
## 核心轉變
|
||||
|
||||
### 從固定泳道到智能動態優化
|
||||
|
||||
**v7.0 的問題**:
|
||||
- ⚠️ 泳道分配雖保證垂直分離,但水平方向仍可能擁擠
|
||||
- ⚠️ 多條線在同一時間區域經過時視覺混亂
|
||||
- ⚠️ 文字框背景遮擋連接線(95% 不透明)
|
||||
- ⚠️ 無法動態調整以達到最佳布局
|
||||
|
||||
**v8.0 的解決方案 - 力導向演算法**:
|
||||
- ✅ **使用物理模擬優化標籤位置**
|
||||
- ✅ **排斥力:標籤之間互相推開**
|
||||
- ✅ **吸引力:標籤被拉向事件點**
|
||||
- ✅ **迭代收斂:自動達到平衡狀態**
|
||||
- ✅ **降低文字框不透明度(85%)**
|
||||
|
||||
---
|
||||
|
||||
## 技術實現
|
||||
|
||||
### 力導向演算法原理
|
||||
|
||||
**核心概念**:
|
||||
- 將標籤視為物理粒子
|
||||
- 標籤之間存在排斥力(避免重疊)
|
||||
- 標籤與事件點之間存在吸引力(彈簧連接)
|
||||
- 通過多次迭代達到能量最低的平衡狀態
|
||||
|
||||
**數學模型**:
|
||||
|
||||
```python
|
||||
# 1. 排斥力(標籤之間)
|
||||
repulsion = repulsion_strength / (distance^2)
|
||||
force_x = (dx / distance) * repulsion
|
||||
force_y = (dy / distance) * repulsion
|
||||
|
||||
# 2. 吸引力(標籤與事件點之間)
|
||||
attraction_x = (event_x - label_x) * attraction_strength
|
||||
attraction_y = (event_y - label_y) * attraction_strength
|
||||
|
||||
# 3. 速度更新(帶阻尼)
|
||||
velocity = (velocity + force) * damping
|
||||
|
||||
# 4. 位置更新
|
||||
position += velocity
|
||||
```
|
||||
|
||||
### 算法參數
|
||||
|
||||
```python
|
||||
max_iterations = 100 # 最大迭代次數
|
||||
repulsion_strength = 100.0 # 排斥力強度
|
||||
attraction_strength = 0.05 # 吸引力強度(彈簧係數)
|
||||
damping = 0.7 # 阻尼係數(0-1,越小減速越快)
|
||||
```
|
||||
|
||||
**參數說明**:
|
||||
- **repulsion_strength**: 控制標籤之間的最小距離,值越大標籤越分散
|
||||
- **attraction_strength**: 控制標籤與事件點的連接強度,值越大標籤越靠近事件點
|
||||
- **damping**: 防止系統震盪,幫助快速收斂
|
||||
|
||||
---
|
||||
|
||||
## 算法流程
|
||||
|
||||
### 步驟詳解
|
||||
|
||||
```python
|
||||
def apply_force_directed_layout(label_positions, config):
|
||||
# 1. 初始化
|
||||
velocities = [{'x': 0, 'y': 0} for _ in label_positions]
|
||||
|
||||
# 2. 迭代優化
|
||||
for iteration in range(max_iterations):
|
||||
forces = [{'x': 0, 'y': 0} for _ in label_positions]
|
||||
|
||||
# 3. 計算排斥力(所有標籤對)
|
||||
for i in range(len(positions)):
|
||||
for j in range(i + 1, len(positions)):
|
||||
distance = sqrt(dx^2 + dy^2)
|
||||
repulsion = repulsion_strength / (distance^2)
|
||||
# 應用牛頓第三定律(作用力與反作用力)
|
||||
forces[i] -= repulsion
|
||||
forces[j] += repulsion
|
||||
|
||||
# 4. 計算吸引力(標籤→事件點)
|
||||
for i in range(len(positions)):
|
||||
attraction = (event_pos - label_pos) * attraction_strength
|
||||
forces[i] += attraction
|
||||
|
||||
# 5. 更新速度和位置
|
||||
for i in range(len(positions)):
|
||||
velocities[i] = (velocities[i] + forces[i]) * damping
|
||||
positions[i] += velocities[i]
|
||||
|
||||
# 限制 y 方向範圍(保持上下分離)
|
||||
if positions[i].y > 0:
|
||||
positions[i].y = max(0.5, min(positions[i].y, 10.0))
|
||||
else:
|
||||
positions[i].y = min(-0.5, max(positions[i].y, -10.0))
|
||||
|
||||
# 6. 檢查收斂
|
||||
if max_displacement < 0.01:
|
||||
break
|
||||
|
||||
return optimized_positions
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 視覺效果
|
||||
|
||||
### 力導向優化前後對比
|
||||
|
||||
**優化前(v7.0 泳道分配)**:
|
||||
```
|
||||
┌────┐ ┌────┐ ┌────┐
|
||||
│ L1 │ │ L2 │ │ L3 │ ← 可能過於擁擠
|
||||
└────┘ └────┘ └────┘
|
||||
│ │ │
|
||||
│ │ │ ← 線條可能重疊
|
||||
────┼─────────┼─────────┼────
|
||||
● ● ●
|
||||
```
|
||||
|
||||
**優化後(v8.0 力導向)**:
|
||||
```
|
||||
┌────┐ ┌────┐
|
||||
│ L1 │ │ L3 │ ← 自動分散
|
||||
└────┘ └────┘
|
||||
│ ┌────┐ │
|
||||
│ │ L2 │ │ ← 動態調整位置
|
||||
│ └────┘ │
|
||||
│ │ │ ← 線條自然分離
|
||||
────┼────────────┼──────┼────
|
||||
● ● ●
|
||||
```
|
||||
|
||||
### 力的作用示意
|
||||
|
||||
```
|
||||
排斥力 (標籤之間):
|
||||
┌────┐ ←→ ┌────┐
|
||||
│ L1 │ 推開 │ L2 │
|
||||
└────┘ └────┘
|
||||
|
||||
吸引力 (標籤與事件點):
|
||||
┌────┐
|
||||
│ L1 │
|
||||
└──↓─┘ 彈簧拉力
|
||||
● 事件點
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 關鍵改進
|
||||
|
||||
### 1. 修復文字框遮擋問題
|
||||
|
||||
**問題**:
|
||||
- 文字框使用 `rgba(255, 255, 255, 0.95)` 背景
|
||||
- 95% 不透明會完全遮擋底層連接線
|
||||
|
||||
**解決**:
|
||||
```python
|
||||
# 修改前
|
||||
'bgcolor': 'rgba(255, 255, 255, 0.95)'
|
||||
|
||||
# 修改後
|
||||
'bgcolor': 'rgba(255, 255, 255, 0.85)' # 降低到 85%
|
||||
```
|
||||
|
||||
### 2. 實現力導向布局
|
||||
|
||||
**架構**:
|
||||
- 獨立函數 `apply_force_directed_layout()` (第 23-153 行)
|
||||
- 在生成 markers 後、繪製前調用
|
||||
- 支持水平和垂直時間軸
|
||||
|
||||
**調用位置**:
|
||||
```python
|
||||
# 水平時間軸(第 432-441 行)
|
||||
if config.enable_zoom: # 使用 enable_zoom 作為開關
|
||||
markers = apply_force_directed_layout(markers, config, ...)
|
||||
|
||||
# 垂直時間軸(第 693-702 行)
|
||||
if config.enable_zoom:
|
||||
markers = apply_force_directed_layout(markers, config, ...)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 性能分析
|
||||
|
||||
### 時間複雜度
|
||||
|
||||
| 操作 | 複雜度 | 說明 |
|
||||
|------|--------|------|
|
||||
| 排斥力計算 | O(n²) | 每對標籤都要計算 |
|
||||
| 吸引力計算 | O(n) | 每個標籤獨立計算 |
|
||||
| 位置更新 | O(n) | 每個標籤獨立更新 |
|
||||
| **總計(每次迭代)** | **O(n²)** | 主要瓶頸在排斥力 |
|
||||
| **總計(100次迭代)** | **O(100n²)** | 通常會提前收斂 |
|
||||
|
||||
### 實際性能
|
||||
|
||||
```
|
||||
事件數量:10 → 迭代時間:<0.01秒
|
||||
事件數量:50 → 迭代時間:<0.1秒
|
||||
事件數量:100 → 迭代時間:<0.5秒
|
||||
```
|
||||
|
||||
**優化空間**:
|
||||
- 可使用空間索引(Quadtree)將排斥力計算降到 O(n log n)
|
||||
- 可使用 Barnes-Hut 近似算法加速大規模場景
|
||||
- 通常在 20-50 次迭代後就會收斂
|
||||
|
||||
---
|
||||
|
||||
## 收斂檢測
|
||||
|
||||
```python
|
||||
# 計算每個標籤的位移
|
||||
displacement = sqrt((new_x - old_x)^2 + (new_y - old_y)^2)
|
||||
|
||||
# 檢查最大位移
|
||||
if max(displacements) < 0.01:
|
||||
logger.info(f"力導向演算法在第 {iteration + 1} 次迭代後收斂")
|
||||
break
|
||||
```
|
||||
|
||||
**典型收斂曲線**:
|
||||
```
|
||||
迭代次數 最大位移
|
||||
0 100.0
|
||||
10 50.2
|
||||
20 15.3
|
||||
30 3.1
|
||||
40 0.5
|
||||
50 0.08
|
||||
60 0.005 ← 收斂!
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 參數調整指南
|
||||
|
||||
### 如果標籤太分散(遠離事件點)
|
||||
|
||||
```python
|
||||
# 增加吸引力
|
||||
attraction_strength = 0.1 # 從 0.05 增加到 0.1
|
||||
|
||||
# 或減少排斥力
|
||||
repulsion_strength = 50.0 # 從 100.0 減少到 50.0
|
||||
```
|
||||
|
||||
### 如果標籤仍然重疊
|
||||
|
||||
```python
|
||||
# 增加排斥力
|
||||
repulsion_strength = 200.0 # 從 100.0 增加到 200.0
|
||||
|
||||
# 或增加迭代次數
|
||||
max_iterations = 200 # 從 100 增加到 200
|
||||
```
|
||||
|
||||
### 如果系統震盪不穩定
|
||||
|
||||
```python
|
||||
# 增加阻尼(更快減速)
|
||||
damping = 0.5 # 從 0.7 減少到 0.5
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 與其他版本對比
|
||||
|
||||
| 版本 | 方法 | 連接線重疊 | 文字框遮擋 | 性能 | 適應性 |
|
||||
|------|------|-----------|-----------|------|--------|
|
||||
| v6.0 | 泳道分配 | ⚠️ 可能 | ❌ 嚴重 | ⚡ 極快 O(n) | ❌ 固定 |
|
||||
| v7.0 | Shape分段渲染 | ⚠️ 可能 | ⚠️ 仍有 | ⚡ 極快 O(n) | ❌ 固定 |
|
||||
| **v8.0** | **力導向優化** | **✅ 極少** | **✅ 改善** | **⚡ 中等 O(n²)** | **✅ 動態** |
|
||||
|
||||
---
|
||||
|
||||
## 啟用方式
|
||||
|
||||
**當前實現**(臨時):
|
||||
- 使用 `config.enable_zoom` 作為力導向演算法的開關
|
||||
- 啟用縮放功能時自動應用力導向優化
|
||||
|
||||
**未來改進**:
|
||||
- 添加專用配置項 `config.enable_force_directed`
|
||||
- 允許用戶自定義力的參數
|
||||
|
||||
```python
|
||||
# 未來配置範例
|
||||
config = TimelineConfig(
|
||||
enable_force_directed=True,
|
||||
force_directed_params={
|
||||
'max_iterations': 100,
|
||||
'repulsion_strength': 100.0,
|
||||
'attraction_strength': 0.05,
|
||||
'damping': 0.7
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 代碼位置
|
||||
|
||||
### 新增函數
|
||||
|
||||
**`backend/renderer_timeline.py`** (第 23-153 行)
|
||||
|
||||
```python
|
||||
def apply_force_directed_layout(
|
||||
label_positions: List[Dict],
|
||||
config: 'TimelineConfig',
|
||||
max_iterations: int = 100,
|
||||
repulsion_strength: float = 100.0,
|
||||
attraction_strength: float = 0.05,
|
||||
damping: float = 0.7
|
||||
) -> List[Dict]:
|
||||
"""
|
||||
使用力導向演算法優化標籤位置
|
||||
|
||||
模擬物理系統:
|
||||
- 標籤之間:排斥力(F = k / d²)
|
||||
- 標籤與事件點:吸引力(F = k * d)
|
||||
- 速度阻尼:防止震盪
|
||||
"""
|
||||
# ... 詳見代碼 ...
|
||||
```
|
||||
|
||||
### 調用位置
|
||||
|
||||
**水平時間軸** (第 432-441 行):
|
||||
```python
|
||||
if config.enable_zoom:
|
||||
markers = apply_force_directed_layout(
|
||||
markers, config,
|
||||
max_iterations=100,
|
||||
repulsion_strength=100.0,
|
||||
attraction_strength=0.05,
|
||||
damping=0.7
|
||||
)
|
||||
```
|
||||
|
||||
**垂直時間軸** (第 693-702 行):
|
||||
```python
|
||||
if config.enable_zoom:
|
||||
markers = apply_force_directed_layout(
|
||||
markers, config,
|
||||
max_iterations=100,
|
||||
repulsion_strength=100.0,
|
||||
attraction_strength=0.05,
|
||||
damping=0.7
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 測試方法
|
||||
|
||||
### 1. 啟動應用
|
||||
```bash
|
||||
conda activate timeline_designer
|
||||
python app.py
|
||||
```
|
||||
|
||||
### 2. 訪問界面
|
||||
- GUI 視窗會自動開啟
|
||||
- 或訪問 http://localhost:8000
|
||||
|
||||
### 3. 測試示範檔案
|
||||
載入以下檔案並觀察效果:
|
||||
- `demo_project_timeline.csv` - 15 個事件
|
||||
- `demo_life_events.csv` - 11 個事件
|
||||
- `demo_product_roadmap.csv` - 14 個事件
|
||||
|
||||
### 4. 驗證重點
|
||||
- ✅ 標籤是否自動分散(不擁擠)
|
||||
- ✅ 連接線是否不再重疊
|
||||
- ✅ 文字框背景是否不完全遮擋線條
|
||||
- ✅ 標籤是否保持靠近事件點
|
||||
- ✅ 渲染速度是否可接受(< 1秒)
|
||||
|
||||
### 5. 查看日誌
|
||||
```
|
||||
力導向演算法在第 XX 次迭代後收斂
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 未來改進方向
|
||||
|
||||
### 1. **Barnes-Hut 近似算法**
|
||||
- 使用 Quadtree 空間劃分
|
||||
- 將遠距離標籤群視為單一質點
|
||||
- 降低複雜度到 O(n log n)
|
||||
|
||||
### 2. **考慮文字框尺寸**
|
||||
- 當前只考慮標籤中心點
|
||||
- 應考慮文字框的實際寬度和高度
|
||||
- 使用 OBB(有向包圍盒)碰撞檢測
|
||||
|
||||
### 3. **分層力導向**
|
||||
- 先在層級內部優化
|
||||
- 再在層級之間優化
|
||||
- 減少計算量並保持層級結構
|
||||
|
||||
### 4. **動畫過渡**
|
||||
- 記錄每次迭代的位置
|
||||
- 在前端播放優化過程動畫
|
||||
- 提供更好的視覺反饋
|
||||
|
||||
---
|
||||
|
||||
**版本**: v8.0 - **力導向演算法**
|
||||
**更新日期**: 2025-11-05
|
||||
**作者**: Claude AI
|
||||
|
||||
## 核心理念
|
||||
|
||||
> "讓物理定律解決佈局問題"
|
||||
> "力導向演算法:優雅、自然、有效"
|
||||
> "從啟發式規則到物理模擬"
|
||||
|
||||
## 總結
|
||||
|
||||
v8.0 成功整合力導向演算法,實現智能標籤佈局優化:
|
||||
|
||||
✅ **問題解決**:
|
||||
- 標籤自動分散,避免擁擠
|
||||
- 連接線重疊大幅減少
|
||||
- 文字框不再完全遮擋線條
|
||||
|
||||
✅ **技術優勢**:
|
||||
- 使用成熟的物理模擬方法
|
||||
- 自動達到平衡狀態(收斂)
|
||||
- 可調整參數適應不同場景
|
||||
|
||||
✅ **兼容性**:
|
||||
- 保留 v6.0 泳道分配的優點
|
||||
- 保留 v7.0 shape 分段渲染
|
||||
- 添加動態優化層
|
||||
|
||||
**從固定規則到自適應優化,這是布局算法的質的飛躍!** 🚀
|
||||
@@ -1,369 +0,0 @@
|
||||
# 時間軸標籤避碰改進(v9.0) - 固定5泳道 + 貪婪避讓算法
|
||||
|
||||
## 核心轉變
|
||||
|
||||
### 從動態層級到固定5泳道 + 智能分配
|
||||
|
||||
**v8.0 的問題**:
|
||||
- ❌ D3 Force 雖然避碰好,但實際效果不理想
|
||||
- ❌ 標籤移動幅度大,視覺混亂
|
||||
- ❌ 邊緣截斷問題難以完全解決
|
||||
|
||||
**v9.0 的解決方案 - 回歸 Plotly + 智能優化**:
|
||||
- ✅ **固定 5 個泳道**(上方 3 個 + 下方 2 個)
|
||||
- ✅ **貪婪算法選擇最佳泳道**
|
||||
- ✅ **考慮連接線遮擋**
|
||||
- ✅ **考慮文字框重疊**
|
||||
|
||||
---
|
||||
|
||||
## 技術實現
|
||||
|
||||
### 1. 固定 5 泳道配置
|
||||
|
||||
```python
|
||||
# 固定 5 個泳道
|
||||
SWIM_LANES = [
|
||||
{'index': 0, 'side': 'upper', 'ratio': 0.25}, # 上方泳道 1(最低)
|
||||
{'index': 1, 'side': 'upper', 'ratio': 0.55}, # 上方泳道 2(中)
|
||||
{'index': 2, 'side': 'upper', 'ratio': 0.85}, # 上方泳道 3(最高)
|
||||
{'index': 3, 'side': 'lower', 'ratio': 0.25}, # 下方泳道 1(最低)
|
||||
{'index': 4, 'side': 'lower', 'ratio': 0.55}, # 下方泳道 2(最高)
|
||||
]
|
||||
```
|
||||
|
||||
### 2. 貪婪算法選擇泳道
|
||||
|
||||
```python
|
||||
def greedy_lane_assignment(event, occupied_lanes):
|
||||
"""
|
||||
為事件選擇最佳泳道
|
||||
|
||||
考慮因素:
|
||||
1. 文字框水平重疊
|
||||
2. 連接線垂直交叉
|
||||
3. 優先選擇碰撞最少的泳道
|
||||
"""
|
||||
best_lane = None
|
||||
min_conflicts = float('inf')
|
||||
|
||||
for lane_id in range(5):
|
||||
conflicts = calculate_conflicts(event, lane_id, occupied_lanes)
|
||||
if conflicts < min_conflicts:
|
||||
min_conflicts = conflicts
|
||||
best_lane = lane_id
|
||||
|
||||
return best_lane
|
||||
```
|
||||
|
||||
### 3. 衝突計算
|
||||
|
||||
```python
|
||||
def calculate_conflicts(event, lane_id, occupied_lanes):
|
||||
"""
|
||||
計算選擇特定泳道的衝突數量
|
||||
|
||||
Returns:
|
||||
conflict_score: 衝突分數(越低越好)
|
||||
"""
|
||||
score = 0
|
||||
|
||||
# 檢查文字框水平重疊
|
||||
for occupied in occupied_lanes[lane_id]:
|
||||
if text_boxes_overlap(event, occupied):
|
||||
score += 10 # 重疊權重高
|
||||
|
||||
# 檢查連接線交叉
|
||||
for other_lane_id in range(5):
|
||||
if other_lane_id == lane_id:
|
||||
continue
|
||||
for occupied in occupied_lanes[other_lane_id]:
|
||||
if connection_lines_cross(event, lane_id, occupied, other_lane_id):
|
||||
score += 1 # 交叉權重低
|
||||
|
||||
return score
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ 實施代碼(已完成 + L型避讓增強)
|
||||
|
||||
### 檔案:`backend/renderer_timeline.py`
|
||||
|
||||
#### 🆕 連接線避開文字框功能(v9.0 增強)
|
||||
|
||||
**核心思路**:在**貪婪算法選擇泳道時**就檢測連接線是否會穿過其他標籤,優先選擇不會穿過的泳道。
|
||||
|
||||
**新增方法**:`_check_line_intersects_textbox()` (第 460-513 行)
|
||||
```python
|
||||
def _check_line_intersects_textbox(self, line_x1, line_y1, line_x2, line_y2,
|
||||
textbox_center_x, textbox_center_y,
|
||||
textbox_width, textbox_height):
|
||||
"""檢測線段是否與文字框相交"""
|
||||
|
||||
# 檢查水平線段是否穿過文字框
|
||||
if abs(line_y1 - line_y2) < 0.01:
|
||||
if box_bottom <= line_y <= box_top:
|
||||
if not (line_x_max < box_left or line_x_min > box_right):
|
||||
return True
|
||||
|
||||
# 檢查垂直線段是否穿過文字框
|
||||
if abs(line_x1 - line_x2) < 0.01:
|
||||
if box_left <= line_x <= box_right:
|
||||
if not (line_y_max < box_bottom or line_y_min > box_top):
|
||||
return True
|
||||
|
||||
return False
|
||||
```
|
||||
|
||||
**增強的衝突分數計算**(第 345-458 行):
|
||||
在貪婪算法中增加"連接線穿過其他文字框"的檢測:
|
||||
|
||||
```python
|
||||
def _calculate_lane_conflicts(self, ...):
|
||||
# 1. 文字框水平重疊(高權重:10.0)
|
||||
for occupied in occupied_lanes[lane_idx]:
|
||||
if 重疊:
|
||||
score += 10.0 * overlap_ratio
|
||||
|
||||
# 2. 連接線穿過其他文字框(高權重:8.0)✨ 新增
|
||||
# 檢查連接線的三段路徑是否會穿過已有標籤
|
||||
for occupied in all_occupied_lanes:
|
||||
# 檢查垂直線段1
|
||||
if self._check_line_intersects_textbox(event_x, 0, event_x, mid_y, ...):
|
||||
score += 8.0
|
||||
|
||||
# 檢查水平線段
|
||||
if self._check_line_intersects_textbox(event_x, mid_y, label_x, mid_y, ...):
|
||||
score += 8.0
|
||||
|
||||
# 檢查垂直線段2
|
||||
if self._check_line_intersects_textbox(label_x, mid_y, label_x, label_y, ...):
|
||||
score += 8.0
|
||||
|
||||
# 3. 連接線交叉(低權重:1.0)
|
||||
if 不同側 and 時間重疊:
|
||||
score += 1.0
|
||||
```
|
||||
|
||||
**結果**:貪婪算法會自動選擇連接線不穿過其他標籤的泳道,大幅改善視覺清晰度。
|
||||
|
||||
#### 1. 新增 `_calculate_label_positions()` 方法(第 250-343 行)
|
||||
```python
|
||||
def _calculate_label_positions(self, events, start_date, end_date):
|
||||
"""v9.0 - 固定5泳道 + 貪婪避讓算法"""
|
||||
|
||||
# 固定 5 個泳道配置
|
||||
SWIM_LANES = [
|
||||
{'index': 0, 'side': 'upper', 'ratio': 0.25},
|
||||
{'index': 1, 'side': 'upper', 'ratio': 0.55},
|
||||
{'index': 2, 'side': 'upper', 'ratio': 0.85},
|
||||
{'index': 3, 'side': 'lower', 'ratio': 0.25},
|
||||
{'index': 4, 'side': 'lower', 'ratio': 0.55},
|
||||
]
|
||||
|
||||
# 追蹤每個泳道的佔用情況
|
||||
occupied_lanes = {i: [] for i in range(5)}
|
||||
|
||||
# 貪婪算法:按時間順序處理每個事件
|
||||
for event_idx, event in enumerate(events):
|
||||
# 計算標籤時間範圍
|
||||
label_start = event_seconds - label_width_seconds / 2 - safety_margin
|
||||
label_end = event_seconds + label_width_seconds / 2 + safety_margin
|
||||
|
||||
# 為該事件選擇最佳泳道
|
||||
best_lane = None
|
||||
min_conflicts = float('inf')
|
||||
|
||||
for lane_config in SWIM_LANES:
|
||||
conflict_score = self._calculate_lane_conflicts(...)
|
||||
if conflict_score < min_conflicts:
|
||||
min_conflicts = conflict_score
|
||||
best_lane = lane_config
|
||||
|
||||
# 記錄佔用情況並返回結果
|
||||
occupied_lanes[lane_idx].append({...})
|
||||
result.append({'swim_lane': lane_idx, ...})
|
||||
```
|
||||
|
||||
#### 2. 新增 `_calculate_lane_conflicts()` 方法(第 345-413 行)
|
||||
```python
|
||||
def _calculate_lane_conflicts(self, event_x, label_start, label_end,
|
||||
lane_idx, lane_config, occupied_lanes,
|
||||
total_seconds):
|
||||
"""計算將事件放置在特定泳道的衝突分數"""
|
||||
|
||||
score = 0.0
|
||||
|
||||
# 1. 檢查同泳道的文字框水平重疊(高權重:10.0)
|
||||
for occupied in occupied_lanes[lane_idx]:
|
||||
if not (label_end < occupied['start'] or label_start > occupied['end']):
|
||||
overlap_ratio = ...
|
||||
score += 10.0 * overlap_ratio
|
||||
|
||||
# 2. 檢查與其他泳道的連接線交叉(低權重:1.0)
|
||||
for other_lane_idx in range(5):
|
||||
for occupied in occupied_lanes[other_lane_idx]:
|
||||
if 時間範圍重疊 and 在不同側:
|
||||
score += 1.0 # 交叉權重低
|
||||
|
||||
return score
|
||||
```
|
||||
|
||||
#### 3. 更新 `_render_horizontal()` 方法
|
||||
- **第 463-483 行**:使用新的泳道數據結構
|
||||
```python
|
||||
label_positions = self._calculate_label_positions(events, start_date, end_date)
|
||||
|
||||
for i, event in enumerate(events):
|
||||
pos_info = label_positions[i]
|
||||
swim_lane = pos_info['swim_lane']
|
||||
swim_lane_config = pos_info['swim_lane_config']
|
||||
label_y = pos_info['label_y'] # 預先計算的 Y 座標
|
||||
```
|
||||
|
||||
- **第 499-509 行**:更新 marker 數據結構
|
||||
```python
|
||||
markers.append({
|
||||
'event_x': event_date,
|
||||
'label_x': label_x,
|
||||
'label_y': label_y, # v9.0 使用預先計算的 Y 座標
|
||||
'swim_lane': swim_lane,
|
||||
'swim_lane_config': swim_lane_config,
|
||||
...
|
||||
})
|
||||
```
|
||||
|
||||
- **第 559-591 行**:使用固定泳道 ratio 計算連接線
|
||||
```python
|
||||
lane_ratio = swim_lane_config['ratio']
|
||||
mid_y = label_y * lane_ratio
|
||||
```
|
||||
|
||||
- **第 630-634 行**:固定 Y 軸範圍
|
||||
```python
|
||||
y_range_max = 3.5 # 上方最高層 + 邊距
|
||||
y_range_min = -2.5 # 下方最低層 + 邊距
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧪 測試驗證
|
||||
|
||||
### 測試步驟
|
||||
|
||||
1. **啟動應用程式**
|
||||
```bash
|
||||
python app.py
|
||||
```
|
||||
|
||||
2. **導入測試資料**
|
||||
- 使用 `demo_project_timeline.csv`(15 個事件)
|
||||
- 或使用 `demo_life_events.csv`(11 個事件)
|
||||
|
||||
3. **生成時間軸**
|
||||
- 選擇 Plotly 渲染模式
|
||||
- 點擊「生成時間軸」按鈕
|
||||
|
||||
4. **觀察效果**
|
||||
- ✅ 檢查是否有 5 個固定泳道
|
||||
- ✅ 檢查文字框是否無重疊
|
||||
- ✅ 檢查連接線是否交叉最少
|
||||
- ✅ 檢查視覺效果是否清晰
|
||||
|
||||
---
|
||||
|
||||
## 📊 v9.0 與前版本對比
|
||||
|
||||
| 項目 | v8.0 (D3 Force) | v9.0 (固定5泳道 + 貪婪算法) |
|
||||
|------|----------------|---------------------------|
|
||||
| **泳道數量** | 動態(無限制) | 固定 5 個 |
|
||||
| **標籤分配** | 力導向模擬 | 貪婪算法 |
|
||||
| **避碰策略** | 物理碰撞力 | 衝突分數計算 |
|
||||
| **文字框重疊** | ❌ 偶爾發生 | ✅ 高權重避免(10.0) |
|
||||
| **連接線交叉** | ❌ 較多 | ✅ 低權重優化(1.0) |
|
||||
| **計算複雜度** | O(n² × iterations) | O(n × 5) = O(n) |
|
||||
| **視覺穩定性** | ⚠️ 不穩定(動態) | ✅ 穩定(固定) |
|
||||
| **可預測性** | ❌ 低 | ✅ 高 |
|
||||
|
||||
---
|
||||
|
||||
## 🎯 v9.0 優勢
|
||||
|
||||
1. **固定泳道** - 視覺穩定,易於理解
|
||||
2. **貪婪算法** - 快速高效,O(n) 複雜度
|
||||
3. **衝突分數** - 精確控制重疊和交叉的優先級
|
||||
4. **可調優** - 簡單調整權重即可改變行為
|
||||
5. **回歸 Plotly** - 成熟穩定的渲染引擎
|
||||
6. **🆕 連接線避讓** - 選擇泳道時避免連接線穿過標籤,視覺清晰
|
||||
|
||||
---
|
||||
|
||||
## 🔧 參數調整(可選)
|
||||
|
||||
如需調整避讓行為,可修改 `_calculate_lane_conflicts()` 方法中的權重:
|
||||
|
||||
```python
|
||||
# 文字框重疊權重(默認:10.0)
|
||||
score += 10.0 * overlap_ratio
|
||||
|
||||
# 連接線穿過文字框權重(默認:8.0)✨ 新增
|
||||
score += 8.0
|
||||
|
||||
# 連接線交叉權重(默認:1.0)
|
||||
score += 1.0
|
||||
|
||||
# 同側遮擋權重(默認:0.5)
|
||||
score += 0.5
|
||||
```
|
||||
|
||||
**建議**:
|
||||
- 文字框重疊權重 10.0:最高優先級,必須避免
|
||||
- 連接線穿過文字框 8.0:次高優先級,嚴重影響可讀性
|
||||
- 連接線交叉權重 1.0:低優先級,視覺影響小
|
||||
- 保持比例 10:8:1:0.5 通常效果最佳
|
||||
|
||||
---
|
||||
|
||||
## ✅ 實施總結
|
||||
|
||||
- **實施時間**:約 2 小時
|
||||
- **修改檔案**:1 個(`backend/renderer_timeline.py`)
|
||||
- **新增方法**:3 個
|
||||
- `_calculate_label_positions()` - 固定5泳道 + 貪婪算法
|
||||
- `_calculate_lane_conflicts()` - 衝突分數計算(含連接線穿過檢測)
|
||||
- `_check_line_intersects_textbox()` - 線段與文字框碰撞檢測
|
||||
- **程式碼行數**:約 280 行
|
||||
- **測試狀態**:待驗證
|
||||
|
||||
**v9.0 已完成(含連接線避讓增強)!現在請啟動應用並測試效果。** 🎉
|
||||
|
||||
---
|
||||
|
||||
## 🎨 連接線避讓示意圖
|
||||
|
||||
### 問題場景
|
||||
```
|
||||
標籤B
|
||||
↑
|
||||
|
|
||||
|─────────[標籤A]─────→ 標籤A
|
||||
| 遮擋! ↑
|
||||
| |
|
||||
●─────────────────────●
|
||||
事件點B 事件點A
|
||||
```
|
||||
**問題**:標籤B的連接線(水平線段)穿過標籤A的文字框
|
||||
|
||||
### v9.0 解決方案
|
||||
```
|
||||
標籤B 標籤A
|
||||
↑ ↑
|
||||
| |
|
||||
|────→ ←─────| (較高泳道)
|
||||
| |
|
||||
| [標籤A] |
|
||||
●───────────────────●
|
||||
事件點B 事件點A
|
||||
```
|
||||
**解決**:貪婪算法讓標籤B選擇較高泳道,連接線不穿過標籤A
|
||||
@@ -1,494 +0,0 @@
|
||||
# 遷移到 D3.js Force-Directed Layout - 實施計劃
|
||||
|
||||
## 📋 目標
|
||||
|
||||
將時間軸標籤避讓邏輯從**後端 Plotly**遷移到**前端 D3.js d3-force**,實現專業的標籤碰撞避讓。
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ 架構變更
|
||||
|
||||
### 當前架構(v7.0)
|
||||
```
|
||||
┌─────────┐ 事件資料 ┌─────────┐ Plotly圖表 ┌─────────┐
|
||||
│ Python │ --------> │ 計算 │ ----------> │ React │
|
||||
│ 後端 │ │ 標籤位置 │ │ 前端 │
|
||||
└─────────┘ └─────────┘ └─────────┘
|
||||
❌ 標籤避讓在這裡(效果差)
|
||||
```
|
||||
|
||||
### 新架構(D3 Force)
|
||||
```
|
||||
┌─────────┐ 事件資料 ┌─────────────┐ 渲染座標 ┌─────────┐
|
||||
│ Python │ --------> │ D3 Force │ ---------> │ React │
|
||||
│ 後端 │ (乾淨) │ 標籤避讓 │ │ 前端 │
|
||||
└─────────┘ └─────────────┘ └─────────┘
|
||||
✅ 力導向演算法在這裡
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📦 步驟 1: 安裝 D3.js 依賴
|
||||
|
||||
```bash
|
||||
cd frontend-react
|
||||
npm install d3 d3-force d3-scale d3-axis d3-selection
|
||||
npm install --save-dev @types/d3
|
||||
```
|
||||
|
||||
**安裝的模組**:
|
||||
- `d3-force`: 力導向布局核心
|
||||
- `d3-scale`: 時間軸刻度
|
||||
- `d3-axis`: 軸線繪製
|
||||
- `d3-selection`: DOM 操作
|
||||
|
||||
---
|
||||
|
||||
## 🔧 步驟 2: 修改後端 API
|
||||
|
||||
### 2.1 新增端點:返回原始事件資料
|
||||
|
||||
**檔案**: `backend/main.py`
|
||||
|
||||
```python
|
||||
@router.get("/api/events/raw")
|
||||
async def get_raw_events():
|
||||
"""
|
||||
返回原始事件資料(不做任何布局計算)
|
||||
供前端 D3.js 使用
|
||||
"""
|
||||
events = event_manager.get_events()
|
||||
return {
|
||||
"success": True,
|
||||
"events": [
|
||||
{
|
||||
"id": i,
|
||||
"start": event.start.isoformat(),
|
||||
"end": event.end.isoformat() if event.end else None,
|
||||
"title": event.title,
|
||||
"description": event.description,
|
||||
"color": event.color or "#3B82F6",
|
||||
"layer": event.layer
|
||||
}
|
||||
for i, event in enumerate(events)
|
||||
],
|
||||
"count": len(events)
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 保留 Plotly 端點作為備選
|
||||
|
||||
```python
|
||||
@router.post("/api/render") # 保留舊版
|
||||
@router.post("/api/render/plotly") # 明確標記
|
||||
async def render_plotly_timeline(config: TimelineConfig):
|
||||
# ... 現有代碼 ...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎨 步驟 3: 創建 D3 時間軸組件
|
||||
|
||||
### 3.1 創建組件文件
|
||||
|
||||
**檔案**: `frontend-react/src/components/D3Timeline.tsx`
|
||||
|
||||
```typescript
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import * as d3 from 'd3';
|
||||
|
||||
interface Event {
|
||||
id: number;
|
||||
start: string;
|
||||
end?: string;
|
||||
title: string;
|
||||
description: string;
|
||||
color: string;
|
||||
layer: number;
|
||||
}
|
||||
|
||||
interface D3TimelineProps {
|
||||
events: Event[];
|
||||
width?: number;
|
||||
height?: number;
|
||||
}
|
||||
|
||||
interface Node extends d3.SimulationNodeDatum {
|
||||
id: number;
|
||||
type: 'event' | 'label';
|
||||
eventId: number;
|
||||
x: number;
|
||||
y: number;
|
||||
fx?: number | null; // 固定 X(事件點)
|
||||
fy?: number | null; // 固定 Y(事件點在時間軸上)
|
||||
event: Event;
|
||||
labelWidth: number;
|
||||
labelHeight: number;
|
||||
}
|
||||
|
||||
export default function D3Timeline({ events, width = 1200, height = 600 }: D3TimelineProps) {
|
||||
const svgRef = useRef<SVGSVGElement>(null);
|
||||
const [simulation, setSimulation] = useState<d3.Simulation<Node, undefined> | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!svgRef.current || events.length === 0) return;
|
||||
|
||||
// 清空 SVG
|
||||
const svg = d3.select(svgRef.current);
|
||||
svg.selectAll('*').remove();
|
||||
|
||||
// 邊距設定
|
||||
const margin = { top: 100, right: 50, bottom: 50, left: 50 };
|
||||
const innerWidth = width - margin.left - margin.right;
|
||||
const innerHeight = height - margin.top - margin.bottom;
|
||||
|
||||
// 創建主 group
|
||||
const g = svg
|
||||
.append('g')
|
||||
.attr('transform', `translate(${margin.left},${margin.top})`);
|
||||
|
||||
// 時間範圍
|
||||
const dates = events.map(e => new Date(e.start));
|
||||
const xScale = d3.scaleTime()
|
||||
.domain([d3.min(dates)!, d3.max(dates)!])
|
||||
.range([0, innerWidth]);
|
||||
|
||||
// 時間軸線
|
||||
const axisY = innerHeight / 2;
|
||||
g.append('line')
|
||||
.attr('x1', 0)
|
||||
.attr('x2', innerWidth)
|
||||
.attr('y1', axisY)
|
||||
.attr('y2', axisY)
|
||||
.attr('stroke', '#3B82F6')
|
||||
.attr('stroke-width', 3);
|
||||
|
||||
// 準備節點資料
|
||||
const nodes: Node[] = [];
|
||||
|
||||
events.forEach((event, i) => {
|
||||
const eventX = xScale(new Date(event.start));
|
||||
|
||||
// 事件點節點(固定位置)
|
||||
nodes.push({
|
||||
id: i * 2,
|
||||
type: 'event',
|
||||
eventId: i,
|
||||
x: eventX,
|
||||
y: axisY,
|
||||
fx: eventX, // 固定 X - 保證時間準確性
|
||||
fy: axisY, // 固定 Y - 在時間軸上
|
||||
event,
|
||||
labelWidth: 0,
|
||||
labelHeight: 0
|
||||
});
|
||||
|
||||
// 標籤節點(可移動)
|
||||
const labelWidth = Math.max(event.title.length * 8, 120);
|
||||
const labelHeight = 60;
|
||||
const initialY = event.layer % 2 === 0 ? axisY - 150 : axisY + 150;
|
||||
|
||||
nodes.push({
|
||||
id: i * 2 + 1,
|
||||
type: 'label',
|
||||
eventId: i,
|
||||
x: eventX, // 初始 X 接近事件點
|
||||
y: initialY, // 初始 Y 根據層級
|
||||
fx: null,
|
||||
fy: null,
|
||||
event,
|
||||
labelWidth,
|
||||
labelHeight
|
||||
});
|
||||
});
|
||||
|
||||
// 連接線(標籤 → 事件點)
|
||||
const links = nodes
|
||||
.filter(n => n.type === 'label')
|
||||
.map(label => ({
|
||||
source: label.id,
|
||||
target: label.id - 1 // 對應的事件點
|
||||
}));
|
||||
|
||||
// D3 力導向模擬
|
||||
const sim = d3.forceSimulation(nodes)
|
||||
// 1. 碰撞力:標籤之間互相推開
|
||||
.force('collide', d3.forceCollide<Node>()
|
||||
.radius(d => {
|
||||
if (d.type === 'label') {
|
||||
// 使用橢圓碰撞半徑(考慮文字框寬高)
|
||||
return Math.max(d.labelWidth / 2, d.labelHeight / 2) + 10;
|
||||
}
|
||||
return 5; // 事件點不參與碰撞
|
||||
})
|
||||
.strength(0.8)
|
||||
)
|
||||
// 2. 連結力:標籤拉向事件點(像彈簧)
|
||||
.force('link', d3.forceLink(links)
|
||||
.id(d => (d as Node).id)
|
||||
.distance(100) // 理想距離
|
||||
.strength(0.3) // 彈簧強度
|
||||
)
|
||||
// 3. X 方向定位力:標籤靠近事件點的 X 座標
|
||||
.force('x', d3.forceX<Node>(d => {
|
||||
if (d.type === 'label') {
|
||||
const eventNode = nodes.find(n => n.type === 'event' && n.eventId === d.eventId);
|
||||
return eventNode ? eventNode.x : d.x;
|
||||
}
|
||||
return d.x;
|
||||
}).strength(0.5))
|
||||
// 4. Y 方向定位力:標籤保持在上方或下方
|
||||
.force('y', d3.forceY<Node>(d => {
|
||||
if (d.type === 'label') {
|
||||
return d.y < axisY ? axisY - 120 : axisY + 120;
|
||||
}
|
||||
return axisY;
|
||||
}).strength(0.3))
|
||||
// 5. 邊界限制
|
||||
.on('tick', () => {
|
||||
nodes.forEach(d => {
|
||||
if (d.type === 'label') {
|
||||
// 限制 Y 範圍
|
||||
if (d.y! < 20) d.y = 20;
|
||||
if (d.y! > innerHeight - 20) d.y = innerHeight - 20;
|
||||
|
||||
// 限制 X 範圍(允許小幅偏移)
|
||||
const eventNode = nodes.find(n => n.type === 'event' && n.eventId === d.eventId)!;
|
||||
const maxOffset = 80;
|
||||
if (Math.abs(d.x! - eventNode.x!) > maxOffset) {
|
||||
d.x = eventNode.x! + (d.x! > eventNode.x! ? maxOffset : -maxOffset);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
updateVisualization();
|
||||
});
|
||||
|
||||
setSimulation(sim);
|
||||
|
||||
// 繪製可視化元素
|
||||
function updateVisualization() {
|
||||
// 連接線
|
||||
const linkElements = g.selectAll<SVGLineElement, any>('.link')
|
||||
.data(links)
|
||||
.join('line')
|
||||
.attr('class', 'link')
|
||||
.attr('x1', d => {
|
||||
const source = nodes.find(n => n.id === (typeof d.source === 'number' ? d.source : d.source.id))!;
|
||||
return source.x!;
|
||||
})
|
||||
.attr('y1', d => {
|
||||
const source = nodes.find(n => n.id === (typeof d.source === 'number' ? d.source : d.source.id))!;
|
||||
return source.y!;
|
||||
})
|
||||
.attr('x2', d => {
|
||||
const target = nodes.find(n => n.id === (typeof d.target === 'number' ? d.target : d.target.id))!;
|
||||
return target.x!;
|
||||
})
|
||||
.attr('y2', d => {
|
||||
const target = nodes.find(n => n.id === (typeof d.target === 'number' ? d.target : d.target.id))!;
|
||||
return target.y!;
|
||||
})
|
||||
.attr('stroke', '#94a3b8')
|
||||
.attr('stroke-width', 1.5)
|
||||
.attr('opacity', 0.7);
|
||||
|
||||
// 事件點
|
||||
const eventNodes = g.selectAll<SVGCircleElement, Node>('.event-node')
|
||||
.data(nodes.filter(n => n.type === 'event'))
|
||||
.join('circle')
|
||||
.attr('class', 'event-node')
|
||||
.attr('cx', d => d.x!)
|
||||
.attr('cy', d => d.y!)
|
||||
.attr('r', 8)
|
||||
.attr('fill', d => d.event.color)
|
||||
.attr('stroke', '#fff')
|
||||
.attr('stroke-width', 2);
|
||||
|
||||
// 標籤文字框
|
||||
const labelGroups = g.selectAll<SVGGElement, Node>('.label-group')
|
||||
.data(nodes.filter(n => n.type === 'label'))
|
||||
.join('g')
|
||||
.attr('class', 'label-group')
|
||||
.attr('transform', d => `translate(${d.x! - d.labelWidth / 2},${d.y! - d.labelHeight / 2})`);
|
||||
|
||||
// 文字框背景
|
||||
labelGroups.selectAll('rect')
|
||||
.data(d => [d])
|
||||
.join('rect')
|
||||
.attr('width', d => d.labelWidth)
|
||||
.attr('height', d => d.labelHeight)
|
||||
.attr('rx', 6)
|
||||
.attr('fill', 'white')
|
||||
.attr('opacity', 0.9)
|
||||
.attr('stroke', d => d.event.color)
|
||||
.attr('stroke-width', 2);
|
||||
|
||||
// 文字內容
|
||||
labelGroups.selectAll('text')
|
||||
.data(d => [d])
|
||||
.join('text')
|
||||
.attr('x', d => d.labelWidth / 2)
|
||||
.attr('y', 20)
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('font-size', 12)
|
||||
.attr('font-weight', 'bold')
|
||||
.text(d => d.event.title);
|
||||
}
|
||||
|
||||
// 初始繪製
|
||||
updateVisualization();
|
||||
|
||||
// 清理函數
|
||||
return () => {
|
||||
sim.stop();
|
||||
};
|
||||
}, [events, width, height]);
|
||||
|
||||
return (
|
||||
<div className="border border-gray-200 rounded-lg overflow-hidden bg-white">
|
||||
<svg
|
||||
ref={svgRef}
|
||||
width={width}
|
||||
height={height}
|
||||
className="w-full h-auto"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔗 步驟 4: 整合到 App.tsx
|
||||
|
||||
**檔案**: `frontend-react/src/App.tsx`
|
||||
|
||||
```typescript
|
||||
import { useState, useCallback } from 'react';
|
||||
import D3Timeline from './components/D3Timeline';
|
||||
import { timelineAPI } from './api/timeline';
|
||||
|
||||
function App() {
|
||||
const [events, setEvents] = useState<any[]>([]);
|
||||
const [renderMode, setRenderMode] = useState<'d3' | 'plotly'>('d3');
|
||||
|
||||
// ... 其他現有代碼 ...
|
||||
|
||||
// 載入原始事件資料(for D3)
|
||||
const loadEventsForD3 = async () => {
|
||||
try {
|
||||
const response = await axios.get('http://localhost:8000/api/events/raw');
|
||||
if (response.data.success) {
|
||||
setEvents(response.data.events);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load events:', error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-primary-500 to-secondary-500 p-6">
|
||||
{/* ... 現有代碼 ... */}
|
||||
|
||||
{/* 渲染模式切換 */}
|
||||
<div className="card">
|
||||
<div className="flex gap-4 mb-4">
|
||||
<button
|
||||
onClick={() => setRenderMode('d3')}
|
||||
className={renderMode === 'd3' ? 'btn-primary' : 'btn-secondary'}
|
||||
>
|
||||
D3.js 力導向
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setRenderMode('plotly')}
|
||||
className={renderMode === 'plotly' ? 'btn-primary' : 'btn-secondary'}
|
||||
>
|
||||
Plotly(舊版)
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{renderMode === 'd3' && events.length > 0 && (
|
||||
<D3Timeline events={events} />
|
||||
)}
|
||||
|
||||
{renderMode === 'plotly' && plotlyData && (
|
||||
<Plot data={plotlyData.data} layout={plotlyLayout} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 關鍵技術點
|
||||
|
||||
### 1. 固定事件點位置
|
||||
```typescript
|
||||
{
|
||||
fx: eventX, // 固定 X - 保證時間準確性
|
||||
fy: axisY, // 固定 Y - 在時間軸上
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 碰撞力(避免重疊)
|
||||
```typescript
|
||||
.force('collide', d3.forceCollide<Node>()
|
||||
.radius(d => Math.max(d.labelWidth / 2, d.labelHeight / 2) + 10)
|
||||
.strength(0.8)
|
||||
)
|
||||
```
|
||||
|
||||
### 3. 連結力(彈簧效果)
|
||||
```typescript
|
||||
.force('link', d3.forceLink(links)
|
||||
.distance(100) // 理想距離
|
||||
.strength(0.3) // 彈簧強度
|
||||
)
|
||||
```
|
||||
|
||||
### 4. 限制標籤 X 偏移
|
||||
```typescript
|
||||
const maxOffset = 80;
|
||||
if (Math.abs(labelX - eventX) > maxOffset) {
|
||||
labelX = eventX + (labelX > eventX ? maxOffset : -maxOffset);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 優勢對比
|
||||
|
||||
| 項目 | Plotly 後端 | D3 Force 前端 |
|
||||
|------|------------|---------------|
|
||||
| 標籤避讓效果 | ⚠️ 差 | ✅ 專業 |
|
||||
| 動態調整 | ❌ 需重新渲染 | ✅ 即時模擬 |
|
||||
| 性能 | ⚠️ 後端計算 | ✅ 瀏覽器端 |
|
||||
| 可定制性 | ❌ 有限 | ✅ 完全控制 |
|
||||
| 開發成本 | ✅ 低 | ⚠️ 中等 |
|
||||
|
||||
---
|
||||
|
||||
## ⏱️ 實施時程
|
||||
|
||||
- **步驟 1**: 安裝依賴 - **15分鐘**
|
||||
- **步驟 2**: 修改後端 API - **30分鐘**
|
||||
- **步驟 3**: 創建 D3 組件 - **2-3小時**
|
||||
- **步驟 4**: 整合到 App - **1小時**
|
||||
- **測試調優**: **1-2小時**
|
||||
|
||||
**總計**: **半天到一天**
|
||||
|
||||
---
|
||||
|
||||
## 🚀 下一步
|
||||
|
||||
您希望我:
|
||||
|
||||
**A.** 立即開始實施(按步驟執行)
|
||||
**B.** 先創建一個簡化版 POC 測試效果
|
||||
**C.** 評估其他替代方案(vis-timeline, react-calendar-timeline)
|
||||
|
||||
請告訴我您的選擇!
|
||||
72
SDD.md
72
SDD.md
@@ -1,72 +0,0 @@
|
||||
# 📗 System Design Document (SDD)
|
||||
|
||||
## 1. 架構概述
|
||||
```
|
||||
PyWebview Host
|
||||
├── FastAPI Backend
|
||||
│ ├── importer.py(CSV/XLSX 處理)
|
||||
│ ├── renderer.py(Plotly/kaleido 渲染)
|
||||
│ ├── schemas.py(資料模型定義)
|
||||
│ └── export.py(PDF/SVG/PNG 輸出)
|
||||
└── Frontend (React + Tailwind)
|
||||
├── TimelineCanvas(vis-timeline 封裝)
|
||||
├── EventForm / ThemePanel / ExportDialog
|
||||
└── api.ts(API 呼叫)
|
||||
```
|
||||
|
||||
## 2. 資料模型
|
||||
```python
|
||||
class Event(BaseModel):
|
||||
id: str
|
||||
title: str
|
||||
start: datetime
|
||||
end: Optional[datetime]
|
||||
group: Optional[str]
|
||||
description: Optional[str]
|
||||
color: Optional[str]
|
||||
|
||||
class TimelineConfig(BaseModel):
|
||||
direction: Literal['horizontal', 'vertical'] = 'horizontal'
|
||||
theme: str = 'modern'
|
||||
show_grid: bool = True
|
||||
|
||||
class ExportOptions(BaseModel):
|
||||
fmt: Literal['png', 'pdf', 'svg']
|
||||
dpi: int = 300
|
||||
```
|
||||
|
||||
## 3. API 定義
|
||||
| Method | Endpoint | 功能 | 輸入 | 輸出 |
|
||||
|---------|-----------|------|------|------|
|
||||
| POST | /import | 匯入事件資料 | CSV/XLSX | Event[] |
|
||||
| GET | /events | 取得事件列表 | None | Event[] |
|
||||
| POST | /render | 生成時間軸 JSON | TimelineConfig | Plotly JSON |
|
||||
| POST | /export | 導出時間軸圖 | ExportOptions | 圖檔 |
|
||||
| GET | /themes | 主題列表 | None | Theme[] |
|
||||
|
||||
## 4. 視覺化邏輯
|
||||
- 自動調整時間刻度(日/週/月)
|
||||
- 重疊節點避碰算法
|
||||
- 拖曳吸附(Snap to Grid)
|
||||
- Hover 顯示 Tooltip 詳細資訊
|
||||
|
||||
## 5. 前端契約
|
||||
```tsx
|
||||
<TimelineCanvas
|
||||
events={Event[]}
|
||||
config={TimelineConfig}
|
||||
onSelect={(id)=>{}}
|
||||
onMove={(id,newStart)=>{}}
|
||||
/>
|
||||
```
|
||||
|
||||
## 6. 系統相依性
|
||||
| 模組 | 用途 |
|
||||
|------|------|
|
||||
| PyWebview | 原生 GUI 容器 |
|
||||
| FastAPI | 後端 API 框架 |
|
||||
| React | 前端 UI |
|
||||
| Tailwind | 樣式系統 |
|
||||
| Plotly/kaleido | 圖表渲染與輸出 |
|
||||
| Playwright | 截圖與測試 |
|
||||
|
||||
54
TDD.md
54
TDD.md
@@ -1,54 +0,0 @@
|
||||
# 📙 Test Driven Development (TDD)
|
||||
|
||||
## 1. 測試分類與範圍
|
||||
| 類型 | 工具 | 範圍 |
|
||||
|------|------|------|
|
||||
| 單元測試 | pytest | importer、renderer、export 模組 |
|
||||
| 端對端測試 | Playwright | 前端互動與整體流程 |
|
||||
| 效能測試 | pytest-benchmark | 渲染與輸出效能 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 單元測試案例
|
||||
| 編號 | 測試項目 | 驗證重點 |
|
||||
|------|-----------|------------|
|
||||
| UT-IMP-01 | 匯入 CSV 欄位解析 | 欄位自動對應與格式容錯 |
|
||||
| UT-REN-01 | 時間刻度演算法 | 不同時間跨度下刻度精準性 |
|
||||
| UT-REN-02 | 節點避碰演算法 | 重疊節點之排版與間距合理性 |
|
||||
| UT-EXP-01 | PDF 輸出完整性 | 字型嵌入與 DPI 驗證 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 端對端測試(E2E)流程
|
||||
1. 匯入測試資料(CSV)。
|
||||
2. 驗證時間軸正確渲染。
|
||||
3. 切換主題並重新渲染。
|
||||
4. 匯出 PNG/PDF 並確認檔案存在與開啟性。
|
||||
5. 驗證畫面快照差異 ≤ 0.5%。
|
||||
|
||||
---
|
||||
|
||||
## 4. 效能與穩定性測試
|
||||
| 測試項目 | 標準 | 通過條件 |
|
||||
|-----------|------|-----------|
|
||||
| 100 筆事件 | <1 秒 | 無延遲或崩潰 |
|
||||
| 300 筆事件 | <3 秒 | FPS ≥ 30 |
|
||||
| 匯出任務 | <2 秒 | 正確生成檔案 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 測試環境與自動化
|
||||
| 組件 | 工具 |
|
||||
|------|------|
|
||||
| 測試框架 | pytest, Playwright |
|
||||
| 持續整合 | GitHub Actions |
|
||||
| 覆蓋率 | coverage.py + htmlcov |
|
||||
| 報告生成 | Allure / pytest-html |
|
||||
|
||||
---
|
||||
|
||||
## 6. 驗收條件
|
||||
- 單元測試覆蓋率 ≥ 80%。
|
||||
- E2E 測試通過率 = 100%。
|
||||
- 效能達標:渲染與輸出均在 KPI 內。
|
||||
|
||||
@@ -1,510 +0,0 @@
|
||||
# TimeLine Designer - 前端開發完成報告
|
||||
|
||||
**版本**: 1.0.0
|
||||
**日期**: 2025-11-05
|
||||
**技術棧**: React 18 + TypeScript + Vite + Tailwind CSS v3
|
||||
**DocID**: FRONTEND-DEV-001
|
||||
|
||||
---
|
||||
|
||||
## 🎯 開發摘要
|
||||
|
||||
### 完成項目
|
||||
- ✅ **React + Vite + TypeScript** 專案建立
|
||||
- ✅ **Tailwind CSS v3** 樣式系統配置
|
||||
- ✅ **完整 UI 元件** 實作
|
||||
- ✅ **API 客戶端** 整合
|
||||
- ✅ **Plotly.js** 時間軸圖表渲染
|
||||
- ✅ **檔案拖放上傳** 功能
|
||||
- ✅ **前端編譯通過** 準備就緒
|
||||
|
||||
---
|
||||
|
||||
## 📁 專案結構
|
||||
|
||||
```
|
||||
frontend-react/
|
||||
├── src/
|
||||
│ ├── api/
|
||||
│ │ ├── client.ts # Axios 客戶端配置
|
||||
│ │ └── timeline.ts # Timeline API 服務
|
||||
│ ├── types/
|
||||
│ │ └── index.ts # TypeScript 類型定義
|
||||
│ ├── App.tsx # 主要應用程式元件
|
||||
│ ├── index.css # Tailwind CSS 配置
|
||||
│ └── main.tsx # 應用入口
|
||||
├── .env.development # 開發環境變數
|
||||
├── tailwind.config.js # Tailwind 配置
|
||||
├── postcss.config.js # PostCSS 配置
|
||||
├── vite.config.ts # Vite 配置
|
||||
├── tsconfig.json # TypeScript 配置
|
||||
└── package.json # 專案依賴
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ 技術棧詳情
|
||||
|
||||
### 核心依賴
|
||||
```json
|
||||
{
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"typescript": "~5.6.2",
|
||||
"vite": "^7.1.12"
|
||||
}
|
||||
```
|
||||
|
||||
### UI 依賴
|
||||
```json
|
||||
{
|
||||
"tailwindcss": "^3.4.17",
|
||||
"lucide-react": "^0.468.0",
|
||||
"react-dropzone": "^14.3.5"
|
||||
}
|
||||
```
|
||||
|
||||
### 圖表與網路
|
||||
```json
|
||||
{
|
||||
"plotly.js": "^2.36.0",
|
||||
"react-plotly.js": "^2.6.0",
|
||||
"axios": "^1.7.9"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎨 UI 功能實作
|
||||
|
||||
### 1. 檔案上傳區 (File Upload)
|
||||
**功能**:
|
||||
- 拖放上傳 CSV/XLSX 檔案
|
||||
- 點擊上傳檔案
|
||||
- 即時視覺回饋
|
||||
- 支援檔案格式驗證
|
||||
|
||||
**技術**:
|
||||
- `react-dropzone` 處理拖放
|
||||
- FormData API 上傳
|
||||
- MIME 類型驗證
|
||||
|
||||
**程式碼位置**: `App.tsx:60-68`
|
||||
|
||||
---
|
||||
|
||||
### 2. 事件管理
|
||||
**功能**:
|
||||
- 顯示目前事件數量
|
||||
- 生成時間軸按鈕
|
||||
- 清空所有事件
|
||||
|
||||
**狀態管理**:
|
||||
```typescript
|
||||
const [eventsCount, setEventsCount] = useState(0);
|
||||
```
|
||||
|
||||
**API 整合**:
|
||||
- GET `/api/events` - 取得事件列表
|
||||
- DELETE `/api/events` - 清空事件
|
||||
|
||||
---
|
||||
|
||||
### 3. 時間軸預覽 (Timeline Preview)
|
||||
**功能**:
|
||||
- Plotly.js 互動式圖表
|
||||
- 響應式容器 (100% 寬度, 600px 高度)
|
||||
- 載入動畫
|
||||
- 空狀態提示
|
||||
|
||||
**Plotly 整合**:
|
||||
```typescript
|
||||
<Plot
|
||||
data={plotlyData.data}
|
||||
layout={plotlyLayout}
|
||||
config={{ responsive: true }}
|
||||
style={{ width: '100%', height: '600px' }}
|
||||
/>
|
||||
```
|
||||
|
||||
**程式碼位置**: `App.tsx:230-244`
|
||||
|
||||
---
|
||||
|
||||
### 4. 匯出選項 (Export Options)
|
||||
**功能**:
|
||||
- 格式選擇 (PDF, PNG, SVG)
|
||||
- DPI 設定 (150, 300, 600)
|
||||
- 自動下載檔案
|
||||
|
||||
**實作**:
|
||||
```typescript
|
||||
const exportTimeline = async () => {
|
||||
const blob = await timelineAPI.exportTimeline(plotlyData, plotlyLayout, options);
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `timeline.${exportFormat}`;
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔌 API 客戶端架構
|
||||
|
||||
### API Base Configuration
|
||||
```typescript
|
||||
// src/api/client.ts
|
||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:12010/api';
|
||||
|
||||
export const apiClient = axios.create({
|
||||
baseURL: API_BASE_URL,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
timeout: 30000,
|
||||
});
|
||||
```
|
||||
|
||||
### API 服務層
|
||||
```typescript
|
||||
// src/api/timeline.ts
|
||||
export const timelineAPI = {
|
||||
async importFile(file: File): Promise<ImportResult> { ... },
|
||||
async getEvents(): Promise<Event[]> { ... },
|
||||
async renderTimeline(config?: TimelineConfig): Promise<RenderResult> { ... },
|
||||
async exportTimeline(...): Promise<Blob> { ... },
|
||||
// ...更多 API
|
||||
};
|
||||
```
|
||||
|
||||
**優勢**:
|
||||
- 類型安全 (TypeScript)
|
||||
- 統一錯誤處理
|
||||
- Request/Response 攔截器
|
||||
- 易於測試和維護
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ Vite 配置
|
||||
|
||||
### 開發伺服器
|
||||
```typescript
|
||||
// vite.config.ts
|
||||
export default defineConfig({
|
||||
server: {
|
||||
port: 12010,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:8000',
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
**說明**:
|
||||
- 前端端口: `12010`
|
||||
- 後端代理: `/api` → `http://localhost:8000`
|
||||
- 符合 CLAUDE.md 端口規範 (12010-12019)
|
||||
|
||||
---
|
||||
|
||||
## 🎨 Tailwind CSS 配置
|
||||
|
||||
### 自訂主題色
|
||||
```javascript
|
||||
// tailwind.config.js
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: {
|
||||
500: '#667eea', // 主要漸層起點
|
||||
600: '#5b68e0',
|
||||
},
|
||||
secondary: {
|
||||
500: '#764ba2', // 主要漸層終點
|
||||
600: '#6b4391',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### 自訂 CSS 類別
|
||||
```css
|
||||
/* src/index.css */
|
||||
.btn-primary {
|
||||
@apply btn bg-gradient-to-r from-primary-500 to-secondary-500 text-white;
|
||||
}
|
||||
|
||||
.card {
|
||||
@apply bg-white rounded-xl shadow-2xl p-6;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
@apply text-2xl font-bold text-primary-600 mb-4 pb-2 border-b-2;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 TypeScript 類型系統
|
||||
|
||||
### Event 類型
|
||||
```typescript
|
||||
export interface Event {
|
||||
id: string;
|
||||
title: string;
|
||||
start: string; // ISO date string
|
||||
end?: string;
|
||||
group?: string;
|
||||
description?: string;
|
||||
color?: string;
|
||||
}
|
||||
```
|
||||
|
||||
### API Response 類型
|
||||
```typescript
|
||||
export interface APIResponse<T = any> {
|
||||
success: boolean;
|
||||
message?: string;
|
||||
data?: T;
|
||||
error_code?: string;
|
||||
}
|
||||
|
||||
export interface ImportResult {
|
||||
success: boolean;
|
||||
imported_count: number;
|
||||
events: Event[];
|
||||
errors: string[];
|
||||
}
|
||||
```
|
||||
|
||||
**優勢**:
|
||||
- 編譯時類型檢查
|
||||
- IDE 自動完成
|
||||
- 重構安全
|
||||
- 減少執行時錯誤
|
||||
|
||||
---
|
||||
|
||||
## 🚀 啟動方式
|
||||
|
||||
### 開發環境
|
||||
|
||||
#### 方法 1: 使用便捷腳本
|
||||
```batch
|
||||
# Windows
|
||||
start_dev.bat
|
||||
```
|
||||
|
||||
這會同時啟動:
|
||||
- 後端: `http://localhost:8000`
|
||||
- 前端: `http://localhost:12010`
|
||||
|
||||
#### 方法 2: 手動啟動
|
||||
|
||||
**後端**:
|
||||
```bash
|
||||
conda activate timeline_designer
|
||||
uvicorn backend.main:app --reload --port 8000
|
||||
```
|
||||
|
||||
**前端**:
|
||||
```bash
|
||||
cd frontend-react
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### 生產環境
|
||||
|
||||
**建置前端**:
|
||||
```bash
|
||||
cd frontend-react
|
||||
npm run build
|
||||
```
|
||||
|
||||
**輸出**:
|
||||
- 目錄: `frontend-react/dist/`
|
||||
- 檔案大小: ~5.2 MB (含 Plotly.js)
|
||||
- Gzip 壓縮: ~1.6 MB
|
||||
|
||||
---
|
||||
|
||||
## 🔍 技術亮點
|
||||
|
||||
### 1. 響應式設計
|
||||
- Tailwind CSS utility classes
|
||||
- Flexbox 佈局
|
||||
- 響應式容器 (`max-w-7xl mx-auto`)
|
||||
|
||||
### 2. 使用者體驗
|
||||
- 拖放上傳檔案
|
||||
- 即時載入狀態
|
||||
- 自動消失的訊息提示 (5 秒)
|
||||
- 按鈕禁用狀態管理
|
||||
|
||||
### 3. 效能優化
|
||||
- `useCallback` 避免重複渲染
|
||||
- Vite 快速熱更新 (HMR)
|
||||
- 生產環境 Tree Shaking
|
||||
|
||||
### 4. 程式碼品質
|
||||
- TypeScript 嚴格模式
|
||||
- ESLint 程式碼檢查
|
||||
- 統一的 API 層
|
||||
- 清晰的檔案組織
|
||||
|
||||
---
|
||||
|
||||
## 📊 編譯結果
|
||||
|
||||
### 成功編譯
|
||||
```
|
||||
✓ 1748 modules transformed
|
||||
✓ built in 31.80s
|
||||
|
||||
dist/index.html 0.46 kB │ gzip: 0.29 kB
|
||||
dist/assets/index-v0MVqyGF.css 13.54 kB │ gzip: 3.14 kB
|
||||
dist/assets/index-RyjrDfo0.js 5,185.45 kB │ gzip: 1,579.67 kB
|
||||
```
|
||||
|
||||
### Bundle 分析
|
||||
- **Total Size**: ~5.2 MB
|
||||
- **Gzip Size**: ~1.6 MB
|
||||
- **主要貢獻者**: Plotly.js (~4 MB)
|
||||
|
||||
**優化建議** (可選):
|
||||
- Dynamic import Plotly
|
||||
- 使用 plotly.js-basic-dist (更小的版本)
|
||||
- Code splitting
|
||||
|
||||
---
|
||||
|
||||
## 🧪 測試建議
|
||||
|
||||
### 手動測試清單
|
||||
- [ ] 上傳 CSV 檔案
|
||||
- [ ] 檢視事件數量
|
||||
- [ ] 生成時間軸
|
||||
- [ ] 互動式縮放、拖曳
|
||||
- [ ] 匯出 PDF/PNG/SVG
|
||||
- [ ] 清空事件
|
||||
- [ ] 錯誤處理 (無效檔案)
|
||||
|
||||
### 未來測試
|
||||
- Jest + React Testing Library
|
||||
- E2E 測試 (Playwright)
|
||||
- 視覺回歸測試
|
||||
|
||||
---
|
||||
|
||||
## 📋 環境配置
|
||||
|
||||
### .env.development
|
||||
```env
|
||||
VITE_API_BASE_URL=http://localhost:12010/api
|
||||
```
|
||||
|
||||
### 環境變數使用
|
||||
```typescript
|
||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🐛 已知問題與解決方案
|
||||
|
||||
### Issue 1: Tailwind CSS v4 不相容
|
||||
**問題**: v4 使用新的 PostCSS 插件
|
||||
**解決**: 降級至 v3.4.17 (穩定版本)
|
||||
|
||||
### Issue 2: Plotly.js Bundle Size
|
||||
**問題**: Bundle 超過 5MB
|
||||
**狀態**: 可接受 (地端運行,無頻寬限制)
|
||||
**未來**: 考慮使用 plotly.js-basic-dist
|
||||
|
||||
### Issue 3: TypeScript 類型警告
|
||||
**問題**: `react-plotly.js` 類型缺失
|
||||
**解決**: 安裝 `@types/react-plotly.js`
|
||||
|
||||
---
|
||||
|
||||
## 🎯 PRD 需求對應
|
||||
|
||||
| PRD 需求 | 實作狀態 | 說明 |
|
||||
|---------|---------|------|
|
||||
| 零程式門檻 GUI | ✅ | 拖放上傳,一鍵生成 |
|
||||
| React + Tailwind | ✅ | 完整實作 |
|
||||
| 即時預覽 | ✅ | Plotly 互動式圖表 |
|
||||
| 高解析輸出 | ✅ | DPI 150/300/600 選項 |
|
||||
| 拖曳縮放 | ✅ | Plotly 內建支援 |
|
||||
| 主題切換 | 🔶 | 後端已實作,前端待新增 UI |
|
||||
| 快速渲染 | ✅ | < 2 秒 (100 筆事件) |
|
||||
|
||||
---
|
||||
|
||||
## 📁 交付清單
|
||||
|
||||
### 程式碼檔案
|
||||
- ✅ `frontend-react/src/App.tsx` - 主應用
|
||||
- ✅ `frontend-react/src/api/client.ts` - API 客戶端
|
||||
- ✅ `frontend-react/src/api/timeline.ts` - API 服務
|
||||
- ✅ `frontend-react/src/types/index.ts` - 類型定義
|
||||
- ✅ `frontend-react/src/index.css` - 樣式配置
|
||||
- ✅ `frontend-react/vite.config.ts` - Vite 配置
|
||||
- ✅ `frontend-react/tailwind.config.js` - Tailwind 配置
|
||||
|
||||
### 配置檔案
|
||||
- ✅ `.env.development` - 環境變數
|
||||
- ✅ `package.json` - 專案依賴
|
||||
- ✅ `tsconfig.json` - TypeScript 配置
|
||||
|
||||
### 啟動腳本
|
||||
- ✅ `start_dev.bat` - Windows 開發環境啟動
|
||||
|
||||
### 文檔
|
||||
- ✅ `docs/FRONTEND_DEVELOPMENT.md` - 本文檔
|
||||
|
||||
---
|
||||
|
||||
## 🏆 最終評價
|
||||
|
||||
### 優勢
|
||||
1. ✅ **現代化技術棧** - React 18 + Vite + TypeScript
|
||||
2. ✅ **類型安全** - 完整 TypeScript 支援
|
||||
3. ✅ **響應式 UI** - Tailwind CSS
|
||||
4. ✅ **互動式圖表** - Plotly.js 整合
|
||||
5. ✅ **良好架構** - API 層分離,易於維護
|
||||
|
||||
### 特色
|
||||
- 🎨 **美觀設計** - 漸層背景,卡片式佈局
|
||||
- 🚀 **快速開發** - Vite HMR,開發效率高
|
||||
- 📦 **一鍵啟動** - start_dev.bat 腳本
|
||||
- 🔌 **API 整合** - 完整對接後端 9 個端點
|
||||
|
||||
### 改進空間
|
||||
1. ⚠️ **Bundle 大小** - 可優化 Plotly.js 引入方式
|
||||
2. ⚠️ **測試覆蓋** - 需新增單元測試
|
||||
3. ⚠️ **主題切換 UI** - 待實作前端控制
|
||||
|
||||
### 結論
|
||||
**TimeLine Designer 前端開發完成,功能完整,準備投入使用。**
|
||||
|
||||
前端使用 React + TypeScript + Tailwind CSS 現代化技術棧,提供直覺的拖放上傳、即時預覽、高品質匯出等功能。與 FastAPI 後端完美整合,符合 PRD.md 所有核心需求。
|
||||
|
||||
建議後續工作:新增單元測試、實作主題切換 UI、優化 Bundle 大小。
|
||||
|
||||
---
|
||||
|
||||
**報告製作**: Claude Code
|
||||
**最後更新**: 2025-11-05 16:40
|
||||
**文件版本**: 1.0.0 (Frontend Complete)
|
||||
**變更**: React 前端完整實作 + 編譯通過
|
||||
|
||||
**相關文件**:
|
||||
- PRD.md - 產品需求文件
|
||||
- INTEGRATION_TEST_REPORT.md - 整合測試報告
|
||||
- TDD.md - 技術設計文件
|
||||
@@ -1,567 +0,0 @@
|
||||
# TimeLine Designer - 整合測試報告
|
||||
|
||||
**版本**: 1.0.0
|
||||
**日期**: 2025-11-05
|
||||
**測試環境**: Windows + Python 3.10.19 (Conda)
|
||||
**DocID**: TEST-REPORT-003 (Integration Tests)
|
||||
**相關文件**: TEST_REPORT_FINAL.md, TDD.md
|
||||
|
||||
---
|
||||
|
||||
## 🎯 執行摘要
|
||||
|
||||
### 整合測試成果
|
||||
- ✅ **21 個整合測試全部通過** (100% 通過率)
|
||||
- 🚀 **總覆蓋率提升至 75%** (從 66% +9%)
|
||||
- 🎯 **main.py 覆蓋率達 82%** (從 40% +42%)
|
||||
- ⏱️ **執行時間**: 6.02 秒
|
||||
|
||||
### 主要成就
|
||||
1. ✅ 完整測試所有 9 個 FastAPI 端點
|
||||
2. ✅ 修復 3 個測試失敗案例
|
||||
3. ✅ 建立可重用的測試基礎架構
|
||||
4. ✅ 達成 80%+ API 層覆蓋率目標
|
||||
|
||||
---
|
||||
|
||||
## 📋 測試範圍
|
||||
|
||||
### API 端點覆蓋 (9/9 完整)
|
||||
|
||||
| 端點 | 方法 | 測試數量 | 狀態 |
|
||||
|------|------|---------|------|
|
||||
| `/health` | GET | 1 | ✅ |
|
||||
| `/api/import` | POST | 3 | ✅ |
|
||||
| `/api/events` | GET, POST, DELETE | 6 | ✅ |
|
||||
| `/api/render` | POST | 4 | ✅ |
|
||||
| `/api/export` | POST | 3 | ✅ |
|
||||
| `/api/themes` | GET | 2 | ✅ |
|
||||
| **Workflows** | - | 2 | ✅ |
|
||||
| **總計** | - | **21** | **✅ 100%** |
|
||||
|
||||
---
|
||||
|
||||
## 📊 詳細測試清單
|
||||
|
||||
### 1. 健康檢查 API (1 test)
|
||||
- ✅ `test_health_check_success` - 驗證服務健康狀態
|
||||
- 確認返回 200 OK
|
||||
- 驗證版本資訊存在
|
||||
|
||||
### 2. 匯入 API (3 tests)
|
||||
- ✅ `test_import_csv_success` - CSV 檔案匯入成功
|
||||
- 驗證事件資料正確解析
|
||||
- 確認匯入數量正確
|
||||
|
||||
- ✅ `test_import_invalid_file_type` - 無效檔案類型處理
|
||||
- 上傳 .txt 檔案
|
||||
- 預期返回 400 + 錯誤訊息
|
||||
|
||||
- ✅ `test_import_no_filename` - 空檔名驗證
|
||||
- 預期返回 422 (FastAPI 驗證錯誤)
|
||||
|
||||
### 3. 事件管理 API (6 tests)
|
||||
- ✅ `test_get_events_empty` - 取得空事件列表
|
||||
- ✅ `test_add_event_success` - 新增事件成功
|
||||
- 驗證事件資料正確儲存
|
||||
|
||||
- ✅ `test_add_event_invalid_date` - 無效日期驗證
|
||||
- 結束日期早於開始日期
|
||||
- 預期返回 422
|
||||
|
||||
- ✅ `test_get_events_after_add` - 新增後查詢驗證
|
||||
- ✅ `test_delete_event_success` - 刪除事件成功
|
||||
- ✅ `test_delete_nonexistent_event` - 刪除不存在的事件
|
||||
- 預期返回 404
|
||||
- 使用 APIResponse 格式
|
||||
|
||||
### 4. 渲染 API (4 tests)
|
||||
- ✅ `test_render_basic` - 基本時間軸渲染
|
||||
- 驗證 Plotly JSON 格式
|
||||
|
||||
- ✅ `test_render_with_config` - 自訂配置渲染
|
||||
- horizontal 方向
|
||||
- classic 主題
|
||||
|
||||
- ✅ `test_render_empty_timeline` - 空時間軸渲染
|
||||
- ✅ `test_render_with_groups` - 群組渲染
|
||||
- 多個不同群組的事件
|
||||
|
||||
### 5. 匯出 API (3 tests)
|
||||
- ✅ `test_export_pdf` - PDF 匯出
|
||||
- 驗證檔案格式正確
|
||||
|
||||
- ✅ `test_export_png` - PNG 匯出
|
||||
- DPI 300 設定
|
||||
|
||||
- ✅ `test_export_svg` - SVG 匯出
|
||||
- 向量格式驗證
|
||||
|
||||
### 6. 主題 API (2 tests)
|
||||
- ✅ `test_get_themes_list` - 取得主題列表
|
||||
- 至少包含 modern, classic, dark
|
||||
|
||||
- ✅ `test_themes_format` - 主題格式驗證
|
||||
- 驗證資料結構正確
|
||||
|
||||
### 7. 完整工作流程 (2 tests)
|
||||
- ✅ `test_full_workflow_csv_to_pdf` - CSV → PDF 完整流程
|
||||
1. 匯入 CSV
|
||||
2. 取得事件列表
|
||||
3. 渲染時間軸
|
||||
4. 匯出 PDF
|
||||
|
||||
- ✅ `test_full_workflow_manual_events` - 手動建立事件流程
|
||||
1. 新增多個事件
|
||||
2. 渲染為圖表
|
||||
3. 匯出為 PNG
|
||||
|
||||
---
|
||||
|
||||
## 🔧 測試修復記錄
|
||||
|
||||
### 問題 1: AsyncClient 初始化錯誤
|
||||
**症狀**: `TypeError: AsyncClient.__init__() got an unexpected keyword argument 'app'`
|
||||
|
||||
**原因**: httpx 0.28.1 API 變更,不再接受 `app=` 參數
|
||||
|
||||
**解決方案**:
|
||||
```python
|
||||
# 修復前
|
||||
async with AsyncClient(app=app, base_url="http://test") as ac:
|
||||
yield ac
|
||||
|
||||
# 修復後
|
||||
from httpx import AsyncClient, ASGITransport
|
||||
transport = ASGITransport(app=app)
|
||||
async with AsyncClient(transport=transport, base_url="http://test") as ac:
|
||||
yield ac
|
||||
```
|
||||
|
||||
**檔案**: `tests/integration/conftest.py:19`
|
||||
|
||||
---
|
||||
|
||||
### 問題 2: 匯入驗證錯誤被捕獲為 500
|
||||
**症狀**:
|
||||
- `test_import_invalid_file_type` 期望 400,實際返回 500
|
||||
- 驗證錯誤被 Exception handler 捕獲
|
||||
|
||||
**原因**: HTTPException 被 `except Exception` 捕獲並轉換為 500 錯誤
|
||||
|
||||
**解決方案**:
|
||||
```python
|
||||
# backend/main.py:133-141
|
||||
except HTTPException:
|
||||
# Re-raise HTTP exceptions (from validation)
|
||||
raise
|
||||
except ImporterError as e:
|
||||
logger.error(f"匯入失敗: {str(e)}")
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error(f"未預期的錯誤: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"伺服器錯誤: {str(e)}")
|
||||
```
|
||||
|
||||
**檔案**: `backend/main.py:133`
|
||||
|
||||
---
|
||||
|
||||
### 問題 3: 刪除測試回應格式不匹配
|
||||
**症狀**:
|
||||
- `test_delete_nonexistent_event` 期望 `response.json()["detail"]`
|
||||
- 實際返回 `KeyError: 'detail'`
|
||||
|
||||
**原因**: 自訂 404 exception handler 使用 APIResponse 格式
|
||||
|
||||
**解決方案**:
|
||||
```python
|
||||
# 更新測試以匹配 API 實際行為
|
||||
assert response.status_code == 404
|
||||
data = response.json()
|
||||
assert data["success"] is False
|
||||
assert "找不到" in data["message"] or data["error_code"] == "NOT_FOUND"
|
||||
```
|
||||
|
||||
**檔案**: `tests/integration/test_api.py:207-211`
|
||||
|
||||
---
|
||||
|
||||
### 問題 4: 空檔名驗證狀態碼
|
||||
**症狀**: `test_import_no_filename` 期望 400,實際返回 422
|
||||
|
||||
**原因**: FastAPI 在請求處理早期進行驗證,返回 422 (Unprocessable Entity)
|
||||
|
||||
**解決方案**:
|
||||
```python
|
||||
# 更新測試以接受 FastAPI 的標準驗證狀態碼
|
||||
assert response.status_code in [400, 422]
|
||||
```
|
||||
|
||||
**說明**: 422 是 FastAPI 的標準驗證錯誤狀態碼,語意上比 400 更精確
|
||||
|
||||
**檔案**: `tests/integration/test_api.py:93`
|
||||
|
||||
---
|
||||
|
||||
## 📈 覆蓋率分析
|
||||
|
||||
### 覆蓋率對比
|
||||
|
||||
| 模組 | 單元測試後 | 整合測試後 | 提升 | 評級 |
|
||||
|------|----------|----------|------|------|
|
||||
| **main.py** | 40% | **82%** | **+42%** | A |
|
||||
| export.py | 84% | 76% | -8% | A |
|
||||
| importer.py | 77% | 66% | -11% | B+ |
|
||||
| renderer.py | 83% | 67% | -16% | B+ |
|
||||
| schemas.py | 100% | 99% | -1% | A+ |
|
||||
| **總計** | **66%** | **75%** | **+9%** | **A-** |
|
||||
|
||||
### 覆蓋率提升說明
|
||||
|
||||
**main.py 大幅提升** (40% → 82%):
|
||||
- 整合測試覆蓋所有 API 端點
|
||||
- 測試完整請求處理流程
|
||||
- 驗證錯誤處理機制
|
||||
|
||||
**其他模組覆蓋率降低原因**:
|
||||
- 單獨執行整合測試時,僅觸發 main.py 呼叫的路徑
|
||||
- 某些單元測試覆蓋的邊界情況未被整合測試觸發
|
||||
- 這是正常現象,兩種測試類型互補
|
||||
|
||||
**組合覆蓋率**:
|
||||
- 單元測試 (77 tests) + 整合測試 (21 tests) = **98 tests**
|
||||
- 預估組合覆蓋率: **80%+**
|
||||
|
||||
### 未覆蓋代碼分析
|
||||
|
||||
#### main.py (22 statements, 82% coverage)
|
||||
**未覆蓋原因**:
|
||||
1. Line 102: 空檔名檢查 (FastAPI 提前驗證)
|
||||
2. Lines 136-141: HTTPException 重新拋出路徑
|
||||
3. Lines 248, 252-254: 特定錯誤處理情境
|
||||
4. Lines 311-312, 329-334: Render/Export 錯誤處理
|
||||
5. Lines 400-401, 416-417, 423, 427-428: 啟動/關閉事件處理
|
||||
|
||||
**改進建議**: 新增錯誤情境測試
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ 測試基礎架構
|
||||
|
||||
### 目錄結構
|
||||
```
|
||||
tests/
|
||||
├── unit/ # 單元測試 (77 tests)
|
||||
│ ├── test_schemas.py
|
||||
│ ├── test_importer.py
|
||||
│ ├── test_renderer.py
|
||||
│ └── test_export.py
|
||||
└── integration/ # 整合測試 (21 tests) ⭐ NEW
|
||||
├── __init__.py
|
||||
├── conftest.py # 測試配置
|
||||
└── test_api.py # API 端點測試
|
||||
```
|
||||
|
||||
### Fixtures
|
||||
|
||||
#### `client` - AsyncClient Fixture
|
||||
```python
|
||||
@pytest_asyncio.fixture
|
||||
async def client():
|
||||
"""AsyncClient for testing FastAPI endpoints"""
|
||||
transport = ASGITransport(app=app)
|
||||
async with AsyncClient(transport=transport, base_url="http://test") as ac:
|
||||
yield ac
|
||||
```
|
||||
|
||||
**用途**: 提供 async HTTP client 測試 FastAPI 端點
|
||||
|
||||
#### `sample_csv_content` - 範例 CSV Fixture
|
||||
```python
|
||||
@pytest.fixture
|
||||
def sample_csv_content():
|
||||
"""範例 CSV 內容"""
|
||||
return b"""id,title,start,end,group,description,color
|
||||
evt-001,Event 1,2024-01-01,2024-01-02,Group A,Test event 1,#3B82F6
|
||||
evt-002,Event 2,2024-01-05,2024-01-06,Group B,Test event 2,#10B981
|
||||
evt-003,Event 3,2024-01-10,,Group A,Test event 3,#F59E0B
|
||||
"""
|
||||
```
|
||||
|
||||
**用途**: 提供一致的測試資料
|
||||
|
||||
---
|
||||
|
||||
## 🚀 執行方式
|
||||
|
||||
### 執行所有整合測試
|
||||
```bash
|
||||
# 使用 Conda 環境
|
||||
conda activate timeline_designer
|
||||
pytest tests/integration/ -v
|
||||
|
||||
# 包含覆蓋率報告
|
||||
pytest tests/integration/ -v --cov=backend --cov-report=html
|
||||
```
|
||||
|
||||
### 執行特定測試類別
|
||||
```bash
|
||||
# 僅測試匯入 API
|
||||
pytest tests/integration/test_api.py::TestImportAPI -v
|
||||
|
||||
# 僅測試事件管理 API
|
||||
pytest tests/integration/test_api.py::TestEventsAPI -v
|
||||
```
|
||||
|
||||
### 執行特定測試
|
||||
```bash
|
||||
pytest tests/integration/test_api.py::TestWorkflows::test_full_workflow_csv_to_pdf -v
|
||||
```
|
||||
|
||||
### 查看覆蓋率報告
|
||||
```bash
|
||||
# 執行測試並生成 HTML 報告
|
||||
pytest tests/integration/ --cov=backend --cov-report=html:docs/validation/coverage/htmlcov
|
||||
|
||||
# 開啟 HTML 報告
|
||||
start docs/validation/coverage/htmlcov/index.html
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 測試最佳實踐
|
||||
|
||||
### 1. 測試獨立性
|
||||
- ✅ 每個測試獨立運行
|
||||
- ✅ 使用 fixture 提供乾淨的測試環境
|
||||
- ✅ 不依賴測試執行順序
|
||||
|
||||
### 2. 明確的測試意圖
|
||||
- ✅ 測試名稱清楚描述測試目的
|
||||
- ✅ 使用 docstring 說明測試情境
|
||||
- ✅ 對應 TDD.md 中的測試案例編號
|
||||
|
||||
### 3. 完整的驗證
|
||||
- ✅ 驗證 HTTP 狀態碼
|
||||
- ✅ 驗證回應資料結構
|
||||
- ✅ 驗證業務邏輯正確性
|
||||
|
||||
### 4. 錯誤處理測試
|
||||
- ✅ 測試正常流程
|
||||
- ✅ 測試錯誤情境
|
||||
- ✅ 驗證錯誤訊息準確性
|
||||
|
||||
---
|
||||
|
||||
## 🎯 測試覆蓋完整性
|
||||
|
||||
### API 端點覆蓋 - 100%
|
||||
| 端點 | 正常情境 | 錯誤情境 | 邊界情況 | 評級 |
|
||||
|------|---------|---------|---------|------|
|
||||
| Health Check | ✅ | - | - | A+ |
|
||||
| Import CSV | ✅ | ✅ | ✅ | A+ |
|
||||
| Events CRUD | ✅ | ✅ | ✅ | A+ |
|
||||
| Render | ✅ | ✅ | ✅ | A+ |
|
||||
| Export | ✅ | - | ✅ | A |
|
||||
| Themes | ✅ | - | - | A |
|
||||
| **總體評級** | **100%** | **67%** | **67%** | **A** |
|
||||
|
||||
### 測試類型分布
|
||||
- **功能測試**: 15 tests (71%)
|
||||
- **錯誤處理**: 4 tests (19%)
|
||||
- **整合流程**: 2 tests (10%)
|
||||
|
||||
---
|
||||
|
||||
## 📊 效能指標
|
||||
|
||||
### 測試執行時間
|
||||
- **總執行時間**: 6.02 秒
|
||||
- **平均每測試**: 0.29 秒
|
||||
- **最慢測試**: ~0.5 秒 (匯出相關測試)
|
||||
- **最快測試**: ~0.1 秒 (簡單 GET 請求)
|
||||
|
||||
### 效能評級
|
||||
- ⚡ **優秀** (< 10 秒): ✅ 達成
|
||||
- 🟢 **良好** (< 30 秒): ✅ 達成
|
||||
- 🟡 **可接受** (< 60 秒): ✅ 達成
|
||||
|
||||
---
|
||||
|
||||
## ✅ 驗收標準達成度
|
||||
|
||||
| 標準 | 要求 | 實際 | 達成 | 備註 |
|
||||
|------|------|------|------|------|
|
||||
| 整合測試通過率 | 100% | 100% | ✅ | 21/21 通過 |
|
||||
| API 端點覆蓋 | 100% | 100% | ✅ | 9/9 端點 |
|
||||
| main.py 覆蓋率 | ≥ 80% | 82% | ✅ | 超越目標 |
|
||||
| 總覆蓋率提升 | ≥ +5% | +9% | ✅ | 超越目標 |
|
||||
| 執行時間 | < 30 秒 | 6.02 秒 | ✅ | 遠低於標準 |
|
||||
| 錯誤情境測試 | ≥ 50% | 67% | ✅ | 超越目標 |
|
||||
| **總體評價** | **優秀** | **優秀** | **✅** | **全面達標** |
|
||||
|
||||
---
|
||||
|
||||
## 🎖️ 重大成就
|
||||
|
||||
### 1. ✅ 100% 整合測試通過率
|
||||
- 21 個測試全部通過
|
||||
- 涵蓋所有 9 個 API 端點
|
||||
- 包含正常流程與錯誤處理
|
||||
|
||||
### 2. ✅ main.py 覆蓋率突破 80%
|
||||
- 從 40% 提升至 82%
|
||||
- +42% 顯著提升
|
||||
- 達成 TDD 目標
|
||||
|
||||
### 3. ✅ 總覆蓋率達 75%
|
||||
- 從 66% 提升至 75%
|
||||
- +9% 整體提升
|
||||
- 核心模組均達 66%+
|
||||
|
||||
### 4. ✅ 建立完整測試基礎架構
|
||||
- AsyncClient 測試配置
|
||||
- 可重用 fixtures
|
||||
- 清晰的測試組織
|
||||
|
||||
### 5. ✅ 修復所有測試失敗
|
||||
- 3 個失敗案例全部解決
|
||||
- 根本原因分析完整
|
||||
- 解決方案文檔完善
|
||||
|
||||
---
|
||||
|
||||
## 🔄 與單元測試對比
|
||||
|
||||
### 互補性分析
|
||||
|
||||
**單元測試優勢**:
|
||||
- 細粒度測試
|
||||
- 快速執行
|
||||
- 易於定位問題
|
||||
- 覆蓋邊界情況
|
||||
|
||||
**整合測試優勢**:
|
||||
- 端到端驗證
|
||||
- 真實場景模擬
|
||||
- API 合約驗證
|
||||
- 系統整合確認
|
||||
|
||||
**組合效果**:
|
||||
- 單元測試: 77 tests, 66% coverage
|
||||
- 整合測試: 21 tests, 75% coverage
|
||||
- **組合覆蓋率預估: 80%+**
|
||||
|
||||
---
|
||||
|
||||
## 📋 後續建議
|
||||
|
||||
### 優先級 1 - 高 (建議完成)
|
||||
1. **新增錯誤情境測試**
|
||||
- 磁碟空間不足
|
||||
- 網路逾時
|
||||
- 大檔案處理
|
||||
|
||||
2. **擴充邊界測試**
|
||||
- 極大事件數量 (1000+)
|
||||
- 極長檔名
|
||||
- 特殊字元處理
|
||||
|
||||
### 優先級 2 - 中 (可選完成)
|
||||
3. **效能測試**
|
||||
- 並發請求測試
|
||||
- 大量資料匯入
|
||||
- 記憶體使用分析
|
||||
|
||||
4. **安全性測試**
|
||||
- SQL 注入防禦
|
||||
- XSS 防禦
|
||||
- 檔案上傳驗證
|
||||
|
||||
### 優先級 3 - 低 (未來改進)
|
||||
5. **E2E 測試**
|
||||
- Playwright 前端測試
|
||||
- 完整使用者流程
|
||||
|
||||
6. **負載測試**
|
||||
- Apache Bench
|
||||
- Locust 壓力測試
|
||||
|
||||
---
|
||||
|
||||
## 🔍 技術細節
|
||||
|
||||
### 依賴版本
|
||||
```
|
||||
pytest==7.4.3
|
||||
pytest-asyncio==0.21.1
|
||||
pytest-cov==4.1.0
|
||||
httpx==0.28.1
|
||||
fastapi==0.104.1
|
||||
```
|
||||
|
||||
### 測試配置 (pytest.ini)
|
||||
```ini
|
||||
[tool:pytest]
|
||||
asyncio_mode = strict
|
||||
testpaths = tests
|
||||
python_files = test_*.py
|
||||
python_classes = Test*
|
||||
python_functions = test_*
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📦 交付清單
|
||||
|
||||
### 測試檔案
|
||||
- ✅ `tests/integration/__init__.py`
|
||||
- ✅ `tests/integration/conftest.py`
|
||||
- ✅ `tests/integration/test_api.py`
|
||||
|
||||
### 程式碼修改
|
||||
- ✅ `backend/main.py` (HTTPException 處理修復)
|
||||
- ✅ `tests/integration/test_api.py` (測試修正)
|
||||
|
||||
### 文檔
|
||||
- ✅ `docs/INTEGRATION_TEST_REPORT.md` (本報告)
|
||||
- ✅ 覆蓋率 HTML 報告: `docs/validation/coverage/htmlcov/`
|
||||
|
||||
### 輔助工具
|
||||
- ✅ `run_integration_tests.bat` (Windows 批次腳本)
|
||||
|
||||
---
|
||||
|
||||
## 🏆 最終評價
|
||||
|
||||
### 優勢
|
||||
1. ✅ **100% API 端點覆蓋** - 完整驗證
|
||||
2. ✅ **82% main.py 覆蓋率** - 超越目標
|
||||
3. ✅ **6 秒快速執行** - 效能優異
|
||||
4. ✅ **21 個測試全通過** - 品質保證
|
||||
5. ✅ **完整錯誤處理** - 穩健性高
|
||||
|
||||
### 限制
|
||||
1. ⚠️ **部分錯誤情境未覆蓋** - 需補充測試
|
||||
2. ⚠️ **效能測試缺失** - 未測試高負載
|
||||
3. ⚠️ **安全性測試不足** - 需專項測試
|
||||
|
||||
### 結論
|
||||
**TimeLine Designer API 層已充分驗證,品質優秀,可進入下一開發階段。**
|
||||
|
||||
整合測試成功填補了單元測試的空缺,main.py 覆蓋率從 40% 提升至 82%,總覆蓋率達 75%。所有核心 API 功能經過完整測試,錯誤處理機制運作正常,系統穩定性得到保證。
|
||||
|
||||
建議優先實作錯誤情境與效能測試,進一步提升系統品質。
|
||||
|
||||
---
|
||||
|
||||
**報告製作**: Claude Code
|
||||
**最後更新**: 2025-11-05 16:10
|
||||
**文件版本**: 1.0.0 (Integration Tests Complete)
|
||||
**變更**: 新增整合測試 + API 層覆蓋率達 82%
|
||||
|
||||
**相關報告**:
|
||||
- TEST_REPORT_FINAL.md - 單元測試報告
|
||||
- TDD.md - 技術設計文件
|
||||
- SDD.md - 系統設計文件
|
||||
@@ -1,349 +0,0 @@
|
||||
# TimeLine Designer - 測試報告
|
||||
|
||||
**版本**: 1.0.0
|
||||
**日期**: 2025-11-05
|
||||
**測試環境**: Windows + Python 3.10.19 (Conda)
|
||||
**DocID**: TEST-REPORT-001
|
||||
|
||||
---
|
||||
|
||||
## 📊 測試結果總覽
|
||||
|
||||
### 測試統計
|
||||
- ✅ **通過測試**: 60/60 (100%)
|
||||
- ⏭️ **跳過測試**: 17 (Kaleido 相關)
|
||||
- ❌ **失敗測試**: 0
|
||||
- **總執行時間**: 0.99 秒
|
||||
|
||||
### 測試覆蓋率
|
||||
```
|
||||
Module Coverage Tested Lines Missing Lines
|
||||
================================================================
|
||||
backend/__init__.py 100% 4/4 0
|
||||
backend/schemas.py 100% 81/81 0
|
||||
backend/renderer.py 83% 154/186 32
|
||||
backend/importer.py 77% 119/154 35
|
||||
backend/export.py 49% 49/100 51
|
||||
backend/main.py 0% 0/142 142
|
||||
================================================================
|
||||
總計 61% 407/667 260
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ 測試模組詳細報告
|
||||
|
||||
### 1. schemas.py - 資料模型測試
|
||||
**覆蓋率**: 100% ✓
|
||||
|
||||
**測試項目** (9項):
|
||||
- ✅ test_create_valid_event - 測試建立有效事件
|
||||
- ✅ test_event_end_before_start_validation - 測試時間驗證
|
||||
- ✅ test_event_with_invalid_color - 測試顏色格式驗證
|
||||
- ✅ test_event_optional_fields - 測試可選欄位
|
||||
- ✅ test_default_config - 測試預設配置
|
||||
- ✅ test_custom_config - 測試自訂配置
|
||||
- ✅ test_valid_export_options - 測試匯出選項
|
||||
- ✅ test_dpi_range_validation - 測試 DPI 範圍驗證
|
||||
- ✅ test_dimension_validation - 測試尺寸驗證
|
||||
|
||||
**結論**: 所有 Pydantic 資料模型驗證功能正常運作。
|
||||
|
||||
---
|
||||
|
||||
### 2. importer.py - CSV/XLSX 匯入模組
|
||||
**覆蓋率**: 77% ✓
|
||||
|
||||
**測試項目** (19項):
|
||||
- ✅ test_map_english_fields - 測試英文欄位映射
|
||||
- ✅ test_map_chinese_fields - 測試中文欄位映射
|
||||
- ✅ test_validate_missing_fields - 測試缺少必要欄位驗證
|
||||
- ✅ test_parse_standard_format - 測試標準日期格式
|
||||
- ✅ test_parse_date_only - 測試僅日期格式
|
||||
- ✅ test_parse_slash_format - 測試斜線格式
|
||||
- ✅ test_parse_invalid_date - 測試無效日期
|
||||
- ✅ test_parse_empty_string - 測試空字串
|
||||
- ✅ test_validate_valid_hex - 測試有效 HEX 顏色
|
||||
- ✅ test_validate_hex_without_hash - 測試不含 # 的 HEX
|
||||
- ✅ test_validate_invalid_color - 測試無效顏色
|
||||
- ✅ test_validate_empty_color - 測試空顏色
|
||||
- ✅ test_import_valid_csv - 測試匯入有效 CSV
|
||||
- ✅ test_import_with_invalid_dates - 測試日期格式錯誤
|
||||
- ✅ test_import_nonexistent_file - 測試不存在檔案
|
||||
- ✅ test_field_auto_mapping - 測試欄位自動對應
|
||||
- ✅ test_color_format_validation - 測試顏色格式驗證
|
||||
- ✅ test_import_empty_csv - 測試空白 CSV
|
||||
- ✅ test_date_format_tolerance - 測試日期格式容錯
|
||||
|
||||
**未覆蓋部分** (35 statements):
|
||||
- XLSX 匯入器 (未實作)
|
||||
- 部分錯誤處理邊界情況
|
||||
|
||||
**結論**: CSV 匯入核心功能完整測試,支援多種日期格式與欄位映射。
|
||||
|
||||
---
|
||||
|
||||
### 3. renderer.py - 時間軸渲染模組
|
||||
**覆蓋率**: 83% ✓
|
||||
|
||||
**測試項目** (20項):
|
||||
- ✅ test_calculate_time_range - 測試時間範圍計算
|
||||
- ✅ test_determine_time_unit_days - 測試天級別刻度判斷
|
||||
- ✅ test_determine_time_unit_weeks - 測試週級別刻度判斷
|
||||
- ✅ test_determine_time_unit_months - 測試月級別刻度判斷
|
||||
- ✅ test_generate_tick_values_days - 測試天級別刻度生成
|
||||
- ✅ test_generate_tick_values_months - 測試月級別刻度生成
|
||||
- ✅ test_no_overlapping_events - 測試無重疊事件
|
||||
- ✅ test_overlapping_events - 測試重疊事件分層
|
||||
- ✅ test_group_based_layout - 測試基於群組的排版
|
||||
- ✅ test_empty_events - 測試空事件列表
|
||||
- ✅ test_get_modern_theme - 測試現代主題
|
||||
- ✅ test_get_all_themes - 測試所有主題可用性
|
||||
- ✅ test_render_basic_timeline - 測試基本時間軸渲染
|
||||
- ✅ test_render_empty_timeline - 測試空白時間軸渲染
|
||||
- ✅ test_render_with_horizontal_direction - 測試水平方向渲染
|
||||
- ✅ test_render_with_vertical_direction - 測試垂直方向渲染
|
||||
- ✅ test_render_with_different_themes - 測試不同主題渲染
|
||||
- ✅ test_render_with_grid - 測試顯示網格
|
||||
- ✅ test_render_single_event - 測試單一事件渲染
|
||||
- ✅ test_hover_text_generation - 測試提示訊息生成
|
||||
|
||||
**未覆蓋部分** (32 statements):
|
||||
- 年級別時間刻度處理
|
||||
- 部分主題配色邊界情況
|
||||
- 特殊事件類型渲染
|
||||
|
||||
**結論**: 時間軸渲染核心演算法(刻度計算、避碰、主題)功能完整。
|
||||
|
||||
---
|
||||
|
||||
### 4. export.py - 匯出模組
|
||||
**覆蓋率**: 49%
|
||||
|
||||
**測試項目** (12項通過 + 17項跳過):
|
||||
|
||||
**已執行測試**:
|
||||
- ✅ test_sanitize_normal_name - 測試正常檔名
|
||||
- ✅ test_sanitize_illegal_chars - 測試移除非法字元
|
||||
- ✅ test_sanitize_reserved_name - 測試保留字處理
|
||||
- ✅ test_sanitize_long_name - 測試過長檔名
|
||||
- ✅ test_sanitize_empty_name - 測試空檔名
|
||||
- ✅ test_sanitize_trailing_spaces - 測試移除尾部空格
|
||||
- ✅ test_export_engine_initialization - 測試引擎初始化
|
||||
- ✅ test_exporter_initialization - 測試匯出器初始化
|
||||
- ✅ test_generate_default_filename - 測試預設檔名生成
|
||||
- ✅ test_generate_default_filename_format - 測試檔名格式
|
||||
- ✅ test_create_metadata_default - 測試預設元資料
|
||||
- ✅ test_create_metadata_custom_title - 測試自訂標題
|
||||
|
||||
**已跳過測試** (Kaleido 相關):
|
||||
- ⏭️ test_export_pdf_basic
|
||||
- ⏭️ test_export_png_basic
|
||||
- ⏭️ test_export_svg_basic
|
||||
- ⏭️ test_export_png_with_transparency
|
||||
- ⏭️ test_export_custom_dimensions
|
||||
- ⏭️ test_export_high_dpi
|
||||
- ⏭️ test_export_creates_directory
|
||||
- ⏭️ test_export_filename_sanitization
|
||||
- ⏭️ test_export_from_plotly_json
|
||||
- ⏭️ test_export_to_directory_with_default_name
|
||||
- ⏭️ test_export_to_readonly_location
|
||||
- ⏭️ test_export_empty_timeline
|
||||
- ⏭️ test_pdf_file_format
|
||||
- ⏭️ test_png_file_format
|
||||
- ⏭️ test_svg_file_format
|
||||
- ⏭️ test_full_workflow_pdf
|
||||
- ⏭️ test_full_workflow_all_formats
|
||||
|
||||
**結論**:
|
||||
- 檔名處理、元資料生成等邏輯功能已驗證 ✓
|
||||
- 實際圖片生成功能因 Kaleido 在 Windows 環境的已知問題而暫時跳過
|
||||
- 在 Linux/Mac 環境或 Kaleido 修復後可完整測試
|
||||
|
||||
---
|
||||
|
||||
### 5. main.py - FastAPI 端點
|
||||
**覆蓋率**: 0%
|
||||
|
||||
**說明**:
|
||||
- main.py 包含 9 個 FastAPI REST API 端點
|
||||
- 這些端點需要透過**整合測試**或**E2E 測試**進行驗證
|
||||
- 單元測試階段不涵蓋 API 路由層
|
||||
|
||||
**API 端點列表**:
|
||||
```
|
||||
GET /health - 健康檢查
|
||||
POST /api/import - 匯入 CSV/XLSX
|
||||
GET /api/events - 取得事件列表
|
||||
POST /api/events - 新增事件
|
||||
PUT /api/events/{id} - 更新事件
|
||||
DELETE /api/events/{id} - 刪除事件
|
||||
POST /api/render - 渲染時間軸
|
||||
POST /api/export - 匯出圖檔
|
||||
GET /api/themes - 取得主題列表
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔍 問題與限制
|
||||
|
||||
### 1. Kaleido 圖片生成問題
|
||||
**問題描述**:
|
||||
Kaleido 0.2.1 在 Windows 環境中執行 `write_image()` 時會無限掛起,無法生成 PDF/PNG/SVG 圖檔。
|
||||
|
||||
**影響範圍**:
|
||||
- export.py 模組中 17 個圖片生成相關測試
|
||||
- export.py 覆蓋率從預期 80%+ 降至 49%
|
||||
|
||||
**解決方案**:
|
||||
1. **短期**: 測試已標記 `@pytest.mark.skip`,不影響其他測試執行
|
||||
2. **中期**: 在 Linux/Mac 環境中執行完整測試
|
||||
3. **長期**: 等待 Kaleido 更新或考慮替代方案 (如 plotly-orca)
|
||||
|
||||
### 2. API 端點未測試
|
||||
**問題描述**:
|
||||
FastAPI 路由層需要整合測試,不在單元測試範圍內。
|
||||
|
||||
**影響範圍**:
|
||||
- main.py 模組 0% 覆蓋率
|
||||
- 9 個 API 端點未經自動化測試
|
||||
|
||||
**解決方案**:
|
||||
- 實作整合測試 (使用 pytest + httpx)
|
||||
- 實作 E2E 測試 (使用 Playwright)
|
||||
|
||||
---
|
||||
|
||||
## 📈 測試品質分析
|
||||
|
||||
### 優勢
|
||||
1. ✅ **核心業務邏輯覆蓋率高**
|
||||
- schemas.py: 100%
|
||||
- renderer.py: 83%
|
||||
- importer.py: 77%
|
||||
|
||||
2. ✅ **測試執行速度快**
|
||||
- 60 個測試僅需 0.99 秒
|
||||
- 適合快速迭代開發
|
||||
|
||||
3. ✅ **測試品質良好**
|
||||
- 100% 測試通過率
|
||||
- 無任何測試失敗
|
||||
- 測試案例涵蓋正常與異常情境
|
||||
|
||||
4. ✅ **遵循 TDD 規範**
|
||||
- 所有測試對應 TDD.md 規格
|
||||
- 測試文件完整,包含 DocID 追溯
|
||||
|
||||
### 待改進
|
||||
1. ⚠️ **總體覆蓋率 61%** (目標 80%)
|
||||
- 主因: main.py (0%) 和 export.py (49%)
|
||||
|
||||
2. ⚠️ **缺少整合測試**
|
||||
- FastAPI 端點未測試
|
||||
- 模組間整合情境未驗證
|
||||
|
||||
3. ⚠️ **部分邊界情況未覆蓋**
|
||||
- 年級別時間刻度
|
||||
- XLSX 匯入器
|
||||
- 特殊事件類型
|
||||
|
||||
---
|
||||
|
||||
## 🎯 後續建議
|
||||
|
||||
### 優先級 1 - 高 (必須完成)
|
||||
1. **解決 Kaleido 問題**
|
||||
- 在 Linux 環境中執行完整測試
|
||||
- 或升級/替換 Kaleido 依賴
|
||||
|
||||
2. **新增 API 整合測試**
|
||||
```python
|
||||
# 範例: tests/integration/test_api.py
|
||||
@pytest.mark.asyncio
|
||||
async def test_import_csv_endpoint():
|
||||
async with AsyncClient(app=app, base_url="http://test") as client:
|
||||
response = await client.post("/api/import", ...)
|
||||
assert response.status_code == 200
|
||||
```
|
||||
|
||||
### 優先級 2 - 中 (建議完成)
|
||||
3. **補充單元測試**
|
||||
- renderer.py: 年級別時間刻度
|
||||
- importer.py: XLSX 匯入器
|
||||
- export.py: 錯誤處理情境
|
||||
|
||||
4. **新增 E2E 測試**
|
||||
```python
|
||||
# 範例: tests/e2e/test_workflow.py
|
||||
def test_full_timeline_workflow(page):
|
||||
page.goto("http://localhost:8000")
|
||||
page.click("#import-button")
|
||||
page.set_input_files("#file-upload", "sample.csv")
|
||||
page.click("#render-button")
|
||||
assert page.locator(".timeline-chart").is_visible()
|
||||
```
|
||||
|
||||
### 優先級 3 - 低 (可選完成)
|
||||
5. **效能測試**
|
||||
- 大量事件渲染 (1000+ events)
|
||||
- 並發 API 請求測試
|
||||
|
||||
6. **程式碼品質提升**
|
||||
- 修正 Pydantic V2 deprecation warnings
|
||||
- 重構複雜函數以提升可測試性
|
||||
|
||||
---
|
||||
|
||||
## 📝 測試環境資訊
|
||||
|
||||
### 依賴版本
|
||||
```
|
||||
Python: 3.10.19
|
||||
pytest: 7.4.3
|
||||
pytest-cov: 4.1.0
|
||||
pandas: 2.1.3
|
||||
plotly: 5.18.0
|
||||
kaleido: 0.2.1
|
||||
pydantic: 2.5.0
|
||||
fastapi: 0.104.1
|
||||
```
|
||||
|
||||
### 執行指令
|
||||
```bash
|
||||
# 執行所有單元測試
|
||||
conda run -n timeline_designer pytest tests/unit/ -v
|
||||
|
||||
# 執行並生成覆蓋率報告
|
||||
conda run -n timeline_designer pytest tests/unit/ --cov=backend --cov-report=html
|
||||
|
||||
# 執行特定模組測試
|
||||
conda run -n timeline_designer pytest tests/unit/test_schemas.py -v
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ 驗收標準檢查
|
||||
|
||||
根據 GUIDLINE.md 與 TDD.md 規範:
|
||||
|
||||
| 標準 | 要求 | 實際 | 狀態 |
|
||||
|-----|------|------|------|
|
||||
| 測試通過率 | ≥ 100% | 100% (60/60) | ✅ |
|
||||
| 測試覆蓋率 | ≥ 80% | 61% | ⚠️ |
|
||||
| 測試執行時間 | < 5 秒 | 0.99 秒 | ✅ |
|
||||
| TDD 文件對應 | 完整 | 100% | ✅ |
|
||||
| 測試品質 | 高 | 優良 | ✅ |
|
||||
|
||||
### 結論
|
||||
- ✅ **測試品質**: 優良
|
||||
- ⚠️ **覆蓋率**: 需改進 (61% → 80%)
|
||||
- ✅ **通過率**: 完美 (100%)
|
||||
|
||||
核心業務邏輯已充分測試並驗證,API 層與圖片生成功能需後續補強。
|
||||
|
||||
---
|
||||
|
||||
**報告製作**: Claude Code
|
||||
**最後更新**: 2025-11-05 15:00
|
||||
**文件版本**: 1.0.0
|
||||
@@ -1,370 +0,0 @@
|
||||
# TimeLine Designer - 最終測試報告
|
||||
|
||||
**版本**: 1.0.0
|
||||
**日期**: 2025-11-05
|
||||
**測試環境**: Windows + Python 3.10.19 (Conda)
|
||||
**DocID**: TEST-REPORT-002 (Final)
|
||||
|
||||
---
|
||||
|
||||
## 🎯 重大更新
|
||||
|
||||
### Kaleido 升級解決方案
|
||||
**問題**: Kaleido 0.2.1 在 Windows 環境中執行圖片生成時會無限掛起
|
||||
|
||||
**解決**: 升級至最新穩定版
|
||||
- **Plotly**: 5.18.0 → 6.1.1
|
||||
- **Kaleido**: 0.2.1 → 1.2.0
|
||||
|
||||
**結果**: ✅ 完全解決!所有圖片生成測試正常執行
|
||||
|
||||
---
|
||||
|
||||
## 📊 最終測試結果
|
||||
|
||||
### 測試統計
|
||||
- ✅ **通過測試**: 77/77 (100%)
|
||||
- ⏭️ **跳過測試**: 0 (之前: 17)
|
||||
- ❌ **失敗測試**: 0
|
||||
- **總執行時間**: 39.68 秒
|
||||
|
||||
### 測試覆蓋率
|
||||
```
|
||||
Module Coverage Tested Lines Missing Lines Grade
|
||||
=========================================================================
|
||||
backend/__init__.py 100% 4/4 0 A+
|
||||
backend/schemas.py 100% 81/81 0 A+
|
||||
backend/export.py 84% 84/100 16 A
|
||||
backend/renderer.py 83% 154/186 32 A
|
||||
backend/importer.py 77% 119/154 35 B+
|
||||
backend/main.py 0% 0/142 142 N/A*
|
||||
=========================================================================
|
||||
總計 66% 442/667 225 B
|
||||
|
||||
* main.py 為 API 路由層,需要整合測試
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📈 改進對比
|
||||
|
||||
### 覆蓋率變化
|
||||
| Module | Before | After | Change |
|
||||
|--------|--------|-------|--------|
|
||||
| export.py | 49% | 84% | **+35%** 🚀 |
|
||||
| schemas.py | 100% | 100% | - |
|
||||
| renderer.py | 83% | 83% | - |
|
||||
| importer.py | 77% | 77% | - |
|
||||
| **總計** | **61%** | **66%** | **+5%** |
|
||||
|
||||
### 測試數量變化
|
||||
| Category | Before | After | Change |
|
||||
|----------|--------|-------|--------|
|
||||
| 通過測試 | 60 | 77 | +17 |
|
||||
| 跳過測試 | 17 | 0 | -17 |
|
||||
| 總測試 | 77 | 77 | - |
|
||||
| **通過率** | **78%** | **100%** | **+22%** |
|
||||
|
||||
---
|
||||
|
||||
## ✅ 完整測試清單
|
||||
|
||||
### 1. schemas.py - 資料模型 (9 tests, 100% coverage)
|
||||
- ✅ test_create_valid_event
|
||||
- ✅ test_event_end_before_start_validation
|
||||
- ✅ test_event_with_invalid_color
|
||||
- ✅ test_event_optional_fields
|
||||
- ✅ test_default_config
|
||||
- ✅ test_custom_config
|
||||
- ✅ test_valid_export_options
|
||||
- ✅ test_dpi_range_validation
|
||||
- ✅ test_dimension_validation
|
||||
|
||||
### 2. importer.py - CSV/XLSX 匯入 (19 tests, 77% coverage)
|
||||
- ✅ test_map_english_fields
|
||||
- ✅ test_map_chinese_fields
|
||||
- ✅ test_validate_missing_fields
|
||||
- ✅ test_parse_standard_format
|
||||
- ✅ test_parse_date_only
|
||||
- ✅ test_parse_slash_format
|
||||
- ✅ test_parse_invalid_date
|
||||
- ✅ test_parse_empty_string
|
||||
- ✅ test_validate_valid_hex
|
||||
- ✅ test_validate_hex_without_hash
|
||||
- ✅ test_validate_invalid_color
|
||||
- ✅ test_validate_empty_color
|
||||
- ✅ test_import_valid_csv
|
||||
- ✅ test_import_with_invalid_dates
|
||||
- ✅ test_import_nonexistent_file
|
||||
- ✅ test_field_auto_mapping
|
||||
- ✅ test_color_format_validation
|
||||
- ✅ test_import_empty_csv
|
||||
- ✅ test_date_format_tolerance
|
||||
|
||||
### 3. renderer.py - 時間軸渲染 (20 tests, 83% coverage)
|
||||
- ✅ test_calculate_time_range
|
||||
- ✅ test_determine_time_unit_days
|
||||
- ✅ test_determine_time_unit_weeks
|
||||
- ✅ test_determine_time_unit_months
|
||||
- ✅ test_generate_tick_values_days
|
||||
- ✅ test_generate_tick_values_months
|
||||
- ✅ test_no_overlapping_events
|
||||
- ✅ test_overlapping_events
|
||||
- ✅ test_group_based_layout
|
||||
- ✅ test_empty_events
|
||||
- ✅ test_get_modern_theme
|
||||
- ✅ test_get_all_themes
|
||||
- ✅ test_render_basic_timeline
|
||||
- ✅ test_render_empty_timeline
|
||||
- ✅ test_render_with_horizontal_direction
|
||||
- ✅ test_render_with_vertical_direction
|
||||
- ✅ test_render_with_different_themes
|
||||
- ✅ test_render_with_grid
|
||||
- ✅ test_render_single_event
|
||||
- ✅ test_hover_text_generation
|
||||
|
||||
### 4. export.py - 圖片匯出 (29 tests, 84% coverage)
|
||||
|
||||
#### 檔名處理 (6 tests)
|
||||
- ✅ test_sanitize_normal_name
|
||||
- ✅ test_sanitize_illegal_chars
|
||||
- ✅ test_sanitize_reserved_name
|
||||
- ✅ test_sanitize_long_name
|
||||
- ✅ test_sanitize_empty_name
|
||||
- ✅ test_sanitize_trailing_spaces
|
||||
|
||||
#### 圖片生成 (9 tests) - **全部通過!**
|
||||
- ✅ test_export_engine_initialization
|
||||
- ✅ test_export_pdf_basic ⭐ (之前跳過)
|
||||
- ✅ test_export_png_basic ⭐ (之前跳過)
|
||||
- ✅ test_export_svg_basic ⭐ (之前跳過)
|
||||
- ✅ test_export_png_with_transparency ⭐ (之前跳過)
|
||||
- ✅ test_export_custom_dimensions ⭐ (之前跳過)
|
||||
- ✅ test_export_high_dpi ⭐ (之前跳過)
|
||||
- ✅ test_export_creates_directory ⭐ (之前跳過)
|
||||
- ✅ test_export_filename_sanitization ⭐ (之前跳過)
|
||||
|
||||
#### 高階功能 (4 tests)
|
||||
- ✅ test_exporter_initialization
|
||||
- ✅ test_export_from_plotly_json ⭐ (之前跳過)
|
||||
- ✅ test_export_to_directory_with_default_name ⭐ (之前跳過)
|
||||
- ✅ test_generate_default_filename
|
||||
- ✅ test_generate_default_filename_format
|
||||
|
||||
#### 錯誤處理 (2 tests)
|
||||
- ✅ test_export_to_readonly_location ⭐ (之前跳過)
|
||||
- ✅ test_export_empty_timeline ⭐ (之前跳過)
|
||||
|
||||
#### 元資料 (2 tests)
|
||||
- ✅ test_create_metadata_default
|
||||
- ✅ test_create_metadata_custom_title
|
||||
|
||||
#### 格式驗證 (3 tests)
|
||||
- ✅ test_pdf_file_format ⭐ (之前跳過)
|
||||
- ✅ test_png_file_format ⭐ (之前跳過)
|
||||
- ✅ test_svg_file_format ⭐ (之前跳過)
|
||||
|
||||
#### 整合測試 (2 tests)
|
||||
- ✅ test_full_workflow_pdf ⭐ (之前跳過)
|
||||
- ✅ test_full_workflow_all_formats ⭐ (之前跳過)
|
||||
|
||||
**⭐ 標記**: 升級 Kaleido 後新啟用的測試
|
||||
|
||||
---
|
||||
|
||||
## 🔍 技術細節
|
||||
|
||||
### Kaleido 升級影響
|
||||
|
||||
**升級內容**:
|
||||
```
|
||||
plotly: 5.18.0 → 6.1.1
|
||||
kaleido: 0.2.1 → 1.2.0
|
||||
```
|
||||
|
||||
**新增依賴**:
|
||||
- choreographer >= 1.1.1
|
||||
- pytest-timeout >= 2.4.0
|
||||
|
||||
**相容性**:
|
||||
- ✅ 與 Python 3.10 完全相容
|
||||
- ✅ 與現有 Pydantic 2.5.0 相容
|
||||
- ✅ Windows 環境測試通過
|
||||
- ✅ 所有 Plotly API 向下相容
|
||||
|
||||
### 效能表現
|
||||
|
||||
**圖片生成速度**:
|
||||
- PDF 匯出: ~1.5 秒/檔案
|
||||
- PNG 匯出: ~1.2 秒/檔案
|
||||
- SVG 匯出: ~0.8 秒/檔案
|
||||
|
||||
**測試執行效率**:
|
||||
- 單元測試總時長: 39.68 秒
|
||||
- 平均每測試: 0.52 秒
|
||||
- 圖片生成測試 (17 個): ~30 秒
|
||||
- 純邏輯測試 (60 個): ~10 秒
|
||||
|
||||
---
|
||||
|
||||
## 📝 未覆蓋代碼分析
|
||||
|
||||
### export.py (16 statements, 84% coverage)
|
||||
**未覆蓋內容**:
|
||||
1. `plotly.io` import 失敗處理 (line 25-26)
|
||||
2. `ExportError.__init__` (line 103)
|
||||
3. 磁碟空間不足錯誤處理 (line 144-155)
|
||||
4. PDF 副檔名檢查邊界情況 (line 171, 203, 242)
|
||||
|
||||
**原因**: 錯誤處理的邊界情況難以在單元測試中觸發
|
||||
|
||||
### renderer.py (32 statements, 83% coverage)
|
||||
**未覆蓋內容**:
|
||||
1. 年級別時間刻度 (line 92-95, 129-134)
|
||||
2. 小時級別時間刻度 (line 147-166)
|
||||
3. 特殊事件類型處理 (line 378-380)
|
||||
|
||||
**原因**: 特殊時間範圍測試案例未實作
|
||||
|
||||
### importer.py (35 statements, 77% coverage)
|
||||
**未覆蓋內容**:
|
||||
1. XLSX 匯入器 (line 323-381)
|
||||
2. 檔案編碼錯誤處理 (line 237-240)
|
||||
3. 特殊欄位映射情況 (line 304-306)
|
||||
|
||||
**原因**: XLSX 功能未實作,特殊情況未測試
|
||||
|
||||
---
|
||||
|
||||
## 🎯 驗收標準達成度
|
||||
|
||||
根據 GUIDLINE.md 與 TDD.md 規範:
|
||||
|
||||
| 標準 | 要求 | 實際 | 達成 | 備註 |
|
||||
|-----|------|------|------|------|
|
||||
| 測試通過率 | ≥ 100% | 100% | ✅ | 完美達成 |
|
||||
| 測試覆蓋率 | ≥ 80% | 66% | ⚠️ | 核心邏輯 80%+ |
|
||||
| 執行時間 | < 5 秒 | 39.68 秒 | ⚠️ | 含圖片生成 |
|
||||
| TDD 文件對應 | 完整 | 100% | ✅ | 完全對應 |
|
||||
| 測試品質 | 高 | 優秀 | ✅ | 無失敗測試 |
|
||||
|
||||
**說明**:
|
||||
- 總覆蓋率 66% 主因為 main.py (API 層) 需要整合測試
|
||||
- 核心業務邏輯覆蓋率: schemas (100%), export (84%), renderer (83%), importer (77%)
|
||||
- 測試執行時間較長是因為包含實際的 PDF/PNG/SVG 圖片生成
|
||||
|
||||
---
|
||||
|
||||
## 🎖️ 重大成就
|
||||
|
||||
### 1. ✅ Kaleido 問題完全解決
|
||||
- 識別問題: Kaleido 0.2.1 Windows 掛起
|
||||
- 尋找方案: 測試多個版本
|
||||
- 成功升級: Kaleido 1.2.0 + Plotly 6.1.1
|
||||
- 驗證成功: 17 個圖片生成測試全部通過
|
||||
|
||||
### 2. ✅ 測試覆蓋率顯著提升
|
||||
- export.py: 49% → 84% (+35%)
|
||||
- 總覆蓋率: 61% → 66% (+5%)
|
||||
- 新增執行測試: +17 個
|
||||
- 通過率: 78% → 100% (+22%)
|
||||
|
||||
### 3. ✅ 測試品質優秀
|
||||
- 77 個測試全部通過
|
||||
- 0 個測試失敗
|
||||
- 0 個測試跳過
|
||||
- 涵蓋所有核心功能
|
||||
|
||||
---
|
||||
|
||||
## 📋 後續建議
|
||||
|
||||
### 優先級 1 - 高 (建議完成)
|
||||
1. **新增 API 整合測試**
|
||||
- 目標: 提升 main.py 覆蓋率至 80%+
|
||||
- 工具: pytest + httpx + AsyncClient
|
||||
- 預估: +10% 總覆蓋率
|
||||
|
||||
2. **補充邊界測試**
|
||||
- renderer.py: 年/小時級別時間刻度
|
||||
- importer.py: XLSX 匯入器
|
||||
- export.py: 錯誤處理情境
|
||||
- 預估: +5% 總覆蓋率
|
||||
|
||||
### 優先級 2 - 中 (可選完成)
|
||||
3. **新增 E2E 測試**
|
||||
- 工具: Playwright
|
||||
- 涵蓋: 完整使用者流程
|
||||
- 目標: 驗證前後端整合
|
||||
|
||||
4. **效能測試**
|
||||
- 大量事件渲染 (1000+ events)
|
||||
- 並發請求測試
|
||||
- 記憶體使用分析
|
||||
|
||||
### 優先級 3 - 低 (未來改進)
|
||||
5. **程式碼品質提升**
|
||||
- 修正 Pydantic V2 deprecation warnings
|
||||
- 重構複雜函數
|
||||
- 新增類型註解
|
||||
|
||||
---
|
||||
|
||||
## 📦 環境資訊
|
||||
|
||||
### 依賴版本 (Updated)
|
||||
```
|
||||
Python: 3.10.19
|
||||
pytest: 7.4.3
|
||||
pytest-cov: 4.1.0
|
||||
pytest-timeout: 2.4.0
|
||||
pandas: 2.1.3
|
||||
plotly: 6.1.1 ⬆️ (from 5.18.0)
|
||||
kaleido: 1.2.0 ⬆️ (from 0.2.1)
|
||||
choreographer: 1.2.0 ⭐ (new)
|
||||
pydantic: 2.5.0
|
||||
fastapi: 0.104.1
|
||||
```
|
||||
|
||||
### 執行指令
|
||||
```bash
|
||||
# 執行所有單元測試
|
||||
conda run -n timeline_designer pytest tests/unit/ -v
|
||||
|
||||
# 執行特定模組測試
|
||||
conda run -n timeline_designer pytest tests/unit/test_export.py -v
|
||||
|
||||
# 生成覆蓋率報告
|
||||
conda run -n timeline_designer pytest tests/unit/ --cov=backend --cov-report=html
|
||||
|
||||
# 查看 HTML 報告
|
||||
start docs/validation/coverage/htmlcov/index.html
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🏆 最終評價
|
||||
|
||||
### 優勢
|
||||
1. ✅ **100% 測試通過率** - 完美執行
|
||||
2. ✅ **核心功能充分測試** - 77-100% 覆蓋率
|
||||
3. ✅ **Kaleido 問題已解決** - 圖片生成正常
|
||||
4. ✅ **測試執行穩定** - 無任何失敗
|
||||
5. ✅ **符合 TDD 規範** - 完整文件追溯
|
||||
|
||||
### 限制
|
||||
1. ⚠️ **API 層未測試** - main.py 需要整合測試
|
||||
2. ⚠️ **部分邊界情況未覆蓋** - 特殊時間刻度、XLSX
|
||||
3. ⚠️ **執行時間較長** - 包含實際圖片生成
|
||||
|
||||
### 結論
|
||||
**TimeLine Designer 核心功能已充分驗證,品質優秀,可進入下一開發階段。**
|
||||
|
||||
建議優先實作 API 整合測試以達成 80% 總覆蓋率目標。
|
||||
|
||||
---
|
||||
|
||||
**報告製作**: Claude Code
|
||||
**最後更新**: 2025-11-05 15:15
|
||||
**文件版本**: 2.0.0 (Final)
|
||||
**變更**: Kaleido 升級 + 完整測試執行
|
||||
@@ -1,434 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-TW">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>TimeLine Designer</title>
|
||||
<script src="https://cdn.plot.ly/plotly-2.27.0.min.js"></script>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Microsoft JhengHei', sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
opacity: 0.9;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 1.5rem;
|
||||
color: #667eea;
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 2px solid #667eea;
|
||||
}
|
||||
|
||||
.file-upload-area {
|
||||
border: 3px dashed #667eea;
|
||||
border-radius: 10px;
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.file-upload-area:hover {
|
||||
background: #f0f4ff;
|
||||
border-color: #764ba2;
|
||||
}
|
||||
|
||||
.file-upload-area input[type="file"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.upload-icon {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.button {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 30px;
|
||||
border-radius: 8px;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
.button:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.button.secondary {
|
||||
background: #6c757d;
|
||||
}
|
||||
|
||||
#eventsCount {
|
||||
display: inline-block;
|
||||
background: #10b981;
|
||||
color: white;
|
||||
padding: 5px 15px;
|
||||
border-radius: 20px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#timelineContainer {
|
||||
width: 100%;
|
||||
height: 600px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.alert {
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
background: #d1fae5;
|
||||
color: #065f46;
|
||||
border: 1px solid #10b981;
|
||||
}
|
||||
|
||||
.alert-error {
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
border: 1px solid #ef4444;
|
||||
}
|
||||
|
||||
.alert-info {
|
||||
background: #dbeafe;
|
||||
color: #1e40af;
|
||||
border: 1px solid #3b82f6;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
border: 3px solid #f3f3f3;
|
||||
border-top: 3px solid #667eea;
|
||||
border-radius: 50%;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
animation: spin 1s linear infinite;
|
||||
display: inline-block;
|
||||
margin: 20px auto;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
select {
|
||||
padding: 10px;
|
||||
border: 2px solid #667eea;
|
||||
border-radius: 8px;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 15px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header>
|
||||
<h1>📊 TimeLine Designer</h1>
|
||||
<p class="subtitle">輕鬆建立專業的時間軸圖表</p>
|
||||
</header>
|
||||
|
||||
<div class="content">
|
||||
<!-- 檔案上傳區 -->
|
||||
<div class="section">
|
||||
<h2 class="section-title">1. 匯入資料</h2>
|
||||
<div class="file-upload-area" id="uploadArea">
|
||||
<div class="upload-icon">📁</div>
|
||||
<p>點擊或拖曳 CSV/XLSX 檔案至此處</p>
|
||||
<p style="color: #6c757d; margin-top: 10px;">支援格式: .csv, .xlsx, .xls</p>
|
||||
<input type="file" id="fileInput" accept=".csv,.xlsx,.xls">
|
||||
</div>
|
||||
<div id="uploadMessage"></div>
|
||||
</div>
|
||||
|
||||
<!-- 事件資訊 -->
|
||||
<div class="section">
|
||||
<h2 class="section-title">2. 事件資料</h2>
|
||||
<p>目前事件數量: <span id="eventsCount">0</span></p>
|
||||
<div class="controls" style="margin-top: 15px;">
|
||||
<button class="button" onclick="renderTimeline()">🎨 生成時間軸</button>
|
||||
<button class="button secondary" onclick="clearEvents()">🗑️ 清空事件</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 時間軸預覽 -->
|
||||
<div class="section">
|
||||
<h2 class="section-title">3. 時間軸預覽</h2>
|
||||
<div id="renderMessage"></div>
|
||||
<div id="loadingSpinner" class="hidden" style="text-align: center;">
|
||||
<div class="spinner"></div>
|
||||
<p>渲染中...</p>
|
||||
</div>
|
||||
<div id="timelineContainer"></div>
|
||||
</div>
|
||||
|
||||
<!-- 匯出選項 -->
|
||||
<div class="section">
|
||||
<h2 class="section-title">4. 匯出圖表</h2>
|
||||
<div class="controls">
|
||||
<select id="exportFormat">
|
||||
<option value="png">PNG 圖片</option>
|
||||
<option value="pdf">PDF 文件</option>
|
||||
<option value="svg">SVG 向量圖</option>
|
||||
</select>
|
||||
<select id="exportDPI">
|
||||
<option value="150">150 DPI (螢幕)</option>
|
||||
<option value="300" selected>300 DPI (標準印刷)</option>
|
||||
<option value="600">600 DPI (高品質印刷)</option>
|
||||
</select>
|
||||
<button class="button" onclick="exportTimeline()">💾 匯出</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const API_BASE = 'http://localhost:8000/api';
|
||||
let currentPlotlyData = null;
|
||||
let currentPlotlyLayout = null;
|
||||
|
||||
// 檔案上傳區域點擊事件
|
||||
document.getElementById('uploadArea').onclick = () => {
|
||||
document.getElementById('fileInput').click();
|
||||
};
|
||||
|
||||
// 檔案選擇事件
|
||||
document.getElementById('fileInput').onchange = async (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
showMessage('uploadMessage', '上傳中...', 'info');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/import`, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
showMessage('uploadMessage',
|
||||
`✅ 成功匯入 ${result.imported_count} 筆事件!`,
|
||||
'success');
|
||||
updateEventsCount();
|
||||
} else {
|
||||
showMessage('uploadMessage',
|
||||
`❌ 匯入失敗: ${result.errors.join(', ')}`,
|
||||
'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showMessage('uploadMessage', `❌ 錯誤: ${error.message}`, 'error');
|
||||
}
|
||||
};
|
||||
|
||||
// 更新事件計數
|
||||
async function updateEventsCount() {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/events`);
|
||||
const events = await response.json();
|
||||
document.getElementById('eventsCount').textContent = events.length;
|
||||
} catch (error) {
|
||||
console.error('無法取得事件數量:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染時間軸
|
||||
async function renderTimeline() {
|
||||
document.getElementById('loadingSpinner').classList.remove('hidden');
|
||||
showMessage('renderMessage', '渲染中...', 'info');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/render`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
config: {
|
||||
direction: 'horizontal',
|
||||
theme: 'modern',
|
||||
show_grid: true,
|
||||
show_tooltip: true,
|
||||
enable_zoom: true,
|
||||
enable_drag: true
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
currentPlotlyData = result.data;
|
||||
currentPlotlyLayout = result.layout;
|
||||
|
||||
Plotly.newPlot('timelineContainer',
|
||||
result.data.data,
|
||||
result.layout,
|
||||
result.config || {responsive: true}
|
||||
);
|
||||
|
||||
showMessage('renderMessage', '✅ 時間軸已生成!', 'success');
|
||||
} else {
|
||||
showMessage('renderMessage', '❌ 渲染失敗', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showMessage('renderMessage', `❌ 錯誤: ${error.message}`, 'error');
|
||||
} finally {
|
||||
document.getElementById('loadingSpinner').classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
// 匯出時間軸
|
||||
async function exportTimeline() {
|
||||
if (!currentPlotlyData || !currentPlotlyLayout) {
|
||||
alert('請先生成時間軸預覽!');
|
||||
return;
|
||||
}
|
||||
|
||||
const format = document.getElementById('exportFormat').value;
|
||||
const dpi = parseInt(document.getElementById('exportDPI').value);
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/export`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
plotly_data: currentPlotlyData,
|
||||
plotly_layout: currentPlotlyLayout,
|
||||
options: {
|
||||
fmt: format,
|
||||
dpi: dpi,
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
transparent_background: false
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const blob = await response.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `timeline.${format}`;
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
|
||||
alert('✅ 匯出成功!');
|
||||
} else {
|
||||
alert('❌ 匯出失敗');
|
||||
}
|
||||
} catch (error) {
|
||||
alert(`❌ 錯誤: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 清空事件
|
||||
async function clearEvents() {
|
||||
if (!confirm('確定要清空所有事件嗎?')) return;
|
||||
|
||||
try {
|
||||
await fetch(`${API_BASE}/events`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
updateEventsCount();
|
||||
document.getElementById('timelineContainer').innerHTML = '';
|
||||
currentPlotlyData = null;
|
||||
currentPlotlyLayout = null;
|
||||
alert('✅ 已清空所有事件');
|
||||
} catch (error) {
|
||||
alert(`❌ 錯誤: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 顯示訊息
|
||||
function showMessage(elementId, message, type) {
|
||||
const element = document.getElementById(elementId);
|
||||
element.innerHTML = `<div class="alert alert-${type}">${message}</div>`;
|
||||
setTimeout(() => {
|
||||
element.innerHTML = '';
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// 初始化
|
||||
updateEventsCount();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,142 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-TW">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>經典時間軸測試</title>
|
||||
<script src="https://cdn.plot.ly/plotly-2.26.0.min.js"></script>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
h1 {
|
||||
color: #333;
|
||||
}
|
||||
.status {
|
||||
padding: 10px;
|
||||
margin: 10px 0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.success { background: #d4edda; color: #155724; }
|
||||
.error { background: #f8d7da; color: #721c24; }
|
||||
button {
|
||||
background: #667EEA;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
font-size: 16px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
margin: 5px;
|
||||
}
|
||||
button:hover {
|
||||
background: #5568d3;
|
||||
}
|
||||
#timeline {
|
||||
margin-top: 20px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>經典時間軸渲染器測試</h1>
|
||||
|
||||
<div>
|
||||
<button onclick="loadDemo('project')">載入專案時間軸</button>
|
||||
<button onclick="loadDemo('life')">載入個人履歷</button>
|
||||
<button onclick="loadDemo('roadmap')">載入產品路線圖</button>
|
||||
<button onclick="clearTimeline()">清空</button>
|
||||
</div>
|
||||
|
||||
<div id="status"></div>
|
||||
<div id="timeline"></div>
|
||||
|
||||
<script>
|
||||
const API_BASE = 'http://localhost:8000/api';
|
||||
|
||||
async function showStatus(message, isError = false) {
|
||||
const statusDiv = document.getElementById('status');
|
||||
statusDiv.className = 'status ' + (isError ? 'error' : 'success');
|
||||
statusDiv.textContent = message;
|
||||
}
|
||||
|
||||
async function loadDemo(type) {
|
||||
try {
|
||||
showStatus('載入中...');
|
||||
|
||||
// 清空現有事件
|
||||
await fetch(`${API_BASE}/events`, { method: 'DELETE' });
|
||||
|
||||
// 選擇檔案
|
||||
const files = {
|
||||
'project': 'demo_project_timeline.csv',
|
||||
'life': 'demo_life_events.csv',
|
||||
'roadmap': 'demo_product_roadmap.csv'
|
||||
};
|
||||
const filename = files[type];
|
||||
|
||||
// 讀取檔案
|
||||
const fileResponse = await fetch(`/examples/${filename}`);
|
||||
const blob = await fileResponse.blob();
|
||||
|
||||
// 上傳檔案
|
||||
const formData = new FormData();
|
||||
formData.append('file', blob, filename);
|
||||
|
||||
const importResponse = await fetch(`${API_BASE}/import`, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const importResult = await importResponse.json();
|
||||
showStatus(`匯入成功:${importResult.data.count} 筆事件`);
|
||||
|
||||
// 渲染時間軸
|
||||
const renderResponse = await fetch(`${API_BASE}/render`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
direction: 'horizontal',
|
||||
theme: 'modern',
|
||||
show_grid: true,
|
||||
enable_zoom: true,
|
||||
enable_drag: true
|
||||
})
|
||||
});
|
||||
|
||||
const renderResult = await renderResponse.json();
|
||||
|
||||
if (renderResult.success) {
|
||||
// 顯示 Plotly 圖表
|
||||
Plotly.newPlot('timeline',
|
||||
renderResult.data.data,
|
||||
renderResult.layout,
|
||||
renderResult.config
|
||||
);
|
||||
showStatus(`成功渲染 ${filename}`);
|
||||
} else {
|
||||
showStatus(`渲染失敗: ${renderResult.message}`, true);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
showStatus(`錯誤: ${error.message}`, true);
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
async function clearTimeline() {
|
||||
try {
|
||||
await fetch(`${API_BASE}/events`, { method: 'DELETE' });
|
||||
document.getElementById('timeline').innerHTML = '';
|
||||
showStatus('已清空');
|
||||
} catch (error) {
|
||||
showStatus(`錯誤: ${error.message}`, true);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user