Ken-INOUE commited on
Commit
9e48adf
·
1 Parent(s): fcd4753

Add new functions for generating trend figures: single subplot and individual figures by tag. Update UI for display mode selection and enhance rendering logic.

Browse files
Files changed (1) hide show
  1. app.py +279 -11
app.py CHANGED
@@ -5,6 +5,8 @@ import os
5
  import re
6
  from typing import Dict, Tuple, List, Optional
7
  import plotly.graph_objects as go
 
 
8
 
9
  # ======================================
10
  # 設定(添付CSVの既定パス:必要に応じて変更可)
@@ -191,7 +193,7 @@ STATUS_COLOR = {
191
  LINE_COLOR = "#4a5568" # 濃いグレー
192
 
193
  # ======================================
194
- # 図作成
195
  # ======================================
196
  def make_trend_figs(
197
  df: pd.DataFrame,
@@ -317,6 +319,240 @@ def make_trend_figs(
317
 
318
  return figs
319
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
320
  # ======================================
321
  # グローバル状態(UI間共有)
322
  # ======================================
@@ -386,7 +622,7 @@ def update_items(process_name: str):
386
 
387
  def render_figs(process_name: str, items: List[str], thr_mode: str, date_min, date_max):
388
  """
389
- 図を生成して返す(複数図)。GradioではList[plotly.Figure]を直接返せる。
390
  """
391
  if G_DF is None:
392
  return "⚠ データ未読み込み", []
@@ -402,6 +638,31 @@ def render_figs(process_name: str, items: List[str], thr_mode: str, date_min, da
402
  return "⚠ 図を生成できませんでした(データ無し or 条件不一致)", []
403
  return f"✅ {process_name}: {len(figs)}枚のトレンド図を生成しました(計測項目タグごと)", figs
404
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
405
  # ======================================
406
  # UI
407
  # ======================================
@@ -412,7 +673,7 @@ with gr.Blocks(css="""
412
 
413
  with gr.Row():
414
  csv_uploader = gr.File(label="① 時系列CSV(3行ヘッダー)", file_count="single", file_types=[".csv"])
415
- thr_uploader = gr.File(label="② 閾値Excel(任意: LL/L/H/HH)", file_count="single", file_types=[".xlsx", ".xls"])
416
 
417
  with gr.Row():
418
  thr_mode = gr.Radio(
@@ -423,6 +684,13 @@ with gr.Blocks(css="""
423
  date_min = gr.Textbox(label="抽出開始日時(任意)例: 2024-07-01 00:00")
424
  date_max = gr.Textbox(label="抽出終了日時(任意)例: 2024-07-31 23:59")
425
 
 
 
 
 
 
 
 
426
  status_csv = gr.Markdown()
427
  status_thr = gr.Markdown()
428
 
@@ -433,7 +701,10 @@ with gr.Blocks(css="""
433
  btn_render = gr.Button("トレンド図を生成", variant="primary")
434
 
435
  msg = gr.Markdown()
436
- plots = gr.Plotly(label="トレンド図(計測項目タグごとに複数枚)", height=540, every=1, interactive=True, show_label=True, scale=100, container=True, visible=True, elem_id="plot_container", elem_classes=["w-full"], )
 
 
 
437
 
438
  # コールバック接続
439
  # 1) 既定CSVの自動ロード
@@ -464,15 +735,12 @@ with gr.Blocks(css="""
464
  )
465
 
466
  # 5) 図生成
467
- def _thr_mode_key(s):
468
- return "excel" if s.startswith("excel") else "auto"
469
-
470
  btn_render.click(
471
- fn=lambda proc, items, mode, dmin, dmax: render_figs(proc, items, _thr_mode_key(mode), dmin, dmax),
472
- inputs=[process_dd, items_cb, thr_mode, date_min, date_max],
473
- outputs=[msg, plots],
474
  )
475
 
476
  if __name__ == "__main__":
477
- # gradio>=4 で Plotly がそのままレンダリング可能
478
  demo.launch()
 
5
  import re
6
  from typing import Dict, Tuple, List, Optional
7
  import plotly.graph_objects as go
8
+ from plotly.subplots import make_subplots
9
+ import plotly.io as pio
10
 
11
  # ======================================
12
  # 設定(添付CSVの既定パス:必要に応じて変更可)
 
193
  LINE_COLOR = "#4a5568" # 濃いグレー
194
 
195
  # ======================================
196
+ # 図作成(既存:グルーピングごとに個別のFigureを返す)
197
  # ======================================
198
  def make_trend_figs(
199
  df: pd.DataFrame,
 
319
 
320
  return figs
321
 
322
+ # ======================================
323
+ # 新規:サブプロット1枚でまとめる図
324
+ # ======================================
325
+ def make_trend_figure(
326
+ df: pd.DataFrame,
327
+ process_map: Dict[str, List[dict]],
328
+ process_name: str,
329
+ selected_items: List[str],
330
+ thr_df: Optional[pd.DataFrame],
331
+ thr_mode: str, # "excel" or "auto"
332
+ date_min: Optional[str] = None,
333
+ date_max: Optional[str] = None,
334
+ ) -> Optional[go.Figure]:
335
+ if df is None or not process_name:
336
+ return None
337
+ recs = process_map.get(process_name, [])
338
+ if not recs:
339
+ return None
340
+ selected_items_set = set([normalize(x) for x in (selected_items or [])])
341
+ recs = [r for r in recs if normalize(r["item"]) in selected_items_set]
342
+ if not recs:
343
+ return None
344
+
345
+ dfw = df.copy()
346
+ if date_min:
347
+ dfw = dfw[dfw["timestamp"] >= pd.to_datetime(date_min)]
348
+ if date_max:
349
+ dfw = dfw[dfw["timestamp"] <= pd.to_datetime(date_max)]
350
+ if dfw.empty:
351
+ return None
352
+
353
+ thr_lookup = build_threshold_lookup(thr_df) if thr_mode == "excel" else {}
354
+
355
+ # 計測項目タグでグルーピング
356
+ groups: Dict[str, List[dict]] = {}
357
+ for r in recs:
358
+ tag = extract_measure_tag(r["item"])
359
+ groups.setdefault(tag, []).append(r)
360
+ tags = list(groups.keys())
361
+ if not tags:
362
+ return None
363
+
364
+ fig = make_subplots(
365
+ rows=len(tags), cols=1, shared_xaxes=True,
366
+ vertical_spacing=0.03,
367
+ subplot_titles=[f"{process_name} | 計測項目: {t}" for t in tags]
368
+ )
369
+
370
+ row_idx = 1
371
+ for tag in tags:
372
+ cols = groups[tag]
373
+ for r in cols:
374
+ col = r["col_tuple"]
375
+ col_str = r["col_str"]
376
+ if col in dfw.columns:
377
+ series = dfw[col]
378
+ elif col_str in dfw.columns:
379
+ series = dfw[col_str]
380
+ else:
381
+ continue
382
+
383
+ x = dfw["timestamp"]
384
+ y = pd.to_numeric(series, errors="coerce")
385
+
386
+ if thr_mode == "excel":
387
+ key = (normalize(r["id"]), normalize(r["item"]), normalize(r["process"]))
388
+ LL, L, H, HH = thr_lookup.get(key, (np.nan, np.nan, np.nan, np.nan))
389
+ if all(pd.isna(v) for v in [LL, L, H, HH]):
390
+ LL, L, H, HH = auto_threshold(y)
391
+ else:
392
+ LL, L, H, HH = auto_threshold(y)
393
+
394
+ # ライン
395
+ fig.add_trace(
396
+ go.Scatter(
397
+ x=x, y=y, mode="lines",
398
+ name=f"{r['item']} ({r['id']})",
399
+ line=dict(color=LINE_COLOR, width=1.5),
400
+ hovertemplate="%{x}<br>%{y}<extra>"+f"{r['item']} ({r['id']})"+"</extra>"
401
+ ),
402
+ row=row_idx, col=1
403
+ )
404
+ # マーカー(色分け)
405
+ colors = []
406
+ for v in y:
407
+ if pd.isna(v):
408
+ colors.append("rgba(0,0,0,0)")
409
+ else:
410
+ st = judge_status(v, LL, L, H, HH)
411
+ colors.append(STATUS_COLOR.get(st, STATUS_COLOR["OK"]))
412
+ fig.add_trace(
413
+ go.Scatter(
414
+ x=x, y=y, mode="markers",
415
+ name=f"{r['item']} markers",
416
+ marker=dict(size=6, color=colors),
417
+ showlegend=False,
418
+ hovertemplate="%{x}<br>%{y}<extra></extra>"
419
+ ),
420
+ row=row_idx, col=1
421
+ )
422
+ # しきい値ガイド
423
+ for val, label in [(LL, "LL"), (L, "L"), (H, "H"), (HH, "HH")]:
424
+ if pd.notna(val):
425
+ fig.add_hline(
426
+ y=float(val), line=dict(width=1, dash="dot"),
427
+ annotation_text=label, annotation_position="top left",
428
+ row=row_idx, col=1
429
+ )
430
+ row_idx += 1
431
+
432
+ fig.update_layout(
433
+ title=f"{process_name} | 計測項目タグごとのトレンド",
434
+ xaxis_title="timestamp",
435
+ showlegend=True,
436
+ margin=dict(l=10, r=10, t=40, b=10),
437
+ hovermode="x unified",
438
+ height=max(400, 260 * len(tags)),
439
+ )
440
+ return fig
441
+
442
+ # ======================================
443
+ # 新規:計測項目タグごとに個別Figure
444
+ # ======================================
445
+ def make_trend_figs_by_tag(
446
+ df: pd.DataFrame,
447
+ process_map: Dict[str, List[dict]],
448
+ process_name: str,
449
+ selected_items: List[str],
450
+ thr_df: Optional[pd.DataFrame],
451
+ thr_mode: str,
452
+ date_min: Optional[str] = None,
453
+ date_max: Optional[str] = None,
454
+ ) -> Dict[str, go.Figure]:
455
+ if df is None or not process_name:
456
+ return {}
457
+ recs = process_map.get(process_name, [])
458
+ if not recs:
459
+ return {}
460
+ selected_items_set = set([normalize(x) for x in (selected_items or [])])
461
+ recs = [r for r in recs if normalize(r["item"]) in selected_items_set]
462
+ if not recs:
463
+ return {}
464
+
465
+ dfw = df.copy()
466
+ if date_min:
467
+ dfw = dfw[dfw["timestamp"] >= pd.to_datetime(date_min)]
468
+ if date_max:
469
+ dfw = dfw[dfw["timestamp"] <= pd.to_datetime(date_max)]
470
+ if dfw.empty:
471
+ return {}
472
+
473
+ thr_lookup = build_threshold_lookup(thr_df) if thr_mode == "excel" else {}
474
+
475
+ groups: Dict[str, List[dict]] = {}
476
+ for r in recs:
477
+ tag = extract_measure_tag(r["item"])
478
+ groups.setdefault(tag, []).append(r)
479
+
480
+ out: Dict[str, go.Figure] = {}
481
+ for tag, cols in groups.items():
482
+ fig = go.Figure()
483
+ for r in cols:
484
+ col = r["col_tuple"]
485
+ col_str = r["col_str"]
486
+ if col in dfw.columns:
487
+ series = dfw[col]
488
+ elif col_str in dfw.columns:
489
+ series = dfw[col_str]
490
+ else:
491
+ continue
492
+
493
+ x = dfw["timestamp"]
494
+ y = pd.to_numeric(series, errors="coerce")
495
+
496
+ if thr_mode == "excel":
497
+ key = (normalize(r["id"]), normalize(r["item"]), normalize(r["process"]))
498
+ LL, L, H, HH = thr_lookup.get(key, (np.nan, np.nan, np.nan, np.nan))
499
+ if all(pd.isna(v) for v in [LL, L, H, HH]):
500
+ LL, L, H, HH = auto_threshold(y)
501
+ else:
502
+ LL, L, H, HH = auto_threshold(y)
503
+
504
+ fig.add_trace(go.Scatter(
505
+ x=x, y=y, mode="lines",
506
+ name=f"{r['item']} ({r['id']})",
507
+ line=dict(color=LINE_COLOR, width=1.5),
508
+ hovertemplate="%{x}<br>%{y}<extra>"+f"{r['item']} ({r['id']})"+"</extra>"
509
+ ))
510
+
511
+ colors = []
512
+ for v in y:
513
+ if pd.isna(v):
514
+ colors.append("rgba(0,0,0,0)")
515
+ else:
516
+ st = judge_status(v, LL, L, H, HH)
517
+ colors.append(STATUS_COLOR.get(st, STATUS_COLOR["OK"]))
518
+
519
+ fig.add_trace(go.Scatter(
520
+ x=x, y=y, mode="markers",
521
+ name=f"{r['item']} markers",
522
+ marker=dict(size=6, color=colors),
523
+ showlegend=False,
524
+ hovertemplate="%{x}<br>%{y}<extra></extra>"
525
+ ))
526
+
527
+ for val, label in [(LL, "LL"), (L, "L"), (H, "H"), (HH, "HH")]:
528
+ if pd.notna(val):
529
+ fig.add_hline(y=float(val), line=dict(width=1, dash="dot"),
530
+ annotation_text=label, annotation_position="top left")
531
+
532
+ fig.update_layout(
533
+ title=f"{process_name} | 計測項目: {tag}",
534
+ xaxis_title="timestamp",
535
+ yaxis_title=tag,
536
+ legend_title="系列",
537
+ margin=dict(l=10, r=10, t=40, b=10),
538
+ hovermode="x unified",
539
+ )
540
+ out[tag] = fig
541
+ return out
542
+
543
+ def figures_to_html(figs_by_tag: Dict[str, go.Figure]) -> str:
544
+ """
545
+ 各 Figure を <div> で順番に並べた HTML を返す。
546
+ 最初の図だけ PlotlyJS をCDNで同梱し、以降はスリムに。
547
+ """
548
+ parts = []
549
+ first = True
550
+ for tag, fig in figs_by_tag.items():
551
+ html = pio.to_html(fig, include_plotlyjs='cdn' if first else False, full_html=False)
552
+ parts.append(html)
553
+ first = False
554
+ return "<br>".join(parts) if parts else "<p>図がありません。</p>"
555
+
556
  # ======================================
557
  # グローバル状態(UI間共有)
558
  # ======================================
 
622
 
623
  def render_figs(process_name: str, items: List[str], thr_mode: str, date_min, date_max):
624
  """
625
+ (旧)図を生成して返す(複数図)。今は未使用だが残置。
626
  """
627
  if G_DF is None:
628
  return "⚠ データ未読み込み", []
 
638
  return "⚠ 図を生成できませんでした(データ無し or 条件不一致)", []
639
  return f"✅ {process_name}: {len(figs)}枚のトレンド図を生成しました(計測項目タグごと)", figs
640
 
641
+ def render_any(process_name: str, items: List[str], display_mode: str, thr_mode_label: str, date_min, date_max):
642
+ """
643
+ 表示形式に応じて Plot(サブプロット1枚)または HTML(個別複数枚)を返す。
644
+ """
645
+ if G_DF is None:
646
+ return "⚠ データ未読み込み", gr.update(visible=False), gr.update(value="", visible=False)
647
+ if not process_name:
648
+ return "⚠ プロセスを選択してください", gr.update(visible=False), gr.update(value="", visible=False)
649
+ if not items:
650
+ return "⚠ 項目を選択してください", gr.update(visible=False), gr.update(value="", visible=False)
651
+
652
+ mode = "excel" if str(thr_mode_label).startswith("excel") else "auto"
653
+
654
+ if str(display_mode).startswith("サブプロット"):
655
+ fig = make_trend_figure(G_DF, G_PROCESS_MAP, process_name, items, G_THRESHOLDS_DF, mode, date_min, date_max)
656
+ if fig is None:
657
+ return "⚠ 図を生成できませんでした(データ無し or 条件不一致)", gr.update(visible=False), gr.update(value="", visible=False)
658
+ return "✅ トレンド図(1枚サブプロット)を生成しました", gr.update(value=fig, visible=True), gr.update(value="", visible=False)
659
+ else:
660
+ figs_by_tag = make_trend_figs_by_tag(G_DF, G_PROCESS_MAP, process_name, items, G_THRESHOLDS_DF, mode, date_min, date_max)
661
+ if not figs_by_tag:
662
+ return "⚠ 図を生成できませんでした(データ無し or 条件不一致)", gr.update(visible=False), gr.update(value="", visible=False)
663
+ html = figures_to_html(figs_by_tag)
664
+ return f"✅ 個別トレンド図 {len(figs_by_tag)} 枚を生成しました", gr.update(visible=False), gr.update(value=html, visible=True)
665
+
666
  # ======================================
667
  # UI
668
  # ======================================
 
673
 
674
  with gr.Row():
675
  csv_uploader = gr.File(label="① 時系列CSV(3行ヘッダー)", file_count="single", file_types=[".csv"])
676
+ thr_uploader = gr.File(label="② 閾値Excel(任意: LL/L/HH/HH)", file_count="single", file_types=[".xlsx", ".xls"])
677
 
678
  with gr.Row():
679
  thr_mode = gr.Radio(
 
684
  date_min = gr.Textbox(label="抽出開始日時(任意)例: 2024-07-01 00:00")
685
  date_max = gr.Textbox(label="抽出終了日時(任意)例: 2024-07-31 23:59")
686
 
687
+ # 表示形式の切り替え
688
+ display_mode = gr.Radio(
689
+ ["サブプロット(1枚)", "個別(複数枚)"],
690
+ value="サブプロット(1枚)",
691
+ label="表示形式"
692
+ )
693
+
694
  status_csv = gr.Markdown()
695
  status_thr = gr.Markdown()
696
 
 
701
  btn_render = gr.Button("トレンド図を生成", variant="primary")
702
 
703
  msg = gr.Markdown()
704
+ # サブプロット用(1枚)
705
+ plot = gr.Plot(label="トレンド図(タグ別サブプロット)", height=540, show_label=True, visible=True)
706
+ # 個別(複数枚)用
707
+ html_multi = gr.HTML(label="個別トレンド図(複数枚)", visible=False)
708
 
709
  # コールバック接続
710
  # 1) 既定CSVの自動ロード
 
735
  )
736
 
737
  # 5) 図生成
 
 
 
738
  btn_render.click(
739
+ fn=lambda proc, items, disp_mode, mode, dmin, dmax: render_any(proc, items, disp_mode, mode, dmin, dmax),
740
+ inputs=[process_dd, items_cb, display_mode, thr_mode, date_min, date_max],
741
+ outputs=[msg, plot, html_multi],
742
  )
743
 
744
  if __name__ == "__main__":
745
+ # gradio>=5: gr.Plot で Plotly Figure を直接表示可
746
  demo.launch()