清理專案:刪除開發過程文檔和測試報告

刪除項目:
- 開發過程文檔(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:
beabigegg
2025-11-06 11:36:36 +08:00
parent 2d37d23bcf
commit 9a83efbec7
20 changed files with 0 additions and 6353 deletions

View File

@@ -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 小時含文檔
**代碼質量**: 生產就緒
**測試狀態**: 等待驗證
**恭喜完成遷移!現在您擁有專業級的時間軸標籤避讓系統!** 🚀

View File

@@ -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/` 目錄
---
**報告結束 - 專案開發完成!** 🎉

View File

@@ -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 檢核 GateCI 強制)
CI 中新增 **Doc-Change Gate**
- PR 變更程式碼但未引用相關 `DocID` **阻擋合併**
- 若新建文檔但 `Pre-Create Check` 證據不足 **阻擋合併**
- 檢查 `REGISTRY.md` 是否同步更新
- 檢查 `Changelog` 是否新增
- 檢查 `Version` 是否依規則遞增fix: patchfeat: minorbreak: major)。
### 8.7 追溯鏈Traceability Chain
- **需求 設計 程式碼 測試 交付** 全鏈路需以 `DocID` `Commit/PR` 互相鏈結
- 需求PRD 條款編號
- 設計SDD 模組節點
- 程式碼目錄/檔案與函式註解
- 測試TDD Case ID
- 交付產物效能與覆蓋率報告
### 8.8 最佳實務
- **改寫優先**小幅調整以段落更新避免碎片化文件
- **章節化**若更新內容較大優先在現有文檔開新章保留連貫脈絡
- **變更影響矩陣**變更單中列出受影響的模組API測試與文件 DocID
- **審核清單**Reviewer 需檢查 `Pre-Create Check``SSOT` 鏈接與 `Changelog` 完整性

View File

@@ -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. 添加標籤碰撞預覽功能

View File

@@ -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.02D 避碰)
- ✅ 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

View File

@@ -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.02D 避碰)
- ✅ 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 | 改進方法 |
|-----|------|------|------|---------|
| 文字框重疊 | 已解決 | 已解決 | 已解決 | 增加間距與安全邊距 |
| 線條交錯 | 嚴重 | 仍存在 | 最小化 | 鏡像分布 + 距離感知 |
| 線條穿框 | 經常 | 偶爾 | 極少 | 距離感知動態調整 |
| 視覺清晰度 | 中等 | 良好 | 優秀 | 多層次優化 |
| 配置靈活性 | 可調 | 高度可調 | 智能自適應 | 動態參數計算 |
| 層級分布 | 單向 | 單向 | 鏡像 | 上下/左右對稱策略 |
| 距離處理 | 固定 | 固定 | 動態 | 根據跨越距離調整 |

View File

@@ -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**:測試所有可能路徑 → **保證選擇最佳路徑**
這是從**被動避讓**到**主動檢測**的質的飛躍! 🚀

View File

@@ -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% 可靠 > 複雜但不可靠"
**從碰撞檢測到泳道分配,這是一次質的飛躍!** 🚀

View File

@@ -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.0Scatter 方式)**
```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.0Shape 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, Ldatetime 對象無法正確解析
- 導致連接線完全不顯示
**修復方案**
改用 `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% 保證線條不交錯
- 視覺整潔專業

View File

@@ -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 分段渲染
- 添加動態優化層
**從固定規則到自適應優化,這是布局算法的質的飛躍!** 🚀

View File

@@ -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

View File

@@ -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
View File

@@ -1,72 +0,0 @@
# 📗 System Design Document (SDD)
## 1. 架構概述
```
PyWebview Host
├── FastAPI Backend
│ ├── importer.pyCSV/XLSX 處理)
│ ├── renderer.pyPlotly/kaleido 渲染)
│ ├── schemas.py資料模型定義
│ └── export.pyPDF/SVG/PNG 輸出)
└── Frontend (React + Tailwind)
├── TimelineCanvasvis-timeline 封裝)
├── EventForm / ThemePanel / ExportDialog
└── api.tsAPI 呼叫)
```
## 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
View File

@@ -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

View File

@@ -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 - 技術設計文件

View File

@@ -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 - 系統設計文件

View File

@@ -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

View File

@@ -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 升級 + 完整測試執行

View File

@@ -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>

View File

@@ -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>