@@ -411,7 +411,7 @@ def _prepare_threshold_hybrid_frame(
411411 agg = aggregation .get (vf , "sum" )
412412 ser = filtered_df [vf ]
413413 if agg in _NUMERIC_COERCE_AGGS :
414- ser = pd . to_numeric (ser , errors = "coerce" )
414+ ser = _coerce_measure_series (ser , vf , null_handling )
415415 if agg == "avg" :
416416 cnt = int (ser .count ())
417417 row [vf ] = float (ser .sum () / cnt ) if cnt else float ("nan" )
@@ -451,7 +451,7 @@ def _prepare_threshold_hybrid_frame(
451451 col_type = column_types .get (dim ) if column_types else None
452452 working [dim ] = _resolve_dim_value_series (working [dim ], col_type , mode , grain )
453453 for vf in numeric_coerce_fields :
454- working [vf ] = pd . to_numeric (working [vf ], errors = "coerce" )
454+ working [vf ] = _coerce_measure_series (working [vf ], vf , null_handling )
455455
456456 out = (
457457 working .groupby (group_fields , dropna = False , observed = True , sort = False )
@@ -655,6 +655,14 @@ def _resolve_dim_value_series(
655655 return series .fillna ("" ).astype (str )
656656
657657
658+ def _coerce_measure_series (series : Any , field : str , null_handling : Any ) -> Any :
659+ """Coerce measure values and zero-fill nulls when requested."""
660+ numeric = pd .to_numeric (series , errors = "coerce" )
661+ if _get_null_mode (field , null_handling ) == "zero" :
662+ return numeric .fillna (0 )
663+ return numeric
664+
665+
658666def _extract_styler_formats (
659667 styler : Any ,
660668) -> tuple [dict [str , str ], dict [str , str ]]:
@@ -942,15 +950,26 @@ def _build_sidecar_fingerprint(
942950 return json .dumps (obj , sort_keys = True , separators = ("," , ":" ))
943951
944952
945- def _sidecar_agg_func (agg : str , series : Any ) -> Any :
953+ def _sidecar_agg_func (
954+ agg : str ,
955+ series : Any ,
956+ field : str | None = None ,
957+ null_handling : Any = None ,
958+ ) -> Any :
946959 """Compute a single aggregate on a pandas Series for sidecar totals.
947960
948961 Applies pd.to_numeric coercion for numeric aggs to match the frontend's
949962 toNumber() semantics (non-numeric strings are dropped).
950963 """
951964 if agg == "count_distinct" :
965+ if field is not None and _get_null_mode (field , null_handling ) == "zero" :
966+ return series .fillna (0 ).nunique ()
952967 return series .nunique ()
953- numeric = pd .to_numeric (series , errors = "coerce" )
968+ numeric = (
969+ _coerce_measure_series (series , field , null_handling )
970+ if field is not None
971+ else pd .to_numeric (series , errors = "coerce" )
972+ )
954973 if agg == "avg" :
955974 return numeric .mean ()
956975 if agg == "median" :
@@ -970,6 +989,7 @@ def _sidecar_groupby_agg(
970989 df : Any ,
971990 group_cols : list [str ],
972991 sidecar_fields : dict [str , str ],
992+ null_handling : Any = None ,
973993) -> list [dict [str , Any ]]:
974994 """GroupBy + aggregate for sidecar total entries."""
975995 if not group_cols or not sidecar_fields :
@@ -981,7 +1001,7 @@ def _sidecar_groupby_agg(
9811001 key = [str (k ) for k in key ]
9821002 values : dict [str , int | float | None ] = {}
9831003 for field , agg in sidecar_fields .items ():
984- val = _sidecar_agg_func (agg , group_df [field ])
1004+ val = _sidecar_agg_func (agg , group_df [field ], field , null_handling )
9851005 values [field ] = _normalize_sidecar_value (val )
9861006 entries .append ({"key" : key , "values" : values })
9871007 return entries
@@ -1062,12 +1082,18 @@ def _compute_hybrid_totals(
10621082
10631083 grand : dict [str , int | float | None ] = {}
10641084 for field , agg in sidecar_fields .items ():
1065- val = _sidecar_agg_func (agg , working [field ])
1085+ val = _sidecar_agg_func (agg , working [field ], field , null_handling )
10661086 grand [field ] = _normalize_sidecar_value (val )
10671087
1068- row_entries = _sidecar_groupby_agg (working , rows , sidecar_fields ) if rows else []
1088+ row_entries = (
1089+ _sidecar_groupby_agg (working , rows , sidecar_fields , null_handling )
1090+ if rows
1091+ else []
1092+ )
10691093 col_entries = (
1070- _sidecar_groupby_agg (working , columns , sidecar_fields ) if columns else []
1094+ _sidecar_groupby_agg (working , columns , sidecar_fields , null_handling )
1095+ if columns
1096+ else []
10711097 )
10721098
10731099 result : dict [str , Any ] = {
@@ -1093,7 +1119,7 @@ def _compute_hybrid_totals(
10931119 cp_key = parts [len (rows ) :]
10941120 vals : dict [str , int | float | None ] = {}
10951121 for field , agg in sidecar_fields .items ():
1096- v = _sidecar_agg_func (agg , group_df [field ])
1122+ v = _sidecar_agg_func (agg , group_df [field ], field , null_handling )
10971123 vals [field ] = _normalize_sidecar_value (v )
10981124 col_prefix_entries .append (
10991125 {"key" : cp_key , "row" : row_key , "values" : vals }
@@ -1110,7 +1136,7 @@ def _compute_hybrid_totals(
11101136 )
11111137 vals_grand : dict [str , int | float | None ] = {}
11121138 for field , agg in sidecar_fields .items ():
1113- v = _sidecar_agg_func (agg , group_df [field ])
1139+ v = _sidecar_agg_func (agg , group_df [field ], field , null_handling )
11141140 vals_grand [field ] = _normalize_sidecar_value (v )
11151141 col_prefix_grand_entries .append ({"key" : cp_key , "values" : vals_grand })
11161142
@@ -1169,7 +1195,9 @@ def _compute_hybrid_totals(
11691195 )
11701196 vals_tp : dict [str , int | float | None ] = {}
11711197 for field , agg in sidecar_fields .items ():
1172- v = _sidecar_agg_func (agg , group_df [field ])
1198+ v = _sidecar_agg_func (
1199+ agg , group_df [field ], field , null_handling
1200+ )
11731201 vals_tp [field ] = _normalize_sidecar_value (v )
11741202 temporal_parent_entries .append (
11751203 {
@@ -1195,7 +1223,9 @@ def _compute_hybrid_totals(
11951223 )
11961224 vals_tp_g : dict [str , int | float | None ] = {}
11971225 for field , agg in sidecar_fields .items ():
1198- v = _sidecar_agg_func (agg , group_df [field ])
1226+ v = _sidecar_agg_func (
1227+ agg , group_df [field ], field , null_handling
1228+ )
11991229 vals_tp_g [field ] = _normalize_sidecar_value (v )
12001230 temporal_parent_grand_entries .append (
12011231 {
@@ -1262,7 +1292,9 @@ def _append_temporal_row_parent_entries(
12621292 col_key = col_key_builder (parts [len (rows ) :])
12631293 vals_trp : dict [str , int | float | None ] = {}
12641294 for field , agg in sidecar_fields .items ():
1265- v = _sidecar_agg_func (agg , group_df [field ])
1295+ v = _sidecar_agg_func (
1296+ agg , group_df [field ], field , null_handling
1297+ )
12661298 vals_trp [field ] = _normalize_sidecar_value (v )
12671299 temporal_row_parent_entries .append (
12681300 {
@@ -1352,7 +1384,9 @@ def _append_temporal_row_parent_entries(
13521384 )
13531385 vals_trp_g : dict [str , int | float | None ] = {}
13541386 for field , agg in sidecar_fields .items ():
1355- v = _sidecar_agg_func (agg , group_df [field ])
1387+ v = _sidecar_agg_func (
1388+ agg , group_df [field ], field , null_handling
1389+ )
13561390 vals_trp_g [field ] = _normalize_sidecar_value (v )
13571391 temporal_row_parent_grand_entries .append (
13581392 {
@@ -1390,7 +1424,9 @@ def _append_temporal_row_parent_entries(
13901424 ck = parts [depth :]
13911425 vals_sub : dict [str , int | float | None ] = {}
13921426 for field , agg in sidecar_fields .items ():
1393- v = _sidecar_agg_func (agg , group_df [field ])
1427+ v = _sidecar_agg_func (
1428+ agg , group_df [field ], field , null_handling
1429+ )
13941430 vals_sub [field ] = _normalize_sidecar_value (v )
13951431 subtotal_entries .append ({"key" : rp , "col" : ck , "values" : vals_sub })
13961432
@@ -1402,7 +1438,7 @@ def _append_temporal_row_parent_entries(
14021438 parts = [str (p ) for p in parts ]
14031439 vals_rt : dict [str , int | float | None ] = {}
14041440 for field , agg in sidecar_fields .items ():
1405- v = _sidecar_agg_func (agg , group_df [field ])
1441+ v = _sidecar_agg_func (agg , group_df [field ], field , null_handling )
14061442 vals_rt [field ] = _normalize_sidecar_value (v )
14071443 subtotal_entries .append ({"key" : parts , "col" : [], "values" : vals_rt })
14081444
@@ -1420,7 +1456,9 @@ def _append_temporal_row_parent_entries(
14201456 cp_c = parts_c [depth :]
14211457 vals_cross : dict [str , int | float | None ] = {}
14221458 for field , agg in sidecar_fields .items ():
1423- v = _sidecar_agg_func (agg , group_df [field ])
1459+ v = _sidecar_agg_func (
1460+ agg , group_df [field ], field , null_handling
1461+ )
14241462 vals_cross [field ] = _normalize_sidecar_value (v )
14251463 cross_subtotal_entries .append (
14261464 {"key" : rp_c , "col_prefix" : cp_c , "values" : vals_cross }
0 commit comments