@@ -58,6 +58,7 @@ pub fn apply_position_adjustments(
5858#[ cfg( test) ]
5959mod tests {
6060 use super :: * ;
61+ use crate :: plot:: facet:: { Facet , FacetLayout } ;
6162 use crate :: plot:: layer:: { Geom , Position } ;
6263 use crate :: plot:: { AestheticValue , Mappings , ParameterValue , Scale , ScaleType } ;
6364 use polars:: prelude:: * ;
@@ -322,4 +323,182 @@ mod tests {
322323 assert ! ( ( -0.3 ..=0.3 ) . contains( & v) ) ;
323324 }
324325 }
326+
327+ #[ test]
328+ fn test_stack_resets_per_facet_panel ( ) {
329+ // Stacking should compute independently within each facet panel.
330+ // Without this, bars in the second facet panel stack on top of
331+ // cumulative values from the first panel (see issue #244).
332+ //
333+ // Two facet panels (F1, F2) each with the same x="A" and two
334+ // fill groups (X, Y). Stacking within each panel should start from 0.
335+ let df = df ! {
336+ "__ggsql_aes_pos1__" => [ "A" , "A" , "A" , "A" ] ,
337+ "__ggsql_aes_pos2__" => [ 10.0 , 20.0 , 30.0 , 40.0 ] ,
338+ "__ggsql_aes_pos2end__" => [ 0.0 , 0.0 , 0.0 , 0.0 ] ,
339+ "__ggsql_aes_fill__" => [ "X" , "Y" , "X" , "Y" ] ,
340+ "__ggsql_aes_facet1__" => [ "F1" , "F1" , "F2" , "F2" ] ,
341+ }
342+ . unwrap ( ) ;
343+
344+ let mut layer = crate :: plot:: Layer :: new ( Geom :: bar ( ) ) ;
345+ layer. mappings = {
346+ let mut m = Mappings :: new ( ) ;
347+ m. insert (
348+ "pos1" ,
349+ AestheticValue :: standard_column ( "__ggsql_aes_pos1__" ) ,
350+ ) ;
351+ m. insert (
352+ "pos2" ,
353+ AestheticValue :: standard_column ( "__ggsql_aes_pos2__" ) ,
354+ ) ;
355+ m. insert (
356+ "pos2end" ,
357+ AestheticValue :: standard_column ( "__ggsql_aes_pos2end__" ) ,
358+ ) ;
359+ m. insert (
360+ "fill" ,
361+ AestheticValue :: standard_column ( "__ggsql_aes_fill__" ) ,
362+ ) ;
363+ m. insert (
364+ "facet1" ,
365+ AestheticValue :: standard_column ( "__ggsql_aes_facet1__" ) ,
366+ ) ;
367+ m
368+ } ;
369+ layer. partition_by = vec ! [
370+ "__ggsql_aes_fill__" . to_string( ) ,
371+ "__ggsql_aes_facet1__" . to_string( ) ,
372+ ] ;
373+ layer. position = Position :: stack ( ) ;
374+ layer. data_key = Some ( "__ggsql_layer_0__" . to_string ( ) ) ;
375+
376+ let mut spec = Plot :: new ( ) ;
377+ spec. scales . push ( make_discrete_scale ( "pos1" ) ) ;
378+ spec. scales . push ( make_continuous_scale ( "pos2" ) ) ;
379+ spec. facet = Some ( Facet :: new ( FacetLayout :: Wrap {
380+ variables : vec ! [ "facet_var" . to_string( ) ] ,
381+ } ) ) ;
382+ let mut data_map = HashMap :: new ( ) ;
383+ data_map. insert ( "__ggsql_layer_0__" . to_string ( ) , df) ;
384+
385+ let mut spec_with_layer = spec;
386+ spec_with_layer. layers . push ( layer) ;
387+
388+ apply_position_adjustments ( & mut spec_with_layer, & mut data_map) . unwrap ( ) ;
389+
390+ let result_df = data_map. get ( "__ggsql_layer_0__" ) . unwrap ( ) ;
391+
392+ // Sort by facet then fill so we can assert in predictable order
393+ let result_df = result_df
394+ . clone ( )
395+ . lazy ( )
396+ . sort_by_exprs (
397+ [ col ( "__ggsql_aes_facet1__" ) , col ( "__ggsql_aes_fill__" ) ] ,
398+ SortMultipleOptions :: default ( ) ,
399+ )
400+ . collect ( )
401+ . unwrap ( ) ;
402+
403+ let pos2 = result_df
404+ . column ( "__ggsql_aes_pos2__" )
405+ . unwrap ( )
406+ . f64 ( )
407+ . unwrap ( ) ;
408+ let pos2end = result_df
409+ . column ( "__ggsql_aes_pos2end__" )
410+ . unwrap ( )
411+ . f64 ( )
412+ . unwrap ( ) ;
413+
414+ let pos2_vals: Vec < f64 > = pos2. into_iter ( ) . flatten ( ) . collect ( ) ;
415+ let pos2end_vals: Vec < f64 > = pos2end. into_iter ( ) . flatten ( ) . collect ( ) ;
416+
417+ // Expected (sorted by facet, fill):
418+ // F1/X: pos2end=0, pos2=10 (first in panel, starts at 0)
419+ // F1/Y: pos2end=10, pos2=30 (stacks on X)
420+ // F2/X: pos2end=0, pos2=30 (first in panel, should reset to 0)
421+ // F2/Y: pos2end=30, pos2=70 (stacks on X)
422+ assert_eq ! (
423+ pos2end_vals[ 2 ] , 0.0 ,
424+ "F2 panel first bar should start at 0, not carry over from F1. pos2end={:?}, pos2={:?}" ,
425+ pos2end_vals, pos2_vals
426+ ) ;
427+ }
428+
429+ #[ test]
430+ fn test_dodge_ignores_facet_columns_in_group_count ( ) {
431+ // Dodge should compute n_groups per facet panel, not globally.
432+ // With fill=["X","Y"] and facet=["F1","F2"], dodge should see
433+ // 2 groups (X, Y) not 4 (X-F1, X-F2, Y-F1, Y-F2).
434+ //
435+ // With 2 groups and default width 0.9:
436+ // adjusted_width = 0.9 / 2 = 0.45
437+ // offsets: -0.225 (group X), +0.225 (group Y)
438+ //
439+ // If facet columns incorrectly inflate n_groups to 4:
440+ // adjusted_width = 0.9 / 4 = 0.225
441+ // offsets would be different (spread across 4 positions)
442+ let df = df ! {
443+ "__ggsql_aes_pos1__" => [ "A" , "A" , "A" , "A" ] ,
444+ "__ggsql_aes_pos2__" => [ 10.0 , 20.0 , 30.0 , 40.0 ] ,
445+ "__ggsql_aes_pos2end__" => [ 0.0 , 0.0 , 0.0 , 0.0 ] ,
446+ "__ggsql_aes_fill__" => [ "X" , "Y" , "X" , "Y" ] ,
447+ "__ggsql_aes_facet1__" => [ "F1" , "F1" , "F2" , "F2" ] ,
448+ }
449+ . unwrap ( ) ;
450+
451+ let mut layer = crate :: plot:: Layer :: new ( Geom :: bar ( ) ) ;
452+ layer. mappings = {
453+ let mut m = Mappings :: new ( ) ;
454+ m. insert (
455+ "pos1" ,
456+ AestheticValue :: standard_column ( "__ggsql_aes_pos1__" ) ,
457+ ) ;
458+ m. insert (
459+ "pos2" ,
460+ AestheticValue :: standard_column ( "__ggsql_aes_pos2__" ) ,
461+ ) ;
462+ m. insert (
463+ "pos2end" ,
464+ AestheticValue :: standard_column ( "__ggsql_aes_pos2end__" ) ,
465+ ) ;
466+ m. insert (
467+ "fill" ,
468+ AestheticValue :: standard_column ( "__ggsql_aes_fill__" ) ,
469+ ) ;
470+ m. insert (
471+ "facet1" ,
472+ AestheticValue :: standard_column ( "__ggsql_aes_facet1__" ) ,
473+ ) ;
474+ m
475+ } ;
476+ layer. partition_by = vec ! [
477+ "__ggsql_aes_fill__" . to_string( ) ,
478+ "__ggsql_aes_facet1__" . to_string( ) ,
479+ ] ;
480+ layer. position = Position :: dodge ( ) ;
481+ layer. data_key = Some ( "__ggsql_layer_0__" . to_string ( ) ) ;
482+
483+ let mut spec = Plot :: new ( ) ;
484+ spec. scales . push ( make_discrete_scale ( "pos1" ) ) ;
485+ spec. scales . push ( make_continuous_scale ( "pos2" ) ) ;
486+ spec. facet = Some ( Facet :: new ( FacetLayout :: Wrap {
487+ variables : vec ! [ "facet_var" . to_string( ) ] ,
488+ } ) ) ;
489+ let mut data_map = HashMap :: new ( ) ;
490+ data_map. insert ( "__ggsql_layer_0__" . to_string ( ) , df) ;
491+
492+ spec. layers . push ( layer) ;
493+
494+ apply_position_adjustments ( & mut spec, & mut data_map) . unwrap ( ) ;
495+
496+ // With 2 groups (X, Y), adjusted_width should be 0.45
497+ let adjusted = spec. layers [ 0 ] . adjusted_width . unwrap ( ) ;
498+ assert ! (
499+ ( adjusted - 0.45 ) . abs( ) < 0.001 ,
500+ "adjusted_width should be 0.45 (2 groups), got {} (facet columns inflated group count)" ,
501+ adjusted
502+ ) ;
503+ }
325504}
0 commit comments