Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -113,9 +113,31 @@ def _style_time_axis(ax):
|
|
| 113 |
plt.margins(x=0.02, y=0.05) # 留一點白邊
|
| 114 |
|
| 115 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
def render_line(df: pd.DataFrame, col: str):
|
|
|
|
| 117 |
fig, ax = plt.subplots(figsize=(6.5, 3.6))
|
| 118 |
-
ax.plot(
|
| 119 |
ax.set_title(col, fontsize=12, pad=8)
|
| 120 |
ax.set_xlabel("Time", fontsize=10)
|
| 121 |
ax.set_ylabel(col, fontsize=10)
|
|
@@ -124,19 +146,13 @@ def render_line(df: pd.DataFrame, col: str):
|
|
| 124 |
return fig
|
| 125 |
|
| 126 |
|
| 127 |
-
def _infer_bar_width_days(times: pd.Series) -> float:
|
| 128 |
-
"""依時間間距估算柱寬(單位:天)。避免整片藍牆。"""
|
| 129 |
-
if len(times) < 2:
|
| 130 |
-
return 60 / 86400 # 60秒
|
| 131 |
-
deltas = (times.iloc[1:].values - times.iloc[:-1].values) / np.timedelta64(1, 's')
|
| 132 |
-
med = np.median(deltas) if len(deltas) else 60
|
| 133 |
-
return max(10, med * 0.8) / 86400.0 # 取 80% 的中位數,至少 10 秒
|
| 134 |
-
|
| 135 |
-
|
| 136 |
def render_bar_or_dual(df: pd.DataFrame, second_col: str, first_col: str, dual_axis: bool):
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
fig, ax = plt.subplots(figsize=(6.5, 3.6))
|
| 138 |
-
width = _infer_bar_width_days(
|
| 139 |
-
x = mdates.date2num(df["time"].to_pydatetime())
|
| 140 |
ax.bar(x, df[second_col], width=width, align="center", label=second_col)
|
| 141 |
ax.set_xlabel("Time", fontsize=10)
|
| 142 |
ax.set_ylabel(second_col, fontsize=10)
|
|
@@ -144,7 +160,7 @@ def render_bar_or_dual(df: pd.DataFrame, second_col: str, first_col: str, dual_a
|
|
| 144 |
|
| 145 |
if dual_axis:
|
| 146 |
ax2 = ax.twinx()
|
| 147 |
-
ax2.plot(
|
| 148 |
ax2.set_ylabel(first_col, fontsize=10)
|
| 149 |
title = f"{second_col} (bar) + {first_col} (line)"
|
| 150 |
h1, l1 = ax.get_legend_handles_labels()
|
|
@@ -160,11 +176,12 @@ def render_bar_or_dual(df: pd.DataFrame, second_col: str, first_col: str, dual_a
|
|
| 160 |
|
| 161 |
|
| 162 |
def render_rolling(df: pd.DataFrame, col: str, window: int = 5):
|
|
|
|
| 163 |
roll_col = f"{col}_rolling{window}"
|
| 164 |
if roll_col not in df.columns:
|
| 165 |
df[roll_col] = df[col].rolling(window=window, min_periods=1).mean()
|
| 166 |
fig, ax = plt.subplots(figsize=(6.5, 3.6))
|
| 167 |
-
ax.plot(
|
| 168 |
ax.set_title(f"{col} rolling({window})", fontsize=12, pad=8)
|
| 169 |
ax.set_xlabel("Time", fontsize=10)
|
| 170 |
ax.set_ylabel(roll_col, fontsize=10)
|
|
@@ -173,6 +190,14 @@ def render_rolling(df: pd.DataFrame, col: str, window: int = 5):
|
|
| 173 |
return fig, df
|
| 174 |
|
| 175 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 176 |
# -----------------------------
|
| 177 |
# Main pipeline
|
| 178 |
# -----------------------------
|
|
@@ -213,14 +238,6 @@ def pipeline(file, series_choice, dual_axis, rolling_window):
|
|
| 213 |
return fig1, fig2, fig3, dash_json_str, json_path, df_with_roll, demo_csv_path
|
| 214 |
|
| 215 |
|
| 216 |
-
def _placeholder_fig():
|
| 217 |
-
fig, ax = plt.subplots(figsize=(6.5, 3.6))
|
| 218 |
-
ax.text(0.5, 0.5, "未選第二數值欄", ha="center", va="center", fontsize=12)
|
| 219 |
-
ax.axis("off")
|
| 220 |
-
fig.tight_layout()
|
| 221 |
-
return fig
|
| 222 |
-
|
| 223 |
-
|
| 224 |
# -----------------------------
|
| 225 |
# Regenerate demo helper
|
| 226 |
# -----------------------------
|
|
|
|
| 113 |
plt.margins(x=0.02, y=0.05) # 留一點白邊
|
| 114 |
|
| 115 |
|
| 116 |
+
def _normalize_times(series: pd.Series) -> pd.Series:
|
| 117 |
+
"""
|
| 118 |
+
把可能帶時區的時間序列,轉成「UTC 再去時區」的 naive datetime,
|
| 119 |
+
以避免 Matplotlib 在處理 tz-aware 時間造成不一致。
|
| 120 |
+
"""
|
| 121 |
+
s = series.copy()
|
| 122 |
+
if getattr(s.dt, "tz", None) is not None:
|
| 123 |
+
s = s.dt.tz_convert("UTC").dt.tz_localize(None)
|
| 124 |
+
return s
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
def _infer_bar_width_days(times: pd.Series) -> float:
|
| 128 |
+
"""依時間間距估算柱寬(單位:天)。避免整片藍牆。"""
|
| 129 |
+
t = pd.Series(times)
|
| 130 |
+
if len(t) < 2:
|
| 131 |
+
return 60 / 86400 # 60秒
|
| 132 |
+
diffs = t.astype("datetime64[ns]").diff().dt.total_seconds().dropna()
|
| 133 |
+
med = diffs.median() if not diffs.empty else 60.0
|
| 134 |
+
return max(10.0, med * 0.8) / 86400.0 # 取 80% 的中位數,至少 10 秒
|
| 135 |
+
|
| 136 |
+
|
| 137 |
def render_line(df: pd.DataFrame, col: str):
|
| 138 |
+
times = _normalize_times(df["time"])
|
| 139 |
fig, ax = plt.subplots(figsize=(6.5, 3.6))
|
| 140 |
+
ax.plot(times, df[col], linewidth=1.6)
|
| 141 |
ax.set_title(col, fontsize=12, pad=8)
|
| 142 |
ax.set_xlabel("Time", fontsize=10)
|
| 143 |
ax.set_ylabel(col, fontsize=10)
|
|
|
|
| 146 |
return fig
|
| 147 |
|
| 148 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 149 |
def render_bar_or_dual(df: pd.DataFrame, second_col: str, first_col: str, dual_axis: bool):
|
| 150 |
+
# --- 修正點:正確轉換 Series 為原生 datetime(處理時區 + .dt.to_pydatetime()) ---
|
| 151 |
+
times = _normalize_times(df["time"])
|
| 152 |
+
x = mdates.date2num(times.dt.to_pydatetime())
|
| 153 |
+
|
| 154 |
fig, ax = plt.subplots(figsize=(6.5, 3.6))
|
| 155 |
+
width = _infer_bar_width_days(times)
|
|
|
|
| 156 |
ax.bar(x, df[second_col], width=width, align="center", label=second_col)
|
| 157 |
ax.set_xlabel("Time", fontsize=10)
|
| 158 |
ax.set_ylabel(second_col, fontsize=10)
|
|
|
|
| 160 |
|
| 161 |
if dual_axis:
|
| 162 |
ax2 = ax.twinx()
|
| 163 |
+
ax2.plot(times, df[first_col], linewidth=1.6, label=f"{first_col} (line)")
|
| 164 |
ax2.set_ylabel(first_col, fontsize=10)
|
| 165 |
title = f"{second_col} (bar) + {first_col} (line)"
|
| 166 |
h1, l1 = ax.get_legend_handles_labels()
|
|
|
|
| 176 |
|
| 177 |
|
| 178 |
def render_rolling(df: pd.DataFrame, col: str, window: int = 5):
|
| 179 |
+
times = _normalize_times(df["time"])
|
| 180 |
roll_col = f"{col}_rolling{window}"
|
| 181 |
if roll_col not in df.columns:
|
| 182 |
df[roll_col] = df[col].rolling(window=window, min_periods=1).mean()
|
| 183 |
fig, ax = plt.subplots(figsize=(6.5, 3.6))
|
| 184 |
+
ax.plot(times, df[roll_col], linewidth=1.6)
|
| 185 |
ax.set_title(f"{col} rolling({window})", fontsize=12, pad=8)
|
| 186 |
ax.set_xlabel("Time", fontsize=10)
|
| 187 |
ax.set_ylabel(roll_col, fontsize=10)
|
|
|
|
| 190 |
return fig, df
|
| 191 |
|
| 192 |
|
| 193 |
+
def _placeholder_fig():
|
| 194 |
+
fig, ax = plt.subplots(figsize=(6.5, 3.6))
|
| 195 |
+
ax.text(0.5, 0.5, "未選第二數值欄", ha="center", va="center", fontsize=12)
|
| 196 |
+
ax.axis("off")
|
| 197 |
+
fig.tight_layout()
|
| 198 |
+
return fig
|
| 199 |
+
|
| 200 |
+
|
| 201 |
# -----------------------------
|
| 202 |
# Main pipeline
|
| 203 |
# -----------------------------
|
|
|
|
| 238 |
return fig1, fig2, fig3, dash_json_str, json_path, df_with_roll, demo_csv_path
|
| 239 |
|
| 240 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 241 |
# -----------------------------
|
| 242 |
# Regenerate demo helper
|
| 243 |
# -----------------------------
|