cwadayi commited on
Commit
3f3c7f0
·
verified ·
1 Parent(s): 951dff8

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +39 -22
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(df["time"], df[col], linewidth=1.6)
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(df["time"])
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(df["time"], df[first_col], linewidth=1.6, label=f"{first_col} (line)")
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(df["time"], df[roll_col], linewidth=1.6)
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
  # -----------------------------