pikobs 3.0.332__py3-none-any.whl → 3.0.334__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -303,6 +303,24 @@ def create_table_if_not_exists(cursor: sqlite3.Cursor) -> None:
303
303
 
304
304
 
305
305
  def combine(pathfileout: str, filememory: str) -> None:
306
+ # Made idempotent: DELETE any existing rows in the target for the
307
+ # SAME date(s) about to be inserted, before inserting. Without this,
308
+ # re-running the extraction for a date already in the accumulator
309
+ # (a retried/resumed job, or two separate --datestart/--dateend runs
310
+ # with an overlapping date range writing to the same output db)
311
+ # silently DUPLICATES that date's rows -- confirmed directly:
312
+ # extracting the same file twice into the same accumulator produced
313
+ # the SAME (date, id_stn) row twice, doubling every downstream sum
314
+ # for that date (and, since different runs can overlap different
315
+ # date spans by different amounts, this produces INCONSISTENT
316
+ # inflation across periods -- e.g. Today off by one factor, This
317
+ # week by another -- rather than one uniform, easy-to-spot scale
318
+ # error). This delete makes a re-run REPLACE that date's data
319
+ # instead of stacking a duplicate on top of it.
320
+ delete_sql = """
321
+ DELETE FROM moyenne
322
+ WHERE date IN (SELECT DISTINCT date FROM this_avg_db.moyenne);
323
+ """
306
324
  insert_sql = """
307
325
  INSERT INTO moyenne (Nobsdata, Nobsprofile, avg_omp, avg_oma, id_stn, channel, date)
308
326
  SELECT Nobsdata, Nobsprofile, avg_omp, avg_oma, id_stn, channel, date
@@ -315,6 +333,7 @@ def combine(pathfileout: str, filememory: str) -> None:
315
333
  create_table_if_not_exists(conn)
316
334
 
317
335
  conn.execute(f"ATTACH DATABASE '{filememory}' AS this_avg_db;")
336
+ conn.execute(delete_sql)
318
337
  conn.execute(insert_sql)
319
338
  conn.commit()
320
339
  conn.execute("DETACH DATABASE this_avg_db;")
@@ -459,6 +459,11 @@ def _draw_profile_summary_table(df_all, run_time, name_in='experience'):
459
459
  'DIFF_10DAY': _calc_pct_diff(at_run, tenday_avg),
460
460
  }
461
461
 
462
+ # Grand-total row: FAMILY='ALL', ID_STN='ALL', summing across EVERY
463
+ # family and station combined -- matches the reference report's own
464
+ # first row exactly (it puts this ahead of the per-family rows).
465
+ rows.append(_build_row('ALL', 'ALL', df_all))
466
+
462
467
  for family, g_fam in df_all.groupby('family', sort=False):
463
468
  for id_stn, g_stn in g_fam.groupby('id_stn', sort=False):
464
469
  rows.append(_build_row(family, id_stn, g_stn))
@@ -499,14 +504,27 @@ def _draw_profile_summary_table(df_all, run_time, name_in='experience'):
499
504
  "</tr>")
500
505
  body_rows = []
501
506
  for _, r in df_summary.iterrows():
502
- row_style = "background:#f5f7ff;font-weight:bold;" if r['ID_STN'] == 'ALL' else ""
503
- if r['ID_STN'] == 'ALL':
507
+ is_grand_total = (r['FAMILY'] == 'ALL' and r['ID_STN'] == 'ALL')
508
+ if is_grand_total:
509
+ row_style = "background:#dbe4f7;font-weight:bold;border-top:2px solid #4C72B0;"
510
+ elif r['ID_STN'] == 'ALL':
511
+ row_style = "background:#f5f7ff;font-weight:bold;"
512
+ else:
513
+ row_style = ""
514
+ if is_grand_total:
515
+ # No single section corresponds to "every family combined" --
516
+ # nothing meaningful to jump to, so this row isn't clickable.
517
+ onclick_attr = ""
518
+ elif r['ID_STN'] == 'ALL':
504
519
  target = f"#family-{_safe_anchor(r['FAMILY'])}"
520
+ row_style += "cursor:pointer;"
521
+ onclick_attr = f" onclick=\"location.href='{target}'\""
505
522
  else:
506
523
  target = f"#station-{_safe_anchor(r['FAMILY'])}-{_safe_anchor(r['ID_STN'])}"
507
- row_style += "cursor:pointer;"
524
+ row_style += "cursor:pointer;"
525
+ onclick_attr = f" onclick=\"location.href='{target}'\""
508
526
  body_rows.append(
509
- f"<tr style='{row_style}' onclick=\"location.href='{target}'\">"
527
+ f"<tr style='{row_style}'{onclick_attr}>"
510
528
  f"<td data-v='{r['FAMILY']}'>{r['FAMILY']}</td>"
511
529
  f"<td data-v='{r['ID_STN']}'>{r['ID_STN']}</td>"
512
530
  f"<td data-v='{r['NB_TODAY']}'>{_fmt_int(r['NB_TODAY'])}</td>"
@@ -703,13 +721,46 @@ def _draw_channel_mosaic(df_chan_stn, family, stn, region, flag_criteria,
703
721
  # the HTML note can say how many cycles this affected.
704
722
  n_gap_cycles = int(np.all(np.isnan(grid), axis=0).sum())
705
723
 
706
- fig_w = _shared_fig_width(n_date)
724
+ # Every OTHER chart in this report (mosaic, global time series,
725
+ # per-station time series) uses left=0.06/right=0.99 of
726
+ # _shared_fig_width(n_date) for its plot area -- no colorbar to eat
727
+ # into that. This chart has a colorbar, which (via the old
728
+ # `fig.colorbar(im, ax=ax, fraction=.., pad=..)` approach) SHRINKS
729
+ # the main axes to make room, so the DATA columns end up narrower
730
+ # and don't line up pixel-for-pixel with every other chart's columns
731
+ # despite the total figure width matching. Fix: reserve a FIXED
732
+ # extra width JUST for the colorbar, and place the main axes
733
+ # explicitly at the same absolute left/right (in inches) as every
734
+ # other chart, so the colorbar never touches that space.
735
+ base_fig_w = _shared_fig_width(n_date)
736
+ cbar_w_inches = 0.9
737
+ fig_w = base_fig_w + cbar_w_inches
707
738
  fig_h = max(3.0, min(0.18 * n_chan, 40)) + 1.4
708
- fig, ax = plt.subplots(figsize=(fig_w, fig_h))
739
+
740
+ # Margins reserved in roughly FIXED inches (title ~0.5", hierarchical
741
+ # x-axis labels ~1.5"), converted to a fraction of THIS figure's own
742
+ # height -- a plain fixed fraction is fine for a short mosaic but
743
+ # wastes enormous whitespace on a tall one (300+ channels can make
744
+ # fig_h > 40"), since the x-axis labels need the same few inches
745
+ # regardless of how tall the figure is.
746
+ top_frac = max(0.80, 1.0 - 0.5 / fig_h)
747
+ bottom_frac = min(0.28, 1.5 / fig_h)
748
+
749
+ left_frac = (0.06 * base_fig_w) / fig_w
750
+ plot_right_frac = (0.99 * base_fig_w) / fig_w # main axes ends here
751
+ cbar_left_frac = plot_right_frac + (0.10 / fig_w)
752
+ cbar_width_frac = 0.18 / fig_w
753
+
754
+ fig = plt.figure(figsize=(fig_w, fig_h))
755
+ ax = fig.add_axes([left_frac, bottom_frac, plot_right_frac - left_frac,
756
+ top_frac - bottom_frac])
709
757
  cmap = plt.get_cmap('viridis').copy()
710
758
  cmap.set_bad(color='#D9D9D9') # explicit grey for NaN, not blank/white
711
759
  im = ax.imshow(grid, aspect='auto', cmap=cmap, interpolation='nearest')
712
- cbar = fig.colorbar(im, ax=ax, fraction=0.02, pad=0.01)
760
+
761
+ cbar_ax = fig.add_axes([cbar_left_frac, bottom_frac, cbar_width_frac,
762
+ top_frac - bottom_frac])
763
+ cbar = fig.colorbar(im, cax=cbar_ax)
713
764
  cbar.set_label('Nobs', fontsize=9)
714
765
 
715
766
  # Keep channel labels readable even with many channels: thin them
@@ -726,15 +777,6 @@ def _draw_channel_mosaic(df_chan_stn, family, stn, region, flag_criteria,
726
777
  title += f' (grey = no channel data, {n_gap_cycles} cycle{"s" if n_gap_cycles != 1 else ""})'
727
778
  ax.set_title(title, fontsize=11)
728
779
 
729
- # Margins reserved in roughly FIXED inches (title ~0.5", hierarchical
730
- # x-axis labels ~1.5"), converted to a fraction of THIS figure's own
731
- # height -- a plain fixed fraction (e.g. bottom=0.28) is fine for a
732
- # short mosaic but wastes enormous whitespace on a tall one (300+
733
- # channels can make fig_h > 40"), since the x-axis labels need the
734
- # same few inches regardless of how tall the figure is.
735
- top_frac = max(0.80, 1.0 - 0.5 / fig_h)
736
- bottom_frac = min(0.28, 1.5 / fig_h)
737
- fig.subplots_adjust(left=0.08, right=0.94, top=top_frac, bottom=bottom_frac)
738
780
  fig.savefig(out_path, dpi=70)
739
781
  plt.close(fig)
740
782
  return out_path, n_gap_cycles
@@ -818,18 +860,24 @@ def _draw_station_full_timeseries(df_stn, stn, family, region, flag_criteria,
818
860
  stn_col = _stn_color(stn)
819
861
 
820
862
  fig, ax = plt.subplots(figsize=(fig_w, 4.6))
821
- ax.plot(df_stn['_x'], df_stn['Nobs'], marker='o', markersize=4,
822
- linestyle='-', linewidth=1.6, color=REF_COLOR, label='Nobs',
823
- zorder=2)
824
-
825
- for tier, label in ((STATUS_DARKGREEN, 'Above baseline'),
863
+ ax.axhline(0.0, color='red', lw=1.0, ls='-', alpha=0.65, zorder=1)
864
+ ax.plot(df_stn['_x'], df_stn['Nobs'], linestyle='-', linewidth=1.6,
865
+ color=REF_COLOR, zorder=2)
866
+
867
+ # Every point gets a marker in its OWN status colour (not just the
868
+ # alert-worthy tiers) -- green for normal, dark green for above
869
+ # baseline, yellow/orange/red for the drop tiers, grey for
870
+ # insufficient history -- so the curve communicates status at a
871
+ # glance along its whole length, the same colour scheme as the
872
+ # mosaic above it, not just where something's flagged.
873
+ for tier, label in ((STATUS_GREEN, 'Normal'), (STATUS_DARKGREEN, 'Above baseline'),
826
874
  (STATUS_YELLOW, 'Yellow'), (STATUS_ORANGE, 'Orange'),
827
- (STATUS_RED, 'Red')):
875
+ (STATUS_RED, 'Red'), (STATUS_GREY, 'Insufficient history')):
828
876
  pts = df_stn[df_stn['status'] == tier]
829
877
  if not pts.empty:
830
- ax.scatter(pts['_x'], pts['Nobs'], marker='o', s=55,
878
+ ax.scatter(pts['_x'], pts['Nobs'], marker='o', s=40,
831
879
  color=_STATUS_COLORS[tier], edgecolor='#333',
832
- linewidth=0.7, zorder=3, label=label)
880
+ linewidth=0.6, zorder=3, label=label)
833
881
 
834
882
  n_alerts = int(df_stn['status'].isin([STATUS_YELLOW, STATUS_ORANGE, STATUS_RED]).sum())
835
883
  ax.set_ylabel('Nobs', fontsize=13, fontweight='bold')
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pikobs
3
- Version: 3.0.332
3
+ Version: 3.0.334
4
4
  Summary: pikobs
5
5
  Home-page: https://gitlab.science.gc.ca/dlo001/pikobs
6
6
  Author: David Lobon
@@ -42,8 +42,8 @@ pikobs/obscutoff/__init__.py,sha256=zBvprGRCin9W3w6_k-SZLJ-p-lZa82cqXr1aDIti-UE,
42
42
  pikobs/obscutoff/obscutoff.py,sha256=z78FWocYD8QZus6z5X4tQ738__K5q8_RxzngEm9N0iQ,40238
43
43
  pikobs/obscutoff/obscutoff_plot.py,sha256=AiqPI6IvViuzoVQzia9NK97lDHtsqcn0newN8MRjojQ,37858
44
44
  pikobs/obstimedb/__init__.py,sha256=0-sYoNyHDzRGXT1mzbU5Ek0hSGGlUXn84yx7lvE3qVk,56
45
- pikobs/obstimedb/obstimedb.py,sha256=LQYwCV3rm5tJQZKj5hd9y7MH_D2TqJ2AEEFmM0nuXOM,27668
46
- pikobs/obstimedb/obstimedb_plot.py,sha256=1JZyZZzPW-dUfPMGUoWx9fM-DRaHVeXchm9RVlB4J-4,68771
45
+ pikobs/obstimedb/obstimedb.py,sha256=AIqHlV9T1Oy5mH_ShQbfcuJImaSXnTwQwkazE_nBiiY,28787
46
+ pikobs/obstimedb/obstimedb_plot.py,sha256=zqca_CsyGyPDLbqrvUrviaIPu8rsmu2ZoMZa9pu6Q9w,71268
47
47
  pikobs/pbs_submit/__init__.py,sha256=atE3cvhxT_mLdd7RFpRQEIicE0rhoqNMm1bqMtckqmA,26
48
48
  pikobs/pbs_submit/pbs_submit.py,sha256=UT8mZnLtCo2xSKW8IZVOsON2x_jIPjBTcpId5RTF6zo,9562
49
49
  pikobs/pikobsburp2rdb/__init__.py,sha256=gGD_XwalMfcunOvTeN4ogtTqBdL5HSOfdWhy6zdIr8E,31
@@ -73,7 +73,7 @@ pikobs/web/viewer.py,sha256=EZsFajvuyIcEoIcqnUAJ65gvqLgq69jqtGjUd4_RZIE,23372
73
73
  pikobs/zone/__init__.py,sha256=3crOJs1I5hf34SkRFtmDCXWwdAoYGuL52gG8QWuiN1Y,45
74
74
  pikobs/zone/zone.py,sha256=UtwUYKZgw9u6OKn2gRFw2m3Th8ChqMFkiO4OOG9FLrA,54443
75
75
  pikobs/zone/zone_plot.py,sha256=epESi90eeoofrqVDCkhjY6QMEyuH1WWbiPjAmNPwWe0,26773
76
- pikobs-3.0.332.dist-info/METADATA,sha256=acD_bePcLiOXWxmwgCLBibBRWne5ec-5hQErZhBHF5c,1059
77
- pikobs-3.0.332.dist-info/WHEEL,sha256=AtBG6SXL3KF_v0NxLf0ehyVOh0cold-JbJYXNGorC6Q,92
78
- pikobs-3.0.332.dist-info/top_level.txt,sha256=UAWCWWUoD-o_b3nFqEG6V0_v37qzVY7i_BPRhfMdnJ0,7
79
- pikobs-3.0.332.dist-info/RECORD,,
76
+ pikobs-3.0.334.dist-info/METADATA,sha256=XcfRaAIAdhzzPLJ6_Yf0NtZjtRtsk_3EIo-GJf3fzgc,1059
77
+ pikobs-3.0.334.dist-info/WHEEL,sha256=AtBG6SXL3KF_v0NxLf0ehyVOh0cold-JbJYXNGorC6Q,92
78
+ pikobs-3.0.334.dist-info/top_level.txt,sha256=UAWCWWUoD-o_b3nFqEG6V0_v37qzVY7i_BPRhfMdnJ0,7
79
+ pikobs-3.0.334.dist-info/RECORD,,