feat: implement dynamic time-domain LOD zoom, locked axis multipliers, and clean 4-column CSV export

This commit is contained in:
pchang718
2026-05-26 14:48:04 +08:00
parent 0140b0495c
commit 8caa8918d0
7 changed files with 499267 additions and 44 deletions
+58 -10
View File
@@ -15,6 +15,11 @@ TEMP_CSV_DIR = "temp_csv"
os.makedirs(TEMP_CSV_DIR, exist_ok=True)
def get_cached_file_path(file_id):
if file_id == "00000000-0000-0000-0000-000000000000":
preset_path = "static/preset_signals.csv"
if os.path.exists(preset_path):
return preset_path
if not re.match(r'^[0-9a-f\-]{36}$', file_id):
raise HTTPException(status_code=400, detail="無效的檔案ID")
file_path = os.path.join(TEMP_CSV_DIR, f"{file_id}.csv")
@@ -114,7 +119,7 @@ def integer_lfilter(b_int, a_int, x_float, shift_in, shift_out, shift_b, shift_a
return y_out.astype(float) / (2**shift_out)
def filter_preview_response(file_id, b_vals, a_vals, col_idx, b_int=None, a_int=None, shift_in=14, shift_out=14, shift_b=14, shift_a=14, use_round=False, stages=None):
def filter_preview_response(file_id, b_vals, a_vals, col_idx, b_int=None, a_int=None, shift_in=14, shift_out=14, shift_b=14, shift_a=14, use_round=False, stages=None, start_idx=None, end_idx=None):
path = get_cached_file_path(file_id)
# 預先讀取欄位名稱,避免用 usecols 讀取後找不到原始索引
@@ -125,10 +130,19 @@ def filter_preview_response(file_id, b_vals, a_vals, col_idx, b_int=None, a_int=
# 精準讀取單一欄位,並加上筆數限制 (極大降低記憶體用量與時間)
df = pd.read_csv(path, usecols=[col_idx], nrows=MAX_ROWS)
x_signal = pd.to_numeric(df[col_to_filter], errors="coerce")
if x_signal.isna().any():
# Check if there are non-numeric strings coerced to NaN
original_nans = df[col_to_filter].isna()
x_signal = pd.to_numeric(df[col_to_filter], errors="coerce")
numeric_nans = x_signal.isna()
if (numeric_nans & ~original_nans).any():
raise HTTPException(status_code=400, detail=f"欄位 {col_to_filter} 含有非數值資料")
# Drop trailing/all NaN empty cells to support different sequence lengths natively
x_signal = x_signal.dropna()
if len(x_signal) == 0:
raise HTTPException(status_code=400, detail=f"欄位 {col_to_filter} 沒有任何有效數值")
x_values = x_signal.to_numpy(dtype=float)
if not np.isfinite(x_values).all():
raise HTTPException(status_code=400, detail=f"欄位 {col_to_filter} 含有非有限數值")
@@ -161,7 +175,24 @@ def filter_preview_response(file_id, b_vals, a_vals, col_idx, b_int=None, a_int=
else:
y_fixed = y_float
index, original, filtered_float, filtered_fixed, step = downsample_for_plot(df.index.to_numpy(), x_signal, y_float, y_fixed)
total_len = len(x_values)
# Robust type checking to bypass FastAPI Form object default values in python unit tests
start_idx_clean = start_idx if isinstance(start_idx, (int, float, str)) else None
end_idx_clean = end_idx if isinstance(end_idx, (int, float, str)) else None
s_idx = max(0, int(start_idx_clean)) if start_idx_clean is not None else 0
e_idx = min(total_len, int(end_idx_clean)) if end_idx_clean is not None else total_len
if s_idx >= e_idx:
s_idx = 0
e_idx = total_len
x_sliced = x_values[s_idx:e_idx]
y_float_sliced = y_float[s_idx:e_idx]
y_fixed_sliced = y_fixed[s_idx:e_idx]
idx_sliced = x_signal.index.to_numpy()[s_idx:e_idx]
index, original, filtered_float, filtered_fixed, step = downsample_for_plot(idx_sliced, x_sliced, y_float_sliced, y_fixed_sliced)
return {
"index": index.tolist(),
@@ -169,13 +200,13 @@ def filter_preview_response(file_id, b_vals, a_vals, col_idx, b_int=None, a_int=
"filtered": filtered_float.tolist(),
"filtered_fixed": filtered_fixed.tolist(),
"col_name": col_to_filter,
"total_points": int(len(df.index)),
"total_points": int(len(x_signal)),
"plot_points": int(len(index)),
"downsample_step": int(step),
}
def filtered_csv_text(file_id, b_vals, a_vals, col_idx, b_int=None, a_int=None, shift_in=14, shift_out=14, shift_b=14, shift_a=14, use_round=False, stages=None):
def filtered_csv_text(file_id, b_vals, a_vals, col_idx, b_int=None, a_int=None, shift_in=14, shift_out=14, shift_b=14, shift_a=14, use_round=False, stages=None, fs=100000.0):
path = get_cached_file_path(file_id)
# 匯出時需要原始所有欄位,但仍受限於 MAX_ROWS
df = pd.read_csv(path, nrows=MAX_ROWS)
@@ -183,8 +214,17 @@ def filtered_csv_text(file_id, b_vals, a_vals, col_idx, b_int=None, a_int=None,
if col_idx < 0 or col_idx >= len(df.columns):
raise HTTPException(status_code=400, detail="欄位索引超出範圍")
col_to_filter = df.columns[col_idx]
# Check if there are non-numeric strings coerced to NaN
original_nans = df[col_to_filter].isna()
x_signal = pd.to_numeric(df[col_to_filter], errors="coerce")
x_values = x_signal.to_numpy(dtype=float)
numeric_nans = x_signal.isna()
if (numeric_nans & ~original_nans).any():
raise HTTPException(status_code=400, detail=f"欄位 {col_to_filter} 含有非數值資料")
# Drop NaNs to process shorter signals properly
x_signal_clean = x_signal.dropna()
x_values = x_signal_clean.to_numpy(dtype=float)
if stages is not None:
y_float = x_values
@@ -210,10 +250,18 @@ def filtered_csv_text(file_id, b_vals, a_vals, col_idx, b_int=None, a_int=None,
else:
y_fixed = y_float
df[f"{col_to_filter}_filtered_ideal"] = y_float
df[f"{col_to_filter}_filtered_fixed"] = y_fixed
# Build a brand-new DataFrame containing exactly the 4 requested columns
time_values = x_signal_clean.index.to_numpy(dtype=float) / fs
export_df = pd.DataFrame({
"Time (s)": time_values,
col_to_filter: x_values,
f"{col_to_filter}_filtered_ideal": y_float,
f"{col_to_filter}_filtered_fixed": y_fixed
})
csv_buffer = io.StringIO()
df.to_csv(csv_buffer, index=False)
export_df.to_csv(csv_buffer, index=False)
return csv_buffer.getvalue()