@@ -368,6 +368,131 @@ mod duckdb_tests {
368368 assert ! ( dataframe. column( "__ggsql_aes_pos1__" ) . is_ok( ) ) ;
369369 assert ! ( dataframe. column( "__ggsql_aes_pos2__" ) . is_ok( ) ) ;
370370 }
371+
372+ #[ test]
373+ fn test_ribbon_transposed_orientation ( ) {
374+ use crate :: naming;
375+ use crate :: plot:: layer:: orientation:: Orientation ;
376+
377+ let reader =
378+ crate :: reader:: DuckDBReader :: from_connection_string ( "duckdb://memory" ) . unwrap ( ) ;
379+
380+ // Ribbon with y as domain axis and xmin/xmax as value range (transposed)
381+ let query =
382+ "VISUALISE FROM ggsql:airquality DRAW ribbon MAPPING Day AS y, Temp AS xmax, 0.0 AS xmin" ;
383+ let result = crate :: execute:: prepare_data_with_reader ( query, & reader) ;
384+
385+ // Debug: print the error if any
386+ if let Err ( ref e) = result {
387+ eprintln ! ( "Error: {:?}" , e) ;
388+ }
389+
390+ let result = result. unwrap ( ) ;
391+
392+ // Debug: print orientation and scales
393+ let layer = & result. specs [ 0 ] . layers [ 0 ] ;
394+ eprintln ! ( "Layer orientation: {:?}" , layer. orientation) ;
395+ eprintln ! (
396+ "Scales: {:?}" ,
397+ result. specs[ 0 ]
398+ . scales
399+ . iter( )
400+ . map( |s| ( & s. aesthetic, & s. scale_type) )
401+ . collect:: <Vec <_>>( )
402+ ) ;
403+ eprintln ! (
404+ "Layer mappings: {:?}" ,
405+ layer. mappings. aesthetics. keys( ) . collect:: <Vec <_>>( )
406+ ) ;
407+
408+ // Check orientation was detected correctly
409+ assert_eq ! (
410+ layer. orientation,
411+ Some ( Orientation :: Transposed ) ,
412+ "Should detect Transposed orientation"
413+ ) ;
414+
415+ let dataframe = result. data . get ( & naming:: layer_key ( 0 ) ) . unwrap ( ) ;
416+
417+ // The flip-back restores user's original axis assignment:
418+ // After flip-back:
419+ // - pos2 = y (user's domain axis = Date/Day)
420+ // - pos1min = xmin (user's value range min = 0.0)
421+ // - pos1max = xmax (user's value range max = Temp)
422+ // The orientation flag tells the writer how to map to x/y.
423+ let cols: Vec < _ > = dataframe. get_column_names ( ) . into_iter ( ) . collect ( ) ;
424+ eprintln ! ( "Columns: {:?}" , cols) ;
425+
426+ assert ! (
427+ dataframe. column( "__ggsql_aes_pos2__" ) . is_ok( ) ,
428+ "Should have pos2 (domain axis), got columns: {:?}" ,
429+ cols
430+ ) ;
431+ assert ! (
432+ dataframe. column( "__ggsql_aes_pos1min__" ) . is_ok( ) ,
433+ "Should have pos1min (value range min), got columns: {:?}" ,
434+ cols
435+ ) ;
436+ assert ! (
437+ dataframe. column( "__ggsql_aes_pos1max__" ) . is_ok( ) ,
438+ "Should have pos1max (value range max), got columns: {:?}" ,
439+ cols
440+ ) ;
441+ }
442+
443+ #[ test]
444+ fn test_ribbon_transposed_vegalite_encoding ( ) {
445+ use crate :: reader:: Reader ;
446+ use crate :: writer:: { VegaLiteWriter , Writer } ;
447+
448+ let reader =
449+ crate :: reader:: DuckDBReader :: from_connection_string ( "duckdb://memory" ) . unwrap ( ) ;
450+
451+ // Ribbon with y as domain axis and xmin/xmax as value range (transposed)
452+ let query =
453+ "VISUALISE FROM ggsql:airquality DRAW ribbon MAPPING Day AS y, Temp AS xmax, 0.0 AS xmin" ;
454+ let spec = reader. execute ( query) . unwrap ( ) ;
455+
456+ let writer = VegaLiteWriter :: new ( ) ;
457+ let json_str = writer. render ( & spec) . unwrap ( ) ;
458+ let vl_spec: serde_json:: Value = serde_json:: from_str ( & json_str) . unwrap ( ) ;
459+
460+ // For transposed ribbon, the encoding should have:
461+ // - y: domain axis (Day)
462+ // - x: value range max (Temp via xmax)
463+ // - x2: value range min (0.0 via xmin)
464+ // The encoding is inside layer[0] since VegaLite uses layered structure
465+ let encoding = & vl_spec[ "layer" ] [ 0 ] [ "encoding" ] ;
466+ assert ! (
467+ encoding. get( "y" ) . is_some( ) ,
468+ "Should have y encoding for domain axis"
469+ ) ;
470+ assert ! (
471+ encoding. get( "x" ) . is_some( ) ,
472+ "Should have x encoding for value max"
473+ ) ;
474+ assert ! (
475+ encoding. get( "x2" ) . is_some( ) ,
476+ "Should have x2 encoding for value min"
477+ ) ;
478+ // Should NOT have ymax/ymin/xmax/xmin (these should be remapped to x/x2/y/y2)
479+ assert ! (
480+ encoding. get( "ymax" ) . is_none( ) ,
481+ "Should not have ymax encoding"
482+ ) ;
483+ assert ! (
484+ encoding. get( "ymin" ) . is_none( ) ,
485+ "Should not have ymin encoding"
486+ ) ;
487+ assert ! (
488+ encoding. get( "xmax" ) . is_none( ) ,
489+ "Should not have xmax encoding"
490+ ) ;
491+ assert ! (
492+ encoding. get( "xmin" ) . is_none( ) ,
493+ "Should not have xmin encoding"
494+ ) ;
495+ }
371496}
372497
373498#[ cfg( feature = "builtin-data" ) ]
0 commit comments