@@ -83,6 +83,21 @@ describe('legend defaults', function () {
8383 expect ( layoutOut . showlegend ) . toBe ( false ) ;
8484 } ) ;
8585
86+ it ( 'defaults itemheight to 6 and clamps values below the minimum' , function ( ) {
87+ fullData = allShown ( [ { type : 'scatter' } , { type : 'scatter' } ] ) ;
88+
89+ supplyLayoutDefaults ( { } , layoutOut , fullData ) ;
90+ expect ( layoutOut . legend . itemheight ) . toBe ( 6 ) ;
91+
92+ layoutOut = { font : Plots . layoutAttributes . font , bg_color : Plots . layoutAttributes . bg_color } ;
93+ supplyLayoutDefaults ( { showlegend : true , legend : { itemheight : 1 } } , layoutOut , fullData ) ;
94+ expect ( layoutOut . legend . itemheight ) . toBe ( 6 ) ;
95+
96+ layoutOut = { font : Plots . layoutAttributes . font , bg_color : Plots . layoutAttributes . bg_color } ;
97+ supplyLayoutDefaults ( { showlegend : true , legend : { itemheight : 24 } } , layoutOut , fullData ) ;
98+ expect ( layoutOut . legend . itemheight ) . toBe ( 24 ) ;
99+ } ) ;
100+
86101 it ( 'shows with one visible pie' , function ( ) {
87102 fullData = allShown ( [ { type : 'pie' } ] ) ;
88103
@@ -3590,3 +3605,182 @@ describe('legend title click', function() {
35903605 } ) . then ( done , done . fail ) ;
35913606 } ) ;
35923607} ) ;
3608+
3609+ describe ( 'legend itemheight:' , function ( ) {
3610+ 'use strict' ;
3611+
3612+ var gd ;
3613+
3614+ beforeEach ( function ( ) {
3615+ gd = createGraphDiv ( ) ;
3616+ } ) ;
3617+
3618+ afterEach ( destroyGraphDiv ) ;
3619+
3620+ function fillPathD ( ) {
3621+ return d3Select ( gd ) . select ( 'g.legendfill' ) . select ( 'path' ) . attr ( 'd' ) ;
3622+ }
3623+
3624+ function linePathD ( ) {
3625+ return d3Select ( gd ) . select ( 'g.legendlines' ) . select ( 'path' ) . attr ( 'd' ) ;
3626+ }
3627+
3628+ // The toggle rect is sized to the computed row height, see setRect in draw.js
3629+ function rowHeights ( ) {
3630+ var heights = [ ] ;
3631+ d3Select ( gd )
3632+ . selectAll ( 'rect.legendtoggle' )
3633+ . each ( function ( ) {
3634+ heights . push ( + this . getAttribute ( 'height' ) ) ;
3635+ } ) ;
3636+ return heights ;
3637+ }
3638+
3639+ var filled = [
3640+ { x : [ 1 , 2 ] , y : [ 1 , 2 ] , fill : 'tozeroy' , name : 'a' } ,
3641+ { x : [ 1 , 2 ] , y : [ 2 , 3 ] , fill : 'tozeroy' , name : 'b' }
3642+ ] ;
3643+
3644+ it ( 'reproduces the historical 6px swatch at the default' , function ( done ) {
3645+ Plotly . newPlot ( gd , filled , { showlegend : true } )
3646+ . then ( function ( ) {
3647+ expect ( gd . _fullLayout . legend . itemheight ) . toBe ( 6 ) ;
3648+ expect ( fillPathD ( ) ) . toBe ( 'M5,0h30v6h-30z' ) ;
3649+ } )
3650+ . then ( done , done . fail ) ;
3651+ } ) ;
3652+
3653+ it ( 'grows the fill swatch to the requested height' , function ( done ) {
3654+ Plotly . newPlot ( gd , filled , { showlegend : true , legend : { itemheight : 24 } } )
3655+ . then ( function ( ) {
3656+ expect ( fillPathD ( ) ) . toBe ( 'M5,0h30v24h-30z' ) ;
3657+ } )
3658+ . then ( done , done . fail ) ;
3659+ } ) ;
3660+
3661+ it ( 'keeps the line on the top edge of a taller fill' , function ( done ) {
3662+ var dfltLine ;
3663+
3664+ Plotly . newPlot ( gd , filled , { showlegend : true } )
3665+ . then ( function ( ) {
3666+ dfltLine = linePathD ( ) ;
3667+ return Plotly . relayout ( gd , 'legend.itemheight' , 30 ) ;
3668+ } )
3669+ . then ( function ( ) {
3670+ // the line marks the top edge of the fill, exactly as in the plot itself
3671+ expect ( linePathD ( ) ) . toBe ( dfltLine ) ;
3672+ expect ( fillPathD ( ) ) . toBe ( 'M5,0h30v30h-30z' ) ;
3673+ } )
3674+ . then ( done , done . fail ) ;
3675+ } ) ;
3676+
3677+ it ( 'does not move the swatch of a trace without fill' , function ( done ) {
3678+ var unfilled = [
3679+ { x : [ 1 , 2 ] , y : [ 1 , 2 ] , name : 'a' } ,
3680+ { x : [ 1 , 2 ] , y : [ 2 , 3 ] , name : 'b' }
3681+ ] ;
3682+ var dfltLine ;
3683+
3684+ Plotly . newPlot ( gd , unfilled , { showlegend : true } )
3685+ . then ( function ( ) {
3686+ dfltLine = linePathD ( ) ;
3687+ return Plotly . relayout ( gd , 'legend.itemheight' , 40 ) ;
3688+ } )
3689+ . then ( function ( ) {
3690+ expect ( linePathD ( ) ) . toBe ( dfltLine ) ;
3691+ } )
3692+ . then ( done , done . fail ) ;
3693+ } ) ;
3694+
3695+ it ( 'grows each legend row so taller swatches do not overlap' , function ( done ) {
3696+ var dflt ;
3697+
3698+ Plotly . newPlot ( gd , filled , { showlegend : true } )
3699+ . then ( function ( ) {
3700+ dflt = rowHeights ( ) ;
3701+ expect ( dflt . length ) . toBe ( 2 ) ;
3702+ return Plotly . relayout ( gd , 'legend.itemheight' , 40 ) ;
3703+ } )
3704+ . then ( function ( ) {
3705+ var grown = rowHeights ( ) ;
3706+ expect ( grown . length ) . toBe ( dflt . length ) ;
3707+ grown . forEach ( function ( h , i ) {
3708+ expect ( h ) . toBeGreaterThan ( dflt [ i ] ) ;
3709+ // itemMinHeight + itemheight - dfltFillHeight + itemHeightPad = 16 + 40 - 6 + 3
3710+ expect ( h ) . toBe ( 53 ) ;
3711+ } ) ;
3712+ } )
3713+ . then ( done , done . fail ) ;
3714+ } ) ;
3715+
3716+ it ( 'leaves row heights untouched at the default' , function ( done ) {
3717+ var dflt ;
3718+
3719+ Plotly . newPlot ( gd , filled , { showlegend : true } )
3720+ . then ( function ( ) {
3721+ dflt = rowHeights ( ) ;
3722+ return Plotly . relayout ( gd , 'legend.itemheight' , 6 ) ;
3723+ } )
3724+ . then ( function ( ) {
3725+ expect ( rowHeights ( ) ) . toEqual ( dflt ) ;
3726+ } )
3727+ . then ( done , done . fail ) ;
3728+ } ) ;
3729+
3730+ it ( 'keeps the default space above and below a taller symbol' , ( done ) => {
3731+ // Space between the row edges and the symbol (fill, line, and marker) of each legend item
3732+ const symbolGaps = ( ) =>
3733+ [ ...gd . querySelectorAll ( 'g.traces' ) ] . map ( ( item ) => {
3734+ const row = item . querySelector ( 'rect.legendtoggle' ) . getBoundingClientRect ( ) ;
3735+ const parts = [
3736+ ...item . querySelectorAll ( 'g.legendfill path, g.legendlines path, g.legendpoints path' )
3737+ ] . map ( ( el ) => el . getBoundingClientRect ( ) ) ;
3738+ return {
3739+ above : Math . min ( ...parts . map ( ( r ) => r . top ) ) - row . top ,
3740+ below : row . bottom - Math . max ( ...parts . map ( ( r ) => r . bottom ) )
3741+ } ;
3742+ } ) ;
3743+ let dfltGaps ;
3744+
3745+ Plotly . newPlot ( gd , filled , { showlegend : true } )
3746+ . then ( ( ) => {
3747+ dfltGaps = symbolGaps ( ) ;
3748+ return Plotly . relayout ( gd , 'legend.itemheight' , 40 ) ;
3749+ } )
3750+ . then ( ( ) => {
3751+ const gaps = symbolGaps ( ) ;
3752+ expect ( gaps . length ) . toBe ( dfltGaps . length ) ;
3753+ gaps . forEach ( ( gap , i ) => {
3754+ expect ( gap . above ) . toBeCloseTo ( dfltGaps [ i ] . above , 1 ) ;
3755+ expect ( gap . below ) . toBeCloseTo ( dfltGaps [ i ] . below , 1 ) ;
3756+ } ) ;
3757+ } )
3758+ . then ( done , done . fail ) ;
3759+ } ) ;
3760+
3761+ it ( 'keeps the default swatch in unified hover labels' , ( done ) => {
3762+ Plotly . newPlot ( gd , filled , {
3763+ showlegend : true ,
3764+ hovermode : 'x unified' ,
3765+ legend : { itemheight : 18 }
3766+ } )
3767+ . then ( ( ) => {
3768+ Plotly . Fx . hover ( gd , { xval : 1 } ) ;
3769+ Lib . clearThrottle ( ) ;
3770+
3771+ // The hover legend does not inherit layout.legend, the same as for itemwidth
3772+ const hoverFills = [ ...gd . querySelectorAll ( 'g.hoverlayer g.legendfill path' ) ] ;
3773+ expect ( hoverFills . length ) . toBe ( 2 ) ;
3774+ hoverFills . forEach ( ( fillPath ) => {
3775+ expect ( fillPath . getAttribute ( 'd' ) ) . toBe ( 'M5,0h30v6h-30z' ) ;
3776+ } ) ;
3777+
3778+ const legendFills = [ ...gd . querySelectorAll ( 'g.infolayer g.legendfill path' ) ] ;
3779+ expect ( legendFills . length ) . toBe ( 2 ) ;
3780+ legendFills . forEach ( ( fillPath ) => {
3781+ expect ( fillPath . getAttribute ( 'd' ) ) . toBe ( 'M5,0h30v18h-30z' ) ;
3782+ } ) ;
3783+ } )
3784+ . then ( done , done . fail ) ;
3785+ } ) ;
3786+ } ) ;
0 commit comments