feat: implement dynamic time-domain LOD zoom, locked axis multipliers, and clean 4-column CSV export
This commit is contained in:
+58
-10
@@ -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()
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user