Files
Timeline_Generator/IMPROVEMENTS_v9.md
beabigegg 2d37d23bcf v9.5: 實作標籤完全不重疊算法
- 新增 _calculate_lane_conflicts_v2() 分開返回標籤重疊和線穿框分數
- 修改泳道選擇算法,優先選擇無標籤重疊的泳道
- 兩階段搜尋:優先側別無可用泳道則嘗試另一側
- 增強日誌輸出,顯示標籤範圍和詳細衝突分數

🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-06 11:35:29 +08:00

370 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 時間軸標籤避碰改進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