後端代碼清理:移除冗餘註解和調試代碼
清理內容: - 移除所有開發元資訊(Author, Version, DocID, Rationale等) - 刪除註解掉的代碼片段(力導向演算法等24行) - 移除調試用的 logger.debug 語句 - 簡化冗餘的內聯註解(emoji、"重要"等標註) - 刪除 TDD 文件引用 清理檔案: - backend/main.py - 移除調試日誌和元資訊 - backend/importer.py - 移除詳細類型檢查調試 - backend/export.py - 簡化 docstring - backend/schemas.py - 移除元資訊 - backend/renderer.py - 移除 TDD 引用 - backend/renderer_timeline.py - 移除註解代碼和冗餘註解 - backend/path_planner.py - 簡化策略註解 保留內容: - 所有函數的 docstring(功能說明、參數、返回值) - 必要的業務邏輯註解 - 簡潔的模組功能說明 效果: - 刪除約 100+ 行冗餘註解 - 代碼更加簡潔專業 - 功能完整性 100% 保留 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -3,12 +3,6 @@
|
|||||||
|
|
||||||
本模組負責將時間軸圖表匯出為各種格式(PDF、PNG、SVG)。
|
本模組負責將時間軸圖表匯出為各種格式(PDF、PNG、SVG)。
|
||||||
使用 Plotly 的 kaleido 引擎進行圖片生成。
|
使用 Plotly 的 kaleido 引擎進行圖片生成。
|
||||||
|
|
||||||
Author: AI Agent
|
|
||||||
Version: 1.0.0
|
|
||||||
DocID: SDD-EXP-001
|
|
||||||
Related: TDD-UT-EXP-001
|
|
||||||
Rationale: 實現 SDD.md 定義的 POST /export API 功能
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
@@ -95,7 +89,6 @@ class ExportEngine:
|
|||||||
匯出引擎
|
匯出引擎
|
||||||
|
|
||||||
負責將 Plotly 圖表匯出為不同格式的檔案。
|
負責將 Plotly 圖表匯出為不同格式的檔案。
|
||||||
對應 TDD.md - UT-EXP-01
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
|||||||
@@ -3,12 +3,6 @@ CSV/XLSX 匯入模組
|
|||||||
|
|
||||||
本模組負責處理時間軸事件的資料匯入。
|
本模組負責處理時間軸事件的資料匯入。
|
||||||
支援 CSV 和 XLSX 格式,包含欄位自動對應與格式容錯功能。
|
支援 CSV 和 XLSX 格式,包含欄位自動對應與格式容錯功能。
|
||||||
|
|
||||||
Author: AI Agent
|
|
||||||
Version: 1.0.0
|
|
||||||
DocID: SDD-IMP-001
|
|
||||||
Related: TDD-UT-IMP-001
|
|
||||||
Rationale: 實現 SDD.md 定義的 POST /import API 功能
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import csv
|
import csv
|
||||||
@@ -441,21 +435,14 @@ class CSVImporter:
|
|||||||
return str(int(value))
|
return str(int(value))
|
||||||
return str(value).strip()
|
return str(value).strip()
|
||||||
|
|
||||||
# 🔍 DEBUG: 顯示原始 row 和 field_mapping
|
|
||||||
logger.debug(f" Row keys: {list(row.keys())}")
|
|
||||||
logger.debug(f" Field mapping: {field_mapping}")
|
|
||||||
|
|
||||||
# 提取欄位值
|
# 提取欄位值
|
||||||
event_id = safe_str(row.get(field_mapping['id'], ''))
|
event_id = safe_str(row.get(field_mapping['id'], ''))
|
||||||
title = safe_str(row.get(field_mapping['title'], ''))
|
title = safe_str(row.get(field_mapping['title'], ''))
|
||||||
start_str = safe_str(row.get(field_mapping['start'], '')) # 🔧 修復:也要使用 safe_str 轉換
|
start_str = safe_str(row.get(field_mapping['start'], ''))
|
||||||
group = safe_str(row.get(field_mapping.get('group', ''), '')) or None
|
group = safe_str(row.get(field_mapping.get('group', ''), '')) or None
|
||||||
description = safe_str(row.get(field_mapping.get('description', ''), '')) or None
|
description = safe_str(row.get(field_mapping.get('description', ''), '')) or None
|
||||||
color = safe_str(row.get(field_mapping.get('color', ''), ''))
|
color = safe_str(row.get(field_mapping.get('color', ''), ''))
|
||||||
|
|
||||||
# 🔍 DEBUG: 顯示提取的欄位值
|
|
||||||
logger.debug(f" 提取欄位 - ID: '{event_id}', 標題: '{title}', 時間: '{start_str}'")
|
|
||||||
|
|
||||||
# 驗證必要欄位
|
# 驗證必要欄位
|
||||||
if not event_id or not title:
|
if not event_id or not title:
|
||||||
raise ValueError("缺少 ID 或標題")
|
raise ValueError("缺少 ID 或標題")
|
||||||
@@ -468,19 +455,19 @@ class CSVImporter:
|
|||||||
if not start:
|
if not start:
|
||||||
raise ValueError(f"無效的時間: {start_str}")
|
raise ValueError(f"無效的時間: {start_str}")
|
||||||
|
|
||||||
# 🔧 修復:將 pandas Timestamp 轉換為標準 datetime
|
# 將 pandas Timestamp 轉換為標準 datetime
|
||||||
if PANDAS_AVAILABLE:
|
if PANDAS_AVAILABLE:
|
||||||
if isinstance(start, pd.Timestamp):
|
if isinstance(start, pd.Timestamp):
|
||||||
start = start.to_pydatetime()
|
start = start.to_pydatetime()
|
||||||
|
|
||||||
# 驗證顏色(確保返回的是字串,不是 None)
|
# 驗證顏色
|
||||||
color = self.color_validator.validate(color, int(row_num))
|
color = self.color_validator.validate(color, int(row_num))
|
||||||
if not color: # 防禦性檢查
|
if not color:
|
||||||
color = self.color_validator.DEFAULT_COLORS[0]
|
color = self.color_validator.DEFAULT_COLORS[0]
|
||||||
|
|
||||||
# 所有事件都是時間點類型(不再有區間)
|
# 所有事件都是時間點類型
|
||||||
event_type = EventType.POINT
|
event_type = EventType.POINT
|
||||||
end = None # 不再使用 end 欄位
|
end = None
|
||||||
|
|
||||||
# 建立 Event 物件
|
# 建立 Event 物件
|
||||||
try:
|
try:
|
||||||
@@ -494,22 +481,9 @@ class CSVImporter:
|
|||||||
color=color,
|
color=color,
|
||||||
event_type=event_type
|
event_type=event_type
|
||||||
)
|
)
|
||||||
# 調試:確認所有欄位類型
|
|
||||||
logger.debug(f"Event 創建成功: id={type(event.id).__name__}, title={type(event.title).__name__}, "
|
|
||||||
f"start={type(event.start).__name__}, end={type(event.end).__name__ if event.end else 'None'}, "
|
|
||||||
f"group={type(event.group).__name__ if event.group else 'None'}, "
|
|
||||||
f"description={type(event.description).__name__ if event.description else 'None'}, "
|
|
||||||
f"color={type(event.color).__name__}")
|
|
||||||
return event
|
return event
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"創建 Event 失敗: {str(e)}")
|
logger.error(f"創建 Event 失敗: {str(e)}")
|
||||||
logger.error(f" id={event_id} ({type(event_id).__name__})")
|
|
||||||
logger.error(f" title={title} ({type(title).__name__})")
|
|
||||||
logger.error(f" start={start} ({type(start).__name__})")
|
|
||||||
logger.error(f" end={end} ({type(end).__name__ if end else 'None'})")
|
|
||||||
logger.error(f" group={group} ({type(group).__name__ if group else 'None'})")
|
|
||||||
logger.error(f" description={description} ({type(description).__name__ if description else 'None'})")
|
|
||||||
logger.error(f" color={color} ({type(color).__name__})")
|
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,12 +2,6 @@
|
|||||||
FastAPI 主程式
|
FastAPI 主程式
|
||||||
|
|
||||||
本模組提供時間軸設計工具的 REST API 服務。
|
本模組提供時間軸設計工具的 REST API 服務。
|
||||||
遵循 SDD.md 定義的 API 規範。
|
|
||||||
|
|
||||||
Author: AI Agent
|
|
||||||
Version: 1.0.0
|
|
||||||
DocID: SDD-API-001
|
|
||||||
Rationale: 實現 SDD.md 第3節定義的 API 接口
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
@@ -124,14 +118,6 @@ async def import_events(file: UploadFile = File(...)):
|
|||||||
events_store = result.events
|
events_store = result.events
|
||||||
logger.info(f"成功匯入 {result.imported_count} 筆事件")
|
logger.info(f"成功匯入 {result.imported_count} 筆事件")
|
||||||
|
|
||||||
# 🔍 調試:檢查 result 的所有欄位類型
|
|
||||||
logger.debug(f"ImportResult 類型檢查:")
|
|
||||||
logger.debug(f" success: {type(result.success).__name__}")
|
|
||||||
logger.debug(f" total_rows: {type(result.total_rows).__name__} = {result.total_rows}")
|
|
||||||
logger.debug(f" imported_count: {type(result.imported_count).__name__} = {result.imported_count}")
|
|
||||||
logger.debug(f" events count: {len(result.events)}")
|
|
||||||
logger.debug(f" errors count: {len(result.errors)}")
|
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
|
|||||||
@@ -3,9 +3,6 @@
|
|||||||
|
|
||||||
使用BFS算法在網格化的繪圖區域中為連接線尋找最佳路徑,
|
使用BFS算法在網格化的繪圖區域中為連接線尋找最佳路徑,
|
||||||
完全避開標籤障礙物。
|
完全避開標籤障礙物。
|
||||||
|
|
||||||
Author: AI Agent
|
|
||||||
Version: 1.0.0
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
@@ -151,49 +148,35 @@ class GridMap:
|
|||||||
path_points: 路徑點列表 [(datetime, y), ...]
|
path_points: 路徑點列表 [(datetime, y), ...]
|
||||||
width_expansion: 寬度擴展倍數
|
width_expansion: 寬度擴展倍數
|
||||||
|
|
||||||
策略:
|
|
||||||
1. 標記所有線段(包括起點線段)
|
|
||||||
2. 但是起點線段只標記離開時間軸的垂直部分
|
|
||||||
3. 時間軸 y=0 本身不標記,避免阻擋其他起點
|
|
||||||
"""
|
"""
|
||||||
if len(path_points) < 2:
|
if len(path_points) < 2:
|
||||||
return
|
return
|
||||||
|
|
||||||
# 標記所有線段
|
|
||||||
for i in range(len(path_points) - 1):
|
for i in range(len(path_points) - 1):
|
||||||
dt1, y1 = path_points[i]
|
dt1, y1 = path_points[i]
|
||||||
dt2, y2 = path_points[i + 1]
|
dt2, y2 = path_points[i + 1]
|
||||||
|
|
||||||
# 如果是從時間軸(y=0)出發的第一段線段
|
|
||||||
if i == 0 and abs(y1) < 0.1:
|
if i == 0 and abs(y1) < 0.1:
|
||||||
# 只標記離開時間軸的部分(從 y=0.2 開始)
|
if abs(y2) > 0.2:
|
||||||
# 避免阻擋其他事件的起點
|
|
||||||
if abs(y2) > 0.2: # 確保終點不在時間軸上
|
|
||||||
# 使用線性插值找到 y=0.2 的點
|
|
||||||
if abs(y2 - y1) > 0.01:
|
if abs(y2 - y1) > 0.01:
|
||||||
t = (0.2 - y1) / (y2 - y1) if y2 > y1 else (-0.2 - y1) / (y2 - y1)
|
t = (0.2 - y1) / (y2 - y1) if y2 > y1 else (-0.2 - y1) / (y2 - y1)
|
||||||
if 0 < t < 1:
|
if 0 < t < 1:
|
||||||
# 計算 y=0.2 時的 datetime
|
|
||||||
seconds_offset = (dt2 - dt1).total_seconds() * t
|
seconds_offset = (dt2 - dt1).total_seconds() * t
|
||||||
dt_cutoff = dt1 + timedelta(seconds=seconds_offset)
|
dt_cutoff = dt1 + timedelta(seconds=seconds_offset)
|
||||||
y_cutoff = 0.2 if y2 > 0 else -0.2
|
y_cutoff = 0.2 if y2 > 0 else -0.2
|
||||||
|
|
||||||
# 只標記從 cutoff 點到終點的部分
|
|
||||||
col1 = self.datetime_to_grid_x(dt_cutoff)
|
col1 = self.datetime_to_grid_x(dt_cutoff)
|
||||||
row1 = self.y_to_grid_y(y_cutoff)
|
row1 = self.y_to_grid_y(y_cutoff)
|
||||||
col2 = self.datetime_to_grid_x(dt2)
|
col2 = self.datetime_to_grid_x(dt2)
|
||||||
row2 = self.y_to_grid_y(y2)
|
row2 = self.y_to_grid_y(y2)
|
||||||
self._mark_line(row1, col1, row2, col2, int(width_expansion))
|
self._mark_line(row1, col1, row2, col2, int(width_expansion))
|
||||||
else:
|
else:
|
||||||
# t 不在範圍內,標記整段
|
|
||||||
col1 = self.datetime_to_grid_x(dt1)
|
col1 = self.datetime_to_grid_x(dt1)
|
||||||
row1 = self.y_to_grid_y(y1)
|
row1 = self.y_to_grid_y(y1)
|
||||||
col2 = self.datetime_to_grid_x(dt2)
|
col2 = self.datetime_to_grid_x(dt2)
|
||||||
row2 = self.y_to_grid_y(y2)
|
row2 = self.y_to_grid_y(y2)
|
||||||
self._mark_line(row1, col1, row2, col2, int(width_expansion))
|
self._mark_line(row1, col1, row2, col2, int(width_expansion))
|
||||||
# 如果終點也在時間軸上,不標記
|
|
||||||
else:
|
else:
|
||||||
# 非起點線段,全部標記
|
|
||||||
col1 = self.datetime_to_grid_x(dt1)
|
col1 = self.datetime_to_grid_x(dt1)
|
||||||
row1 = self.y_to_grid_y(y1)
|
row1 = self.y_to_grid_y(y1)
|
||||||
col2 = self.datetime_to_grid_x(dt2)
|
col2 = self.datetime_to_grid_x(dt2)
|
||||||
@@ -308,15 +291,10 @@ def find_path_bfs(
|
|||||||
end_row: int,
|
end_row: int,
|
||||||
end_col: int,
|
end_col: int,
|
||||||
grid_map: GridMap,
|
grid_map: GridMap,
|
||||||
direction_constraint: str = "up" # "up" or "down"
|
direction_constraint: str = "up"
|
||||||
) -> Optional[List[Tuple[int, int]]]:
|
) -> Optional[List[Tuple[int, int]]]:
|
||||||
"""
|
"""
|
||||||
使用BFS尋找路徑(改進版:優先離開時間軸)
|
使用BFS尋找路徑
|
||||||
|
|
||||||
策略:
|
|
||||||
1. 優先垂直移動(離開時間軸)
|
|
||||||
2. 遇到障礙物才水平繞行
|
|
||||||
3. 使用優先隊列,根據與時間軸的距離排序
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
start_row, start_col: 起點網格座標
|
start_row, start_col: 起點網格座標
|
||||||
@@ -338,23 +316,16 @@ def find_path_bfs(
|
|||||||
|
|
||||||
import heapq
|
import heapq
|
||||||
|
|
||||||
# 計算時間軸的Y座標(row)
|
|
||||||
timeline_row = grid_map.y_to_grid_y(0)
|
timeline_row = grid_map.y_to_grid_y(0)
|
||||||
|
|
||||||
# 優先隊列:(優先度, row, col, path)
|
|
||||||
# 優先度 = 與時間軸的距離(越遠越好)+ 路徑長度(越短越好)
|
|
||||||
start_priority = 0
|
start_priority = 0
|
||||||
heap = [(start_priority, start_row, start_col, [(start_row, start_col)])]
|
heap = [(start_priority, start_row, start_col, [(start_row, start_col)])]
|
||||||
visited = set()
|
visited = set()
|
||||||
visited.add((start_row, start_col))
|
visited.add((start_row, start_col))
|
||||||
|
|
||||||
# 方向優先順序(垂直優先於水平)
|
|
||||||
if direction_constraint == "up":
|
if direction_constraint == "up":
|
||||||
# 優先往上,然後才左右
|
directions = [(-1, 0), (0, 1), (0, -1)]
|
||||||
directions = [(-1, 0), (0, 1), (0, -1)] # 上、右、左
|
else:
|
||||||
else: # "down"
|
directions = [(1, 0), (0, 1), (0, -1)]
|
||||||
# 優先往下,然後才左右
|
|
||||||
directions = [(1, 0), (0, 1), (0, -1)] # 下、右、左
|
|
||||||
|
|
||||||
max_iterations = grid_map.grid_rows * grid_map.grid_cols * 2
|
max_iterations = grid_map.grid_rows * grid_map.grid_cols * 2
|
||||||
iterations = 0
|
iterations = 0
|
||||||
@@ -363,42 +334,30 @@ def find_path_bfs(
|
|||||||
iterations += 1
|
iterations += 1
|
||||||
_, current_row, current_col, path = heapq.heappop(heap)
|
_, current_row, current_col, path = heapq.heappop(heap)
|
||||||
|
|
||||||
# 到達終點
|
|
||||||
if current_row == end_row and current_col == end_col:
|
if current_row == end_row and current_col == end_col:
|
||||||
logger.info(f"找到路徑,長度: {len(path)},迭代: {iterations}")
|
logger.info(f"找到路徑,長度: {len(path)},迭代: {iterations}")
|
||||||
return path
|
return path
|
||||||
|
|
||||||
# 探索鄰居(按優先順序)
|
|
||||||
for d_row, d_col in directions:
|
for d_row, d_col in directions:
|
||||||
next_row = current_row + d_row
|
next_row = current_row + d_row
|
||||||
next_col = current_col + d_col
|
next_col = current_col + d_col
|
||||||
|
|
||||||
# 檢查是否可通行
|
|
||||||
if (next_row, next_col) in visited:
|
if (next_row, next_col) in visited:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if not grid_map.is_free(next_row, next_col):
|
if not grid_map.is_free(next_row, next_col):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 計算優先度
|
|
||||||
# 1. 與時間軸的距離(主要因素)
|
|
||||||
distance_from_timeline = abs(next_row - timeline_row)
|
distance_from_timeline = abs(next_row - timeline_row)
|
||||||
|
|
||||||
# 2. 曼哈頓距離到終點(次要因素)
|
|
||||||
manhattan_to_goal = abs(next_row - end_row) + abs(next_col - end_col)
|
manhattan_to_goal = abs(next_row - end_row) + abs(next_col - end_col)
|
||||||
|
|
||||||
# 3. 路徑長度(避免繞太遠)
|
|
||||||
path_length = len(path)
|
path_length = len(path)
|
||||||
|
|
||||||
# 綜合優先度:離時間軸越遠越好,離目標越近越好
|
|
||||||
# 權重調整:優先離開時間軸
|
|
||||||
priority = (
|
priority = (
|
||||||
-distance_from_timeline * 100 + # 負數因為要最大化
|
-distance_from_timeline * 100 +
|
||||||
manhattan_to_goal * 10 +
|
manhattan_to_goal * 10 +
|
||||||
path_length
|
path_length
|
||||||
)
|
)
|
||||||
|
|
||||||
# 添加到優先隊列
|
|
||||||
visited.add((next_row, next_col))
|
visited.add((next_row, next_col))
|
||||||
new_path = path + [(next_row, next_col)]
|
new_path = path + [(next_row, next_col)]
|
||||||
heapq.heappush(heap, (priority, next_row, next_col, new_path))
|
heapq.heappush(heap, (priority, next_row, next_col, new_path))
|
||||||
|
|||||||
@@ -3,12 +3,6 @@
|
|||||||
|
|
||||||
本模組負責將事件資料轉換為視覺化的時間軸圖表。
|
本模組負責將事件資料轉換為視覺化的時間軸圖表。
|
||||||
使用 Plotly 進行渲染,支援時間刻度自動調整與節點避碰。
|
使用 Plotly 進行渲染,支援時間刻度自動調整與節點避碰。
|
||||||
|
|
||||||
Author: AI Agent
|
|
||||||
Version: 1.0.0
|
|
||||||
DocID: SDD-REN-001
|
|
||||||
Related: TDD-UT-REN-001, TDD-UT-REN-002
|
|
||||||
Rationale: 實現 SDD.md 定義的 POST /render API 功能
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
@@ -36,7 +30,6 @@ class TimeScaleCalculator:
|
|||||||
時間刻度計算器
|
時間刻度計算器
|
||||||
|
|
||||||
根據事件的時間跨度自動選擇最適合的刻度單位與間隔。
|
根據事件的時間跨度自動選擇最適合的刻度單位與間隔。
|
||||||
對應 TDD.md - UT-REN-01
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -173,7 +166,6 @@ class CollisionResolver:
|
|||||||
節點避碰解析器
|
節點避碰解析器
|
||||||
|
|
||||||
處理時間軸上重疊事件的排版,確保事件不會相互覆蓋。
|
處理時間軸上重疊事件的排版,確保事件不會相互覆蓋。
|
||||||
對應 TDD.md - UT-REN-02
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, min_spacing: int = 10):
|
def __init__(self, min_spacing: int = 10):
|
||||||
|
|||||||
@@ -6,9 +6,6 @@
|
|||||||
- 事件點標記
|
- 事件點標記
|
||||||
- 交錯的文字標註
|
- 交錯的文字標註
|
||||||
- 連接線
|
- 連接線
|
||||||
|
|
||||||
Author: AI Agent
|
|
||||||
Version: 2.0.0
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
@@ -663,17 +660,6 @@ class ClassicTimelineRenderer:
|
|||||||
line_through_box_score += 100.0
|
line_through_box_score += 100.0
|
||||||
|
|
||||||
return overlap_score, line_through_box_score
|
return overlap_score, line_through_box_score
|
||||||
# for other_lane_idx in range(7):
|
|
||||||
# if other_lane_idx == lane_idx:
|
|
||||||
# continue
|
|
||||||
# for occupied in occupied_lanes[other_lane_idx]:
|
|
||||||
# if not (label_end < occupied['start'] or label_start > occupied['end']):
|
|
||||||
# same_side = (current_label_y > 0 and occupied['label_y'] > 0) or \
|
|
||||||
# (current_label_y < 0 and occupied['label_y'] < 0)
|
|
||||||
# if not same_side:
|
|
||||||
# score += 1.0 # 交叉權重(已禁用)
|
|
||||||
|
|
||||||
return score
|
|
||||||
|
|
||||||
def _check_line_intersects_textbox(
|
def _check_line_intersects_textbox(
|
||||||
self,
|
self,
|
||||||
@@ -810,18 +796,17 @@ class ClassicTimelineRenderer:
|
|||||||
line_x2_ts = label_x.timestamp()
|
line_x2_ts = label_x.timestamp()
|
||||||
line_y2 = label_y
|
line_y2 = label_y
|
||||||
|
|
||||||
# 🔍 判斷是否為垂直線(label_x == event_x)
|
# 判斷是否為垂直線
|
||||||
is_vertical_line = abs(line_x2_ts - line_x1_ts) < 1e-6
|
is_vertical_line = abs(line_x2_ts - line_x1_ts) < 1e-6
|
||||||
|
|
||||||
# 檢查是否與其他標籤相交
|
# 檢查是否與其他標籤相交
|
||||||
line_blocked = False
|
line_blocked = False
|
||||||
blocking_labels = []
|
blocking_labels = []
|
||||||
|
|
||||||
# ⚠️ 對於垂直線(x_offset=0),跳過碰撞檢測
|
# 垂直線跳過碰撞檢測
|
||||||
# 原因:固定泳道算法已確保標籤本身不重疊,垂直線無法避開其他標籤
|
|
||||||
if is_vertical_line:
|
if is_vertical_line:
|
||||||
logger.debug(f" 🔹 '{title}' 是垂直線,跳過碰撞檢測,直接繪製")
|
logger.debug(f" '{title}' 是垂直線,跳過碰撞檢測")
|
||||||
line_blocked = False # 強制不使用BFS
|
line_blocked = False
|
||||||
|
|
||||||
# 只對非垂直線進行碰撞檢測
|
# 只對非垂直線進行碰撞檢測
|
||||||
for j, other in enumerate(sorted_markers) if not is_vertical_line else []:
|
for j, other in enumerate(sorted_markers) if not is_vertical_line else []:
|
||||||
@@ -838,36 +823,23 @@ class ClassicTimelineRenderer:
|
|||||||
other_top = other_y + label_height / 2
|
other_top = other_y + label_height / 2
|
||||||
other_bottom = other_y - label_height / 2
|
other_bottom = other_y - label_height / 2
|
||||||
|
|
||||||
# 🔍 DEBUG: 記錄檢測的標籤詳情
|
|
||||||
logger.debug(f" 檢查 {title} vs {other_title}:")
|
|
||||||
logger.debug(f" {title} 線段: X1={datetime.fromtimestamp(line_x1_ts)}, Y1={line_y1:.2f} -> X2={datetime.fromtimestamp(line_x2_ts)}, Y2={line_y2:.2f}")
|
|
||||||
logger.debug(f" {other_title} 標籤: X=[{datetime.fromtimestamp(other_left)} ~ {datetime.fromtimestamp(other_right)}], Y=[{other_bottom:.2f} ~ {other_top:.2f}]")
|
|
||||||
logger.debug(f" {other_title} 泳道: {other.get('swim_lane', '?')}")
|
|
||||||
|
|
||||||
# 檢測線段與矩形的相交
|
# 檢測線段與矩形的相交
|
||||||
# 1. 首先檢查X範圍是否重疊
|
# 1. 首先檢查X範圍是否重疊
|
||||||
line_x_min = min(line_x1_ts, line_x2_ts)
|
line_x_min = min(line_x1_ts, line_x2_ts)
|
||||||
line_x_max = max(line_x1_ts, line_x2_ts)
|
line_x_max = max(line_x1_ts, line_x2_ts)
|
||||||
|
|
||||||
if line_x_max < other_left or line_x_min > other_right:
|
if line_x_max < other_left or line_x_min > other_right:
|
||||||
logger.debug(f" ✓ X範圍不重疊,跳過")
|
continue
|
||||||
continue # X範圍不重疊,不可能相交
|
|
||||||
|
|
||||||
# 2. 計算線段在標籤X範圍內的Y值
|
# 2. 計算線段在標籤X範圍內的Y值
|
||||||
# 使用線性插值:y = y1 + (x - x1) * (y2 - y1) / (x2 - x1)
|
|
||||||
if abs(line_x2_ts - line_x1_ts) < 1e-6:
|
if abs(line_x2_ts - line_x1_ts) < 1e-6:
|
||||||
# 垂直線(幾乎不可能,但要處理)
|
|
||||||
if other_left <= line_x1_ts <= other_right:
|
if other_left <= line_x1_ts <= other_right:
|
||||||
# 檢查Y範圍
|
|
||||||
line_y_min = min(line_y1, line_y2)
|
line_y_min = min(line_y1, line_y2)
|
||||||
line_y_max = max(line_y1, line_y2)
|
line_y_max = max(line_y1, line_y2)
|
||||||
if not (line_y_max < other_bottom or line_y_min > other_top):
|
if not (line_y_max < other_bottom or line_y_min > other_top):
|
||||||
line_blocked = True
|
line_blocked = True
|
||||||
blocking_labels.append(other_title)
|
blocking_labels.append(other_title)
|
||||||
else:
|
else:
|
||||||
# 改進的碰撞檢測:檢查線段是否真的穿過標籤矩形
|
|
||||||
# 方法:計算線段與標籤矩形的所有可能交點
|
|
||||||
|
|
||||||
intersects = False
|
intersects = False
|
||||||
intersection_reason = ""
|
intersection_reason = ""
|
||||||
|
|
||||||
@@ -883,51 +855,40 @@ class ClassicTimelineRenderer:
|
|||||||
|
|
||||||
# 2. 檢查線段是否與標籤的四條邊相交
|
# 2. 檢查線段是否與標籤的四條邊相交
|
||||||
if not intersects and line_x1_ts != line_x2_ts:
|
if not intersects and line_x1_ts != line_x2_ts:
|
||||||
# 線段與標籤左邊界的交點
|
|
||||||
if line_x_min <= other_left <= line_x_max:
|
if line_x_min <= other_left <= line_x_max:
|
||||||
t = (other_left - line_x1_ts) / (line_x2_ts - line_x1_ts)
|
t = (other_left - line_x1_ts) / (line_x2_ts - line_x1_ts)
|
||||||
y_at_left = line_y1 + t * (line_y2 - line_y1)
|
y_at_left = line_y1 + t * (line_y2 - line_y1)
|
||||||
logger.debug(f" 檢查左邊界: t={t:.4f}, y_at_left={y_at_left:.2f}, Y範圍=[{other_bottom:.2f}~{other_top:.2f}]")
|
|
||||||
if other_bottom <= y_at_left <= other_top:
|
if other_bottom <= y_at_left <= other_top:
|
||||||
intersects = True
|
intersects = True
|
||||||
intersection_reason = f"穿過左邊界 (y={y_at_left:.2f})"
|
intersection_reason = f"穿過左邊界"
|
||||||
|
|
||||||
# 線段與標籤右邊界的交點
|
|
||||||
if not intersects and line_x_min <= other_right <= line_x_max:
|
if not intersects and line_x_min <= other_right <= line_x_max:
|
||||||
t = (other_right - line_x1_ts) / (line_x2_ts - line_x1_ts)
|
t = (other_right - line_x1_ts) / (line_x2_ts - line_x1_ts)
|
||||||
y_at_right = line_y1 + t * (line_y2 - line_y1)
|
y_at_right = line_y1 + t * (line_y2 - line_y1)
|
||||||
logger.debug(f" 檢查右邊界: t={t:.4f}, y_at_right={y_at_right:.2f}, Y範圍=[{other_bottom:.2f}~{other_top:.2f}]")
|
|
||||||
if other_bottom <= y_at_right <= other_top:
|
if other_bottom <= y_at_right <= other_top:
|
||||||
intersects = True
|
intersects = True
|
||||||
intersection_reason = f"穿過右邊界 (y={y_at_right:.2f})"
|
intersection_reason = f"穿過右邊界"
|
||||||
|
|
||||||
# 3. 檢查線段是否與標籤的上下邊界相交
|
# 3. 檢查線段是否與標籤的上下邊界相交
|
||||||
if not intersects and abs(line_y2 - line_y1) > 1e-6:
|
if not intersects and abs(line_y2 - line_y1) > 1e-6:
|
||||||
# 線段與標籤下邊界的交點
|
|
||||||
t_bottom = (other_bottom - line_y1) / (line_y2 - line_y1)
|
t_bottom = (other_bottom - line_y1) / (line_y2 - line_y1)
|
||||||
if 0 <= t_bottom <= 1:
|
if 0 <= t_bottom <= 1:
|
||||||
x_at_bottom = line_x1_ts + t_bottom * (line_x2_ts - line_x1_ts)
|
x_at_bottom = line_x1_ts + t_bottom * (line_x2_ts - line_x1_ts)
|
||||||
logger.debug(f" 檢查下邊界: t={t_bottom:.4f}, x_at_bottom={datetime.fromtimestamp(x_at_bottom)}, X範圍=[{datetime.fromtimestamp(other_left)}~{datetime.fromtimestamp(other_right)}]")
|
|
||||||
if other_left <= x_at_bottom <= other_right:
|
if other_left <= x_at_bottom <= other_right:
|
||||||
intersects = True
|
intersects = True
|
||||||
intersection_reason = f"穿過下邊界 (x={datetime.fromtimestamp(x_at_bottom)})"
|
intersection_reason = f"穿過下邊界"
|
||||||
|
|
||||||
# 線段與標籤上邊界的交點
|
|
||||||
if not intersects:
|
if not intersects:
|
||||||
t_top = (other_top - line_y1) / (line_y2 - line_y1)
|
t_top = (other_top - line_y1) / (line_y2 - line_y1)
|
||||||
if 0 <= t_top <= 1:
|
if 0 <= t_top <= 1:
|
||||||
x_at_top = line_x1_ts + t_top * (line_x2_ts - line_x1_ts)
|
x_at_top = line_x1_ts + t_top * (line_x2_ts - line_x1_ts)
|
||||||
logger.debug(f" 檢查上邊界: t={t_top:.4f}, x_at_top={datetime.fromtimestamp(x_at_top)}, X範圍=[{datetime.fromtimestamp(other_left)}~{datetime.fromtimestamp(other_right)}]")
|
|
||||||
if other_left <= x_at_top <= other_right:
|
if other_left <= x_at_top <= other_right:
|
||||||
intersects = True
|
intersects = True
|
||||||
intersection_reason = f"穿過上邊界 (x={datetime.fromtimestamp(x_at_top)})"
|
intersection_reason = f"穿過上邊界"
|
||||||
|
|
||||||
if intersects:
|
if intersects:
|
||||||
line_blocked = True
|
line_blocked = True
|
||||||
blocking_labels.append(other_title)
|
blocking_labels.append(other_title)
|
||||||
logger.debug(f" ❌ 碰撞確認: {intersection_reason}")
|
|
||||||
else:
|
|
||||||
logger.debug(f" ✓ 無碰撞")
|
|
||||||
|
|
||||||
if not line_blocked:
|
if not line_blocked:
|
||||||
# 直線不被遮擋,直接繪製
|
# 直線不被遮擋,直接繪製
|
||||||
@@ -991,11 +952,9 @@ class ClassicTimelineRenderer:
|
|||||||
expansion_ratio=0.0 # 不外擴
|
expansion_ratio=0.0 # 不外擴
|
||||||
)
|
)
|
||||||
|
|
||||||
# 如果標籤與事件在同一時間(垂直對齊),也清除事件點附近
|
# 如果標籤與事件在同一時間(垂直對齊),清除事件點附近
|
||||||
# 這是為了處理 Event 4 和 Event 5 這種情況
|
|
||||||
if abs((label_x - event_x).total_seconds()) < label_width_seconds / 4:
|
if abs((label_x - event_x).total_seconds()) < label_width_seconds / 4:
|
||||||
# 清除起點附近的障礙物(只清除一小塊)
|
start_clear_seconds = 3600
|
||||||
start_clear_seconds = 3600 # 清除起點附近1小時的範圍
|
|
||||||
grid_map.mark_rectangle(
|
grid_map.mark_rectangle(
|
||||||
center_x_datetime=event_x,
|
center_x_datetime=event_x,
|
||||||
center_y=0,
|
center_y=0,
|
||||||
@@ -1009,14 +968,11 @@ class ClassicTimelineRenderer:
|
|||||||
start_col = grid_map.datetime_to_grid_x(event_x)
|
start_col = grid_map.datetime_to_grid_x(event_x)
|
||||||
start_row = grid_map.y_to_grid_y(0)
|
start_row = grid_map.y_to_grid_y(0)
|
||||||
|
|
||||||
# 終點:標籤邊緣(而非中心!)
|
# 終點:標籤邊緣
|
||||||
# 根據標籤在上方還是下方,設定終點在標籤的下邊緣或上邊緣
|
|
||||||
if label_y > 0:
|
if label_y > 0:
|
||||||
# 上方標籤:終點在下邊緣(靠近時間軸的一側)
|
|
||||||
label_edge_y = label_y - label_height / 2
|
label_edge_y = label_y - label_height / 2
|
||||||
direction_constraint = "up"
|
direction_constraint = "up"
|
||||||
else:
|
else:
|
||||||
# 下方標籤:終點在上邊緣(靠近時間軸的一側)
|
|
||||||
label_edge_y = label_y + label_height / 2
|
label_edge_y = label_y + label_height / 2
|
||||||
direction_constraint = "down"
|
direction_constraint = "down"
|
||||||
|
|
||||||
@@ -1039,8 +995,7 @@ class ClassicTimelineRenderer:
|
|||||||
)
|
)
|
||||||
|
|
||||||
if path_grid is None:
|
if path_grid is None:
|
||||||
# BFS 失敗,強制使用直線
|
logger.warning(f" BFS 找不到路徑,使用直線")
|
||||||
logger.warning(f" ✗ BFS 找不到路徑,強制使用直線")
|
|
||||||
shapes.append({
|
shapes.append({
|
||||||
'type': 'line',
|
'type': 'line',
|
||||||
'x0': event_x,
|
'x0': event_x,
|
||||||
@@ -1049,21 +1004,18 @@ class ClassicTimelineRenderer:
|
|||||||
'y1': label_y,
|
'y1': label_y,
|
||||||
'xref': 'x',
|
'xref': 'x',
|
||||||
'yref': 'y',
|
'yref': 'y',
|
||||||
'line': {'color': color, 'width': 1.5, 'dash': 'dot'}, # 虛線表示強制
|
'line': {'color': color, 'width': 1.5, 'dash': 'dot'},
|
||||||
'layer': 'below',
|
'layer': 'below',
|
||||||
'opacity': 0.5
|
'opacity': 0.5
|
||||||
})
|
})
|
||||||
|
|
||||||
# 重要:即使是強制直線,也要標記為障礙物!
|
|
||||||
path_points = [(event_x, 0), (label_x, label_y)]
|
path_points = [(event_x, 0), (label_x, label_y)]
|
||||||
grid_map.mark_path(path_points, width_expansion=2.5)
|
grid_map.mark_path(path_points, width_expansion=2.5)
|
||||||
else:
|
else:
|
||||||
# BFS 成功,簡化並繪製路徑
|
logger.info(f" BFS 找到路徑,長度: {len(path_grid)}")
|
||||||
logger.info(f" ✓ BFS 找到路徑,長度: {len(path_grid)}")
|
|
||||||
|
|
||||||
# 簡化路徑
|
# 簡化路徑
|
||||||
path_coords = simplify_path(path_grid, grid_map)
|
path_coords = simplify_path(path_grid, grid_map)
|
||||||
logger.debug(f" 簡化後: {len(path_coords)} 個轉折點")
|
|
||||||
|
|
||||||
# 繪製路徑(多段線)
|
# 繪製路徑(多段線)
|
||||||
for i in range(len(path_coords) - 1):
|
for i in range(len(path_coords) - 1):
|
||||||
@@ -1082,17 +1034,17 @@ class ClassicTimelineRenderer:
|
|||||||
'opacity': 0.7
|
'opacity': 0.7
|
||||||
})
|
})
|
||||||
|
|
||||||
# 將路徑標記為障礙物(供後續路徑避讓)
|
# 將路徑標記為障礙物
|
||||||
grid_map.mark_path(path_coords, width_expansion=2.5)
|
grid_map.mark_path(path_coords, width_expansion=2.5)
|
||||||
|
|
||||||
# 恢復當前標籤為障礙物(重要!)
|
# 恢復當前標籤為障礙物
|
||||||
grid_map.mark_rectangle(
|
grid_map.mark_rectangle(
|
||||||
center_x_datetime=label_x,
|
center_x_datetime=label_x,
|
||||||
center_y=label_y,
|
center_y=label_y,
|
||||||
width_seconds=label_width_seconds,
|
width_seconds=label_width_seconds,
|
||||||
height=label_height,
|
height=label_height,
|
||||||
state=GridMap.OBSTACLE,
|
state=GridMap.OBSTACLE,
|
||||||
expansion_ratio=0.0 # 不外擴
|
expansion_ratio=0.0
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info(f"BFS 路徑規劃完成,共生成 {len(shapes)} 個線段")
|
logger.info(f"BFS 路徑規劃完成,共生成 {len(shapes)} 個線段")
|
||||||
@@ -1212,18 +1164,6 @@ class ClassicTimelineRenderer:
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
# 應用力導向演算法優化標籤位置(如果配置啟用)
|
|
||||||
# 暫時禁用:效果不佳,考慮使用專業套件(D3.js, Vega-Lite)
|
|
||||||
# if config.enable_zoom: # 使用 enable_zoom 作為啟用力導向的標誌(臨時)
|
|
||||||
# markers = apply_force_directed_layout(
|
|
||||||
# markers,
|
|
||||||
# config,
|
|
||||||
# time_range_seconds, # 新增:傳入時間範圍用於計算文字框尺寸
|
|
||||||
# max_iterations=100,
|
|
||||||
# repulsion_strength=50.0, # 調整:降低排斥力強度
|
|
||||||
# damping=0.8 # 調整:增加阻尼係數
|
|
||||||
# )
|
|
||||||
|
|
||||||
# 2. 事件點
|
# 2. 事件點
|
||||||
for marker in markers:
|
for marker in markers:
|
||||||
# 事件圓點
|
# 事件圓點
|
||||||
@@ -1264,7 +1204,7 @@ class ClassicTimelineRenderer:
|
|||||||
'size': 10,
|
'size': 10,
|
||||||
'color': theme['text_color']
|
'color': theme['text_color']
|
||||||
},
|
},
|
||||||
'bgcolor': 'rgba(255, 255, 255, 0.85)', # 降低不透明度,避免完全遮擋底層連接線
|
'bgcolor': 'rgba(255, 255, 255, 0.85)',
|
||||||
'bordercolor': marker['color'],
|
'bordercolor': marker['color'],
|
||||||
'borderwidth': 2,
|
'borderwidth': 2,
|
||||||
'borderpad': 5,
|
'borderpad': 5,
|
||||||
@@ -1272,11 +1212,10 @@ class ClassicTimelineRenderer:
|
|||||||
'align': 'left'
|
'align': 'left'
|
||||||
})
|
})
|
||||||
|
|
||||||
# 計算 Y 軸範圍(v9.1 - 固定7泳道,調整下層最低位置)
|
# 計算 Y 軸範圍
|
||||||
# 上方最高為 4.0,下方最低為 -2.5 (ratio 0.50 * 5.0)
|
y_range_max = 4.5
|
||||||
y_range_max = 4.5 # 上方最高層 + 邊距
|
y_range_min = -2.5
|
||||||
y_range_min = -2.5 # 下方最低層(已調整,避免遮擋日期)
|
y_margin = 0.8
|
||||||
y_margin = 0.8 # 額外邊距(增加以確保日期文字完全可見)
|
|
||||||
|
|
||||||
# 佈局配置
|
# 佈局配置
|
||||||
layout = {
|
layout = {
|
||||||
@@ -1305,7 +1244,6 @@ class ClassicTimelineRenderer:
|
|||||||
'margin': {'l': 50, 'r': 50, 't': 80, 'b': 80}
|
'margin': {'l': 50, 'r': 50, 't': 80, 'b': 80}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Plotly 配置
|
|
||||||
plotly_config = {
|
plotly_config = {
|
||||||
'responsive': True,
|
'responsive': True,
|
||||||
'displayModeBar': True,
|
'displayModeBar': True,
|
||||||
@@ -1413,18 +1351,6 @@ class ClassicTimelineRenderer:
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
# 應用力導向演算法優化標籤位置(如果配置啟用)
|
|
||||||
# 暫時禁用:效果不佳,考慮使用專業套件(D3.js, Vega-Lite)
|
|
||||||
# if config.enable_zoom: # 使用 enable_zoom 作為啟用力導向的標誌(臨時)
|
|
||||||
# markers = apply_force_directed_layout(
|
|
||||||
# markers,
|
|
||||||
# config,
|
|
||||||
# time_range_seconds, # 新增:傳入時間範圍用於計算文字框尺寸
|
|
||||||
# max_iterations=100,
|
|
||||||
# repulsion_strength=50.0, # 調整:降低排斥力強度
|
|
||||||
# damping=0.8 # 調整:增加阻尼係數
|
|
||||||
# )
|
|
||||||
|
|
||||||
# 2. 事件點、時間標籤和連接線
|
# 2. 事件點、時間標籤和連接線
|
||||||
for marker in markers:
|
for marker in markers:
|
||||||
# 事件圓點
|
# 事件圓點
|
||||||
@@ -1458,42 +1384,27 @@ class ClassicTimelineRenderer:
|
|||||||
line_x_points = [marker['x'], label_x]
|
line_x_points = [marker['x'], label_x]
|
||||||
line_y_points = [event_y, event_y]
|
line_y_points = [event_y, event_y]
|
||||||
else:
|
else:
|
||||||
# 使用 L 形直角折線(水平 -> 垂直 -> 水平)
|
# 使用 L 形直角折線
|
||||||
# 智能路徑規劃:根據層級、方向、跨越距離動態調整
|
is_right_side = label_x > 0
|
||||||
|
|
||||||
# 1. 判斷標籤在左側還是右側
|
|
||||||
is_right_side = label_x > 0 # 右側為正
|
|
||||||
|
|
||||||
# 2. 計算跨越距離(標準化)
|
|
||||||
total_range = (end_date - start_date).total_seconds()
|
total_range = (end_date - start_date).total_seconds()
|
||||||
y_span_ratio = abs(y_diff_seconds) / total_range if total_range > 0 else 0
|
y_span_ratio = abs(y_diff_seconds) / total_range if total_range > 0 else 0
|
||||||
|
layer_group = layer % 10
|
||||||
|
|
||||||
# 3. 根據層級計算基礎偏移(增加偏移幅度和範圍)
|
|
||||||
layer_group = layer % 10 # 每10層循環一次(增加變化)
|
|
||||||
|
|
||||||
# 4. 根據左右方向使用不同的層級策略
|
|
||||||
# 右側:從低到高 (0.25 -> 0.85)
|
|
||||||
# 左側:從高到低 (0.85 -> 0.25),鏡像分布避免交錯
|
|
||||||
if is_right_side:
|
if is_right_side:
|
||||||
base_ratio = 0.25
|
base_ratio = 0.25
|
||||||
layer_offset = layer_group * 0.06 # 6% 增量
|
layer_offset = layer_group * 0.06
|
||||||
else:
|
else:
|
||||||
base_ratio = 0.85
|
base_ratio = 0.85
|
||||||
layer_offset = -layer_group * 0.06 # 負向偏移
|
layer_offset = -layer_group * 0.06
|
||||||
|
|
||||||
# 5. 根據跨越距離調整
|
if y_span_ratio > 0.3:
|
||||||
# 距離越遠,調整幅度越大
|
|
||||||
if y_span_ratio > 0.3: # 跨越超過30%的時間軸
|
|
||||||
distance_adjustment = -0.10 if is_right_side else 0.10
|
distance_adjustment = -0.10 if is_right_side else 0.10
|
||||||
elif y_span_ratio > 0.15: # 跨越15-30%
|
elif y_span_ratio > 0.15:
|
||||||
distance_adjustment = -0.05 if is_right_side else 0.05
|
distance_adjustment = -0.05 if is_right_side else 0.05
|
||||||
else:
|
else:
|
||||||
distance_adjustment = 0
|
distance_adjustment = 0
|
||||||
|
|
||||||
# 6. 計算最終的中間寬度比例
|
|
||||||
mid_x_ratio = base_ratio + layer_offset + distance_adjustment
|
mid_x_ratio = base_ratio + layer_offset + distance_adjustment
|
||||||
|
|
||||||
# 7. 限制範圍,避免過遠或過近
|
|
||||||
mid_x_ratio = max(0.20, min(mid_x_ratio, 0.90))
|
mid_x_ratio = max(0.20, min(mid_x_ratio, 0.90))
|
||||||
|
|
||||||
mid_x = label_x * mid_x_ratio
|
mid_x = label_x * mid_x_ratio
|
||||||
@@ -1508,11 +1419,10 @@ class ClassicTimelineRenderer:
|
|||||||
event_y, # 起點
|
event_y, # 起點
|
||||||
event_y, # 保持在同一高度
|
event_y, # 保持在同一高度
|
||||||
label_y, # 垂直移動到標籤 y
|
label_y, # 垂直移動到標籤 y
|
||||||
label_y # 終點
|
label_y
|
||||||
]
|
]
|
||||||
|
|
||||||
# 使用 shape line 繪製連接線(分段),設定 layer='below' 避免遮擋
|
# 繪製連接線
|
||||||
# 將每一段連線分別繪製為獨立的 shape
|
|
||||||
for i in range(len(line_x_points) - 1):
|
for i in range(len(line_x_points) - 1):
|
||||||
shapes.append({
|
shapes.append({
|
||||||
'type': 'line',
|
'type': 'line',
|
||||||
@@ -1526,8 +1436,8 @@ class ClassicTimelineRenderer:
|
|||||||
'color': marker['color'],
|
'color': marker['color'],
|
||||||
'width': 1.5,
|
'width': 1.5,
|
||||||
},
|
},
|
||||||
'layer': 'below', # 線條置於底層,不遮擋事件點和文字框
|
'layer': 'below',
|
||||||
'opacity': 0.7, # 半透明,作為視覺輔助
|
'opacity': 0.7,
|
||||||
})
|
})
|
||||||
|
|
||||||
# 文字標註(包含時間、標題、描述)
|
# 文字標註(包含時間、標題、描述)
|
||||||
@@ -1540,7 +1450,7 @@ class ClassicTimelineRenderer:
|
|||||||
'size': 10,
|
'size': 10,
|
||||||
'color': theme['text_color']
|
'color': theme['text_color']
|
||||||
},
|
},
|
||||||
'bgcolor': 'rgba(255, 255, 255, 0.85)', # 降低不透明度,避免完全遮擋底層連接線
|
'bgcolor': 'rgba(255, 255, 255, 0.85)',
|
||||||
'bordercolor': marker['color'],
|
'bordercolor': marker['color'],
|
||||||
'borderwidth': 2,
|
'borderwidth': 2,
|
||||||
'borderpad': 5,
|
'borderpad': 5,
|
||||||
@@ -1548,10 +1458,10 @@ class ClassicTimelineRenderer:
|
|||||||
'align': 'left'
|
'align': 'left'
|
||||||
})
|
})
|
||||||
|
|
||||||
# 計算 X 軸範圍(根據最大層級動態調整,並為時間標籤預留空間)
|
# 計算 X 軸範圍
|
||||||
x_range_max = max((pos['layer'] // 2 + 1) * layer_spacing for pos in label_positions) if label_positions else layer_spacing
|
x_range_max = max((pos['layer'] // 2 + 1) * layer_spacing for pos in label_positions) if label_positions else layer_spacing
|
||||||
x_range_min = -x_range_max
|
x_range_min = -x_range_max
|
||||||
x_margin = 0.4 # 額外邊距(增加以容納時間標籤)
|
x_margin = 0.4
|
||||||
|
|
||||||
# 佈局配置
|
# 佈局配置
|
||||||
layout = {
|
layout = {
|
||||||
@@ -1580,7 +1490,6 @@ class ClassicTimelineRenderer:
|
|||||||
'margin': {'l': 100, 'r': 100, 't': 80, 'b': 50}
|
'margin': {'l': 100, 'r': 100, 't': 80, 'b': 50}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Plotly 配置
|
|
||||||
plotly_config = {
|
plotly_config = {
|
||||||
'responsive': True,
|
'responsive': True,
|
||||||
'displayModeBar': True,
|
'displayModeBar': True,
|
||||||
|
|||||||
@@ -3,11 +3,6 @@
|
|||||||
|
|
||||||
本模組定義 TimeLine Designer 所有資料結構。
|
本模組定義 TimeLine Designer 所有資料結構。
|
||||||
遵循 Pydantic BaseModel 進行嚴格型別驗證。
|
遵循 Pydantic BaseModel 進行嚴格型別驗證。
|
||||||
|
|
||||||
Author: AI Agent
|
|
||||||
Version: 1.0.0
|
|
||||||
DocID: SDD-SCHEMA-001
|
|
||||||
Rationale: 實現 SDD.md 第2節定義的資料模型
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|||||||
Reference in New Issue
Block a user