@@ -49,6 +49,24 @@ const POINTS_TO_PIXELS: f64 = 96.0 / 72.0;
4949/// So: area_px^2 = pi * (r_pt * POINTS_TO_PIXELS)^2 = pi * r_pt^2 * (96/72)^2
5050const POINTS_TO_AREA : f64 = std:: f64:: consts:: PI * POINTS_TO_PIXELS * POINTS_TO_PIXELS ;
5151
52+ /// Split a label string on newlines and return appropriate JSON value
53+ ///
54+ /// Returns a JSON array if the string contains multiple lines, or a JSON string
55+ /// if it's a single line. Handles both actual newlines (from database columns,
56+ /// CHAR(10), imported data) and literal \\n (from SQL string literals).
57+ fn split_label_on_newlines ( label : & str ) -> Value {
58+ // Normalize literal \\n to actual \n, then split on any newline type
59+ let normalized = label. replace ( "\\ n" , "\n " ) ;
60+ let lines: Vec < & str > = normalized. lines ( ) . collect ( ) ;
61+
62+ // Return array if multiple lines, string if single line
63+ if lines. len ( ) > 1 {
64+ json ! ( lines)
65+ } else {
66+ json ! ( label)
67+ }
68+ }
69+
5270/// Result of preparing layer data for rendering
5371///
5472/// Contains the datasets, renderers, and prepared data needed to build Vega-Lite layers.
@@ -1097,13 +1115,20 @@ impl Writer for VegaLiteWriter {
10971115 match ( title, subtitle) {
10981116 ( Some ( t) , Some ( st) ) => {
10991117 // Vega-Lite uses an object for title + subtitle
1100- vl_spec[ "title" ] = json ! ( { "text" : t, "subtitle" : st} ) ;
1118+ // Split both title and subtitle on newlines
1119+ vl_spec[ "title" ] = json ! ( {
1120+ "text" : split_label_on_newlines( t) ,
1121+ "subtitle" : split_label_on_newlines( st)
1122+ } ) ;
11011123 }
11021124 ( Some ( t) , None ) => {
1103- vl_spec[ "title" ] = json ! ( t) ;
1125+ vl_spec[ "title" ] = split_label_on_newlines ( t) ;
11041126 }
11051127 ( None , Some ( st) ) => {
1106- vl_spec[ "title" ] = json ! ( { "text" : "" , "subtitle" : st} ) ;
1128+ vl_spec[ "title" ] = json ! ( {
1129+ "text" : "" ,
1130+ "subtitle" : split_label_on_newlines( st)
1131+ } ) ;
11071132 }
11081133 ( None , None ) => { }
11091134 }
@@ -1558,6 +1583,94 @@ mod tests {
15581583 assert_eq ! ( vl_spec[ "layer" ] [ 0 ] [ "mark" ] [ "clip" ] , true ) ;
15591584 }
15601585
1586+ #[ test]
1587+ fn test_labels_newline_splitting ( ) {
1588+ use crate :: execute;
1589+ use crate :: reader:: DuckDBReader ;
1590+
1591+ // Test that LABEL clause values with newlines are split into arrays
1592+
1593+ let reader = DuckDBReader :: from_connection_string ( "duckdb://memory" ) . unwrap ( ) ;
1594+
1595+ let query = r#"
1596+ SELECT
1597+ n::INTEGER as x,
1598+ n::INTEGER as y,
1599+ CASE WHEN n % 2 = 0 THEN 'Group A' ELSE 'Group B' END as category
1600+ FROM generate_series(0, 2) as t(n)
1601+ VISUALISE x, y, category AS stroke
1602+ DRAW point
1603+ LABEL
1604+ title => 'Multi-line\nChart Title',
1605+ subtitle => 'Line 1\nLine 2\nLine 3',
1606+ x => 'X Axis\nWith Newline',
1607+ y => 'Single Line',
1608+ stroke => 'Category\nMulti-line Legend'
1609+ "# ;
1610+
1611+ let prepared = execute:: prepare_data_with_reader ( query, & reader) . unwrap ( ) ;
1612+ let spec = & prepared. specs [ 0 ] ;
1613+
1614+ let writer = VegaLiteWriter :: new ( ) ;
1615+ let json_str = writer. write ( spec, & prepared. data ) . unwrap ( ) ;
1616+ let vl_spec: Value = serde_json:: from_str ( & json_str) . unwrap ( ) ;
1617+
1618+ // Check title (should be object with text and subtitle)
1619+ assert ! ( vl_spec[ "title" ] . is_object( ) , "Title should be an object" ) ;
1620+ let title_obj = vl_spec[ "title" ] . as_object ( ) . unwrap ( ) ;
1621+
1622+ // Check title text (multi-line, should be array)
1623+ assert ! (
1624+ title_obj[ "text" ] . is_array( ) ,
1625+ "Title with newline should be an array"
1626+ ) ;
1627+ let title_lines = title_obj[ "text" ] . as_array ( ) . unwrap ( ) ;
1628+ assert_eq ! ( title_lines. len( ) , 2 ) ;
1629+ assert_eq ! ( title_lines[ 0 ] . as_str( ) . unwrap( ) , "Multi-line" ) ;
1630+ assert_eq ! ( title_lines[ 1 ] . as_str( ) . unwrap( ) , "Chart Title" ) ;
1631+
1632+ // Check subtitle (multi-line, should be array)
1633+ assert ! (
1634+ title_obj[ "subtitle" ] . is_array( ) ,
1635+ "Subtitle with newlines should be an array"
1636+ ) ;
1637+ let subtitle_lines = title_obj[ "subtitle" ] . as_array ( ) . unwrap ( ) ;
1638+ assert_eq ! ( subtitle_lines. len( ) , 3 ) ;
1639+ assert_eq ! ( subtitle_lines[ 0 ] . as_str( ) . unwrap( ) , "Line 1" ) ;
1640+ assert_eq ! ( subtitle_lines[ 1 ] . as_str( ) . unwrap( ) , "Line 2" ) ;
1641+ assert_eq ! ( subtitle_lines[ 2 ] . as_str( ) . unwrap( ) , "Line 3" ) ;
1642+
1643+ // Check x axis label (multi-line, should be array)
1644+ let x_encoding = & vl_spec[ "layer" ] [ 0 ] [ "encoding" ] [ "x" ] ;
1645+ assert ! (
1646+ x_encoding[ "title" ] . is_array( ) ,
1647+ "X axis label with newline should be an array"
1648+ ) ;
1649+ let x_label_lines = x_encoding[ "title" ] . as_array ( ) . unwrap ( ) ;
1650+ assert_eq ! ( x_label_lines. len( ) , 2 ) ;
1651+ assert_eq ! ( x_label_lines[ 0 ] . as_str( ) . unwrap( ) , "X Axis" ) ;
1652+ assert_eq ! ( x_label_lines[ 1 ] . as_str( ) . unwrap( ) , "With Newline" ) ;
1653+
1654+ // Check y axis label (single line, should be string)
1655+ let y_encoding = & vl_spec[ "layer" ] [ 0 ] [ "encoding" ] [ "y" ] ;
1656+ assert ! (
1657+ y_encoding[ "title" ] . is_string( ) ,
1658+ "Y axis label without newline should be a string"
1659+ ) ;
1660+ assert_eq ! ( y_encoding[ "title" ] . as_str( ) . unwrap( ) , "Single Line" ) ;
1661+
1662+ // Check stroke legend label (multi-line, should be array)
1663+ let stroke_encoding = & vl_spec[ "layer" ] [ 0 ] [ "encoding" ] [ "stroke" ] ;
1664+ assert ! (
1665+ stroke_encoding[ "title" ] . is_array( ) ,
1666+ "Stroke legend title with newline should be an array"
1667+ ) ;
1668+ let stroke_label_lines = stroke_encoding[ "title" ] . as_array ( ) . unwrap ( ) ;
1669+ assert_eq ! ( stroke_label_lines. len( ) , 2 ) ;
1670+ assert_eq ! ( stroke_label_lines[ 0 ] . as_str( ) . unwrap( ) , "Category" ) ;
1671+ assert_eq ! ( stroke_label_lines[ 1 ] . as_str( ) . unwrap( ) , "Multi-line Legend" ) ;
1672+ }
1673+
15611674 #[ test]
15621675 fn test_fontsize_linear_scaling ( ) {
15631676 use crate :: plot:: { ArrayElement , OutputRange , Scale , ScaleType } ;
0 commit comments