@@ -63,6 +63,19 @@ module.exports = function draw(gd) {
6363 . call ( Color . fill , opts . bgcolor )
6464 . style ( 'stroke-width' , opts . borderwidth + 'px' ) ;
6565
66+
67+ var title = fullLayout . legend . title ;
68+ gd . _fullLayout . _legendTitleWidth = 0 ;
69+ gd . _fullLayout . _legendTitleHeight = 0 ;
70+ if ( title . text ) {
71+ var titleEl = Lib . ensureSingle ( legend , 'text' , 'legendtitletext' ) ;
72+ titleEl . attr ( 'text-anchor' , 'start' )
73+ . classed ( 'user-select-none' , true )
74+ . call ( Drawing . font , title . font )
75+ . text ( title . text ) ;
76+
77+ textLayout ( titleEl , legend , gd ) ; // handle mathjax or multi-line text and compute title height
78+ }
6679 var scrollBox = Lib . ensureSingle ( legend , 'g' , 'scrollbox' ) ;
6780
6881 var scrollBar = Lib . ensureSingle ( legend , 'rect' , 'scrollbar' , function ( s ) {
@@ -121,7 +134,7 @@ module.exports = function draw(gd) {
121134 }
122135
123136 // Set size and position of all the elements that make up a legend:
124- // legend, background and border, scroll box and scroll bar
137+ // legend, background and border, scroll box and scroll bar as well as title
125138 Drawing . setTranslate ( legend , lx , ly ) ;
126139
127140 // to be safe, remove previous listeners
@@ -375,18 +388,12 @@ function drawTexts(g, gd) {
375388
376389 svgTextUtils . positionText ( textEl , constants . textGap , 0 ) ;
377390
378- function textLayout ( s ) {
379- svgTextUtils . convertToTspans ( s , gd , function ( ) {
380- computeTextDimensions ( g , gd ) ;
381- } ) ;
382- }
383-
384391 if ( isEditable ) {
385392 textEl . call ( svgTextUtils . makeEditable , { gd : gd , text : name } )
386- . call ( textLayout )
393+ . call ( textLayout , g , gd )
387394 . on ( 'edit' , function ( newName ) {
388395 this . text ( ensureLength ( newName , maxNameLength ) )
389- . call ( textLayout ) ;
396+ . call ( textLayout , g , gd ) ;
390397
391398 var fullInput = legendItem . trace . _fullInput || { } ;
392399 var update = { } ;
@@ -407,7 +414,7 @@ function drawTexts(g, gd) {
407414 return Registry . call ( '_guiRestyle' , gd , update , traceIndex ) ;
408415 } ) ;
409416 } else {
410- textLayout ( textEl ) ;
417+ textLayout ( textEl , g , gd ) ;
411418 }
412419}
413420
@@ -460,17 +467,25 @@ function setupTraceToggle(g, gd) {
460467 } ) ;
461468}
462469
470+ function textLayout ( s , g , gd ) {
471+ svgTextUtils . convertToTspans ( s , gd , function ( ) {
472+ computeTextDimensions ( g , gd ) ;
473+ } ) ;
474+ }
475+
463476function computeTextDimensions ( g , gd ) {
464477 var legendItem = g . data ( ) [ 0 ] [ 0 ] ;
465-
466- if ( ! legendItem . trace . showlegend ) {
478+ if ( legendItem && ! legendItem . trace . showlegend ) {
467479 g . remove ( ) ;
468480 return ;
469481 }
470482
471483 var mathjaxGroup = g . select ( 'g[class*=math-group]' ) ;
472484 var mathjaxNode = mathjaxGroup . node ( ) ;
473- var opts = gd . _fullLayout . legend ;
485+ var bw = gd . _fullLayout . legend . borderwidth ;
486+ var opts = legendItem ?
487+ gd . _fullLayout . legend :
488+ gd . _fullLayout . legend . title ;
474489 var lineHeight = opts . font . size * LINE_SPACING ;
475490 var height , width ;
476491
@@ -480,24 +495,43 @@ function computeTextDimensions(g, gd) {
480495 height = mathjaxBB . height ;
481496 width = mathjaxBB . width ;
482497
483- Drawing . setTranslate ( mathjaxGroup , 0 , ( height / 4 ) ) ;
498+ if ( legendItem ) {
499+ Drawing . setTranslate ( mathjaxGroup , 0 , height * 0.25 ) ;
500+ } else { // case of title
501+ Drawing . setTranslate ( mathjaxGroup , bw , height * 0.75 + bw ) ;
502+ }
484503 } else {
485- var text = g . select ( '.legendtext' ) ;
486- var textLines = svgTextUtils . lineCount ( text ) ;
487- var textNode = text . node ( ) ;
504+ var textEl = g . select ( legendItem ?
505+ '.legendtext' : '.legendtitletext'
506+ ) ;
507+ var textLines = svgTextUtils . lineCount ( textEl ) ;
508+ var textNode = textEl . node ( ) ;
488509
489510 height = lineHeight * textLines ;
490511 width = textNode ? Drawing . bBox ( textNode ) . width : 0 ;
491512
492513 // approximation to height offset to center the font
493514 // to avoid getBoundingClientRect
494- var textY = lineHeight * ( 0.3 + ( 1 - textLines ) / 2 ) ;
495- svgTextUtils . positionText ( text , constants . textGap , textY ) ;
515+ var textY = lineHeight * ( ( textLines - 1 ) / 2 - 0.3 ) ;
516+ if ( legendItem ) {
517+ svgTextUtils . positionText ( textEl , constants . textGap , - textY ) ;
518+ } else { // case of title
519+ svgTextUtils . positionText ( textEl , constants . titlePad + bw , lineHeight + bw ) ;
520+ }
496521 }
497522
498- legendItem . lineHeight = lineHeight ;
499- legendItem . height = Math . max ( height , 16 ) + 3 ;
500- legendItem . width = width ;
523+ if ( legendItem ) {
524+ legendItem . lineHeight = lineHeight ;
525+ legendItem . height = Math . max ( height , 16 ) + 3 ;
526+ legendItem . width = width ;
527+ } else { // case of title
528+ if ( opts . side . indexOf ( 'left' ) !== - 1 ) {
529+ gd . _fullLayout . _legendTitleWidth = width ;
530+ }
531+ if ( opts . side . indexOf ( 'top' ) !== - 1 ) {
532+ gd . _fullLayout . _legendTitleHeight = height ;
533+ }
534+ }
501535}
502536
503537/*
@@ -514,6 +548,9 @@ function computeLegendDimensions(gd, groups, traces) {
514548 var fullLayout = gd . _fullLayout ;
515549 var opts = fullLayout . legend ;
516550 var gs = fullLayout . _size ;
551+ opts . _titleWidth = fullLayout . _legendTitleWidth ;
552+ opts . _titleHeight = fullLayout . _legendTitleHeight ;
553+
517554 var isVertical = helpers . isVertical ( opts ) ;
518555 var isGrouped = helpers . isGrouped ( opts ) ;
519556
@@ -541,7 +578,10 @@ function computeLegendDimensions(gd, groups, traces) {
541578 if ( isVertical ) {
542579 traces . each ( function ( d ) {
543580 var h = d [ 0 ] . height ;
544- Drawing . setTranslate ( this , bw , itemGap + bw + opts . _height + h / 2 ) ;
581+ Drawing . setTranslate ( this ,
582+ bw + opts . _titleWidth ,
583+ bw + opts . _titleHeight + opts . _height + h / 2 + itemGap
584+ ) ;
545585 opts . _height += h ;
546586 opts . _width = Math . max ( opts . _width , d [ 0 ] . width ) ;
547587 } ) ;
@@ -591,7 +631,10 @@ function computeLegendDimensions(gd, groups, traces) {
591631 var offsetY = 0 ;
592632 d3 . select ( this ) . selectAll ( 'g.traces' ) . each ( function ( d ) {
593633 var h = d [ 0 ] . height ;
594- Drawing . setTranslate ( this , 0 , itemGap + bw + h / 2 + offsetY ) ;
634+ Drawing . setTranslate ( this ,
635+ opts . _titleWidth ,
636+ bw + opts . _titleHeight + itemGap + h / 2 + offsetY
637+ ) ;
595638 offsetY += h ;
596639 maxWidthInGroup = Math . max ( maxWidthInGroup , textGap + d [ 0 ] . width ) ;
597640 } ) ;
@@ -634,7 +677,10 @@ function computeLegendDimensions(gd, groups, traces) {
634677 maxItemHeightInRow = 0 ;
635678 }
636679
637- Drawing . setTranslate ( this , bw + offsetX , itemGap + bw + h / 2 + offsetY ) ;
680+ Drawing . setTranslate ( this ,
681+ bw + opts . _titleWidth + offsetX ,
682+ bw + opts . _titleHeight + offsetY + h / 2 + itemGap
683+ ) ;
638684
639685 rowWidth = offsetX + w + itemGap ;
640686 offsetX += next ;
@@ -651,8 +697,8 @@ function computeLegendDimensions(gd, groups, traces) {
651697 }
652698 }
653699
654- opts . _width = Math . ceil ( opts . _width ) ;
655- opts . _height = Math . ceil ( opts . _height ) ;
700+ opts . _width = Math . ceil ( opts . _width + opts . _titleWidth ) ;
701+ opts . _height = Math . ceil ( opts . _height + opts . _titleHeight ) ;
656702
657703 opts . _effHeight = Math . min ( opts . _height , opts . _maxHeight ) ;
658704
0 commit comments