@@ -10,7 +10,8 @@ var dragElement = require('../dragelement');
1010var Drawing = require ( '../drawing' ) ;
1111var Color = require ( '../color' ) ;
1212var svgTextUtils = require ( '../../lib/svg_text_utils' ) ;
13- var handleClick = require ( './handle_click' ) ;
13+ var handleItemClick = require ( './handle_click' ) . handleItemClick ;
14+ var handleTitleClick = require ( './handle_click' ) . handleTitleClick ;
1415
1516var constants = require ( './constants' ) ;
1617var alignmentConstants = require ( '../../constants/alignment' ) ;
@@ -82,7 +83,7 @@ function drawOne(gd, opts) {
8283 var legendObj = opts || { } ;
8384
8485 var fullLayout = gd . _fullLayout ;
85- var legendId = getId ( legendObj ) ;
86+ var legendId = helpers . getId ( legendObj ) ;
8687
8788 var clipId , layer ;
8889
@@ -180,8 +181,14 @@ function drawOne(gd, opts) {
180181 . text ( title . text ) ;
181182
182183 textLayout ( titleEl , scrollBox , gd , legendObj , MAIN_TITLE ) ; // handle mathjax or multi-line text and compute title height
184+
185+ // Set up title click if enabled and not in hover mode
186+ if ( ! inHover && ( legendObj . titleclick || legendObj . titledoubleclick ) ) {
187+ setupTitleToggle ( scrollBox , gd , legendObj , legendId ) ;
188+ }
183189 } else {
184190 scrollBox . selectAll ( '.' + legendId + 'titletext' ) . remove ( ) ;
191+ scrollBox . selectAll ( '.' + legendId + 'titletoggle' ) . remove ( ) ;
185192 }
186193
187194 var scrollBar = Lib . ensureSingle ( legend , 'rect' , 'scrollbar' , function ( s ) {
@@ -198,7 +205,22 @@ function drawOne(gd, opts) {
198205 traces . exit ( ) . remove ( ) ;
199206
200207 traces . style ( 'opacity' , function ( d ) {
201- var trace = d [ 0 ] . trace ;
208+ const legendItem = d [ 0 ] ;
209+ const trace = legendItem . trace ;
210+
211+ // Toggle opacity of legend group titles if all items in the group are hidden
212+ if ( legendItem . groupTitle ) {
213+ const groupName = trace . legendgroup ;
214+ const shapes = ( fullLayout . shapes || [ ] ) . filter ( function ( s ) { return s . showlegend ; } ) ;
215+ const anyVisible = gd . _fullData . concat ( shapes ) . some ( function ( item ) {
216+ return item . legendgroup === groupName &&
217+ ( item . legend || 'legend' ) === legendId &&
218+ item . visible === true ;
219+ } ) ;
220+
221+ return anyVisible ? 1 : 0.5 ;
222+ }
223+
202224 if ( Registry . traceIs ( trace , 'pie-like' ) ) {
203225 return hiddenSlices . indexOf ( d [ 0 ] . label ) !== - 1 ? 0.5 : 1 ;
204226 } else {
@@ -207,20 +229,34 @@ function drawOne(gd, opts) {
207229 } )
208230 . each ( function ( ) { d3 . select ( this ) . call ( drawTexts , gd , legendObj ) ; } )
209231 . call ( style , gd , legendObj )
210- . each ( function ( ) { if ( ! inHover ) d3 . select ( this ) . call ( setupTraceToggle , gd , legendId ) ; } ) ;
232+ . each ( function ( d ) {
233+ if ( inHover ) return ;
234+ // Don't create a click targets for group titles when groupclick is 'toggleitem'
235+ if ( d [ 0 ] . groupTitle && legendObj . groupclick === 'toggleitem' ) return ;
236+ d3 . select ( this ) . call ( setupTraceToggle , gd , legendId ) ;
237+ } ) ;
211238
212239 Lib . syncOrAsync ( [
213240 Plots . previousPromises ,
214- function ( ) { return computeLegendDimensions ( gd , groups , traces , legendObj ) ; } ,
241+ function ( ) { return computeLegendDimensions ( gd , groups , traces , legendObj , scrollBox ) ; } ,
215242 function ( ) {
216243 var gs = fullLayout . _size ;
217244 var bw = legendObj . borderwidth ;
218245 var isPaperX = legendObj . xref === 'paper' ;
219246 var isPaperY = legendObj . yref === 'paper' ;
220247
221- // re-calculate title position after legend width is derived. To allow for horizontal alignment
222248 if ( title . text ) {
223- horizontalAlignTitle ( titleEl , legendObj , bw ) ;
249+ // Toggle opacity of legend titles if all items in the legend are hidden
250+ const shapes = ( fullLayout . shapes || [ ] ) . filter ( function ( s ) { return s . showlegend ; } ) ;
251+ const anyVisible = gd . _fullData . concat ( shapes ) . some ( function ( item ) {
252+ const legendAttr = item . legend || 'legend' ;
253+ var inThisLegend = Array . isArray ( legendAttr )
254+ ? legendAttr . includes ( legendId )
255+ : legendAttr === legendId ;
256+ return inThisLegend && item . visible === true ;
257+ } ) ;
258+
259+ titleEl . style ( 'opacity' , anyVisible ? 1 : 0.5 ) ;
224260 }
225261
226262 if ( ! inHover ) {
@@ -458,7 +494,7 @@ function drawOne(gd, opts) {
458494 ) ;
459495 } ) ;
460496 if ( clickedTrace . size ( ) > 0 ) {
461- clickOrDoubleClick ( gd , legend , clickedTrace , numClicks , e ) ;
497+ clickOrDoubleClick ( gd , legendObj , clickedTrace , numClicks , e ) ;
462498 }
463499 }
464500 } ) ;
@@ -479,7 +515,12 @@ function getTraceWidth(d, legendObj, textGap) {
479515}
480516
481517function clickOrDoubleClick ( gd , legend , legendItem , numClicks , evt ) {
518+ var fullLayout = gd . _fullLayout ;
482519 var trace = legendItem . data ( ) [ 0 ] [ 0 ] . trace ;
520+
521+ var itemClick = legend . itemclick ;
522+ var itemDoubleClick = legend . itemdoubleclick ;
523+
483524 var evtData = {
484525 event : evt ,
485526 node : legendItem . node ( ) ,
@@ -490,7 +531,7 @@ function clickOrDoubleClick(gd, legend, legendItem, numClicks, evt) {
490531 frames : gd . _transitionData . _frames ,
491532 config : gd . _context ,
492533 fullData : gd . _fullData ,
493- fullLayout : gd . _fullLayout
534+ fullLayout : fullLayout
494535 } ;
495536
496537 if ( trace . _group ) {
@@ -504,20 +545,22 @@ function clickOrDoubleClick(gd, legend, legendItem, numClicks, evt) {
504545 if ( clickVal === false ) return ;
505546 legend . _clickTimeout = setTimeout ( function ( ) {
506547 if ( ! gd . _fullLayout ) return ;
507- handleClick ( legendItem , gd , numClicks ) ;
548+ if ( itemClick ) handleItemClick ( legendItem , gd , legend , itemClick ) ;
508549 } , gd . _context . doubleClickDelay ) ;
509550 } else if ( numClicks === 2 ) {
510551 if ( legend . _clickTimeout ) clearTimeout ( legend . _clickTimeout ) ;
511552 gd . _legendMouseDownTime = 0 ;
512553
513554 var dblClickVal = Events . triggerHandler ( gd , 'plotly_legenddoubleclick' , evtData ) ;
514555 // Activate default double click behaviour only when both single click and double click values are not false
515- if ( dblClickVal !== false && clickVal !== false ) handleClick ( legendItem , gd , numClicks ) ;
556+ if ( dblClickVal !== false && clickVal !== false && itemDoubleClick ) {
557+ handleItemClick ( legendItem , gd , legend , itemDoubleClick ) ;
558+ }
516559 }
517560}
518561
519562function drawTexts ( g , gd , legendObj ) {
520- var legendId = getId ( legendObj ) ;
563+ var legendId = helpers . getId ( legendObj ) ;
521564 var legendItem = g . data ( ) [ 0 ] [ 0 ] ;
522565 var trace = legendItem . trace ;
523566 var isPieLike = Registry . traceIs ( trace , 'pie-like' ) ;
@@ -624,6 +667,73 @@ function setupTraceToggle(g, gd, legendId) {
624667 } ) ;
625668}
626669
670+ function setupTitleToggle ( scrollBox , gd , legendObj , legendId ) {
671+ // For now, skip title click for legends containing pie-like traces
672+ const hasPie = gd . _fullData . some ( function ( trace ) {
673+ const legend = trace . legend || 'legend' ;
674+ const inThisLegend = Array . isArray ( legend ) ? legend . includes ( legendId ) : legend === legendId ;
675+ return inThisLegend && Registry . traceIs ( trace , 'pie-like' ) ;
676+ } ) ;
677+ if ( hasPie ) return ;
678+
679+ const doubleClickDelay = gd . _context . doubleClickDelay ;
680+ var newMouseDownTime ;
681+ var numClicks = 1 ;
682+
683+ const titleToggle = Lib . ensureSingle ( scrollBox , 'rect' , legendId + 'titletoggle' , function ( s ) {
684+ if ( ! gd . _context . staticPlot ) {
685+ s . style ( 'cursor' , 'pointer' ) . attr ( 'pointer-events' , 'all' ) ;
686+ }
687+ s . call ( Color . fill , 'rgba(0,0,0,0)' ) ;
688+ } ) ;
689+
690+ if ( gd . _context . staticPlot ) return ;
691+
692+ titleToggle . on ( 'mousedown' , function ( ) {
693+ newMouseDownTime = ( new Date ( ) ) . getTime ( ) ;
694+ if ( newMouseDownTime - gd . _legendMouseDownTime < doubleClickDelay ) {
695+ // in a click train
696+ numClicks += 1 ;
697+ } else {
698+ // new click train
699+ numClicks = 1 ;
700+ gd . _legendMouseDownTime = newMouseDownTime ;
701+ }
702+ } ) ;
703+ titleToggle . on ( 'mouseup' , function ( ) {
704+ if ( gd . _dragged || gd . _editing ) return ;
705+
706+ if ( ( new Date ( ) ) . getTime ( ) - gd . _legendMouseDownTime > doubleClickDelay ) {
707+ numClicks = Math . max ( numClicks - 1 , 1 ) ;
708+ }
709+
710+ const evtData = {
711+ event : d3 . event ,
712+ legendId : legendId ,
713+ data : gd . data ,
714+ layout : gd . layout ,
715+ fullData : gd . _fullData ,
716+ fullLayout : gd . _fullLayout
717+ } ;
718+
719+ if ( numClicks === 1 && legendObj . titleclick ) {
720+ const clickVal = Events . triggerHandler ( gd , 'plotly_legendtitleclick' , evtData ) ;
721+ if ( clickVal === false ) return ;
722+
723+ legendObj . _titleClickTimeout = setTimeout ( function ( ) {
724+ if ( gd . _fullLayout ) handleTitleClick ( gd , legendObj , legendObj . titleclick ) ;
725+ } , doubleClickDelay ) ;
726+ } else if ( numClicks === 2 ) {
727+ if ( legendObj . _titleClickTimeout ) clearTimeout ( legendObj . _titleClickTimeout ) ;
728+ gd . _legendMouseDownTime = 0 ;
729+
730+ const dblClickVal = Events . triggerHandler ( gd , 'plotly_legendtitledoubleclick' , evtData ) ;
731+ if ( dblClickVal !== false && legendObj . titledoubleclick ) handleTitleClick ( gd , legendObj , legendObj . titledoubleclick ) ;
732+ }
733+ } ) ;
734+ }
735+
736+
627737function textLayout ( s , g , gd , legendObj , aTitle ) {
628738 if ( legendObj . _inHover ) s . attr ( 'data-notex' , true ) ; // do not process MathJax for unified hover
629739 svgTextUtils . convertToTspans ( s , gd , function ( ) {
@@ -645,7 +755,7 @@ function computeTextDimensions(g, gd, legendObj, aTitle) {
645755 var mathjaxGroup = g . select ( 'g[class*=math-group]' ) ;
646756 var mathjaxNode = mathjaxGroup . node ( ) ;
647757
648- var legendId = getId ( legendObj ) ;
758+ var legendId = helpers . getId ( legendObj ) ;
649759 if ( ! legendObj ) {
650760 legendObj = gd . _fullLayout [ legendId ] ;
651761 }
@@ -748,9 +858,9 @@ function getTitleSize(legendObj) {
748858 * - _width: legend width
749859 * - _maxWidth (for orientation:h only): maximum width before starting new row
750860 */
751- function computeLegendDimensions ( gd , groups , traces , legendObj ) {
861+ function computeLegendDimensions ( gd , groups , traces , legendObj , scrollBox ) {
752862 var fullLayout = gd . _fullLayout ;
753- var legendId = getId ( legendObj ) ;
863+ var legendId = helpers . getId ( legendObj ) ;
754864 if ( ! legendObj ) {
755865 legendObj = fullLayout [ legendId ] ;
756866 }
@@ -955,6 +1065,25 @@ function computeLegendDimensions(gd, groups, traces, legendObj) {
9551065 }
9561066 Drawing . setRect ( traceToggle , 0 , - h / 2 , w , h ) ;
9571067 } ) ;
1068+
1069+ // align legend title horizontally
1070+ var titleEl = scrollBox . select ( '.' + legendId + 'titletext' ) ;
1071+ if ( titleEl . node ( ) ) {
1072+ horizontalAlignTitle ( titleEl , legendObj , bw ) ;
1073+ }
1074+
1075+ // position title click target to cover the title text, parallel to traceToggle above
1076+ var titleToggle = scrollBox . select ( '.' + legendId + 'titletoggle' ) ;
1077+ if ( titleToggle . size ( ) && titleEl . node ( ) ) {
1078+ var titleX = titleEl . attr ( 'x' ) || 0 ;
1079+ var pad = constants . titlePad ;
1080+ Drawing . setRect ( titleToggle ,
1081+ titleX - pad ,
1082+ bw ,
1083+ legendObj . _titleWidth + 2 * pad ,
1084+ legendObj . _titleHeight + 2 * pad
1085+ ) ;
1086+ }
9581087}
9591088
9601089function expandMargin ( gd , legendId , lx , ly ) {
@@ -1009,7 +1138,3 @@ function getYanchor(legendObj) {
10091138 Lib . isMiddleAnchor ( legendObj ) ? 'middle' :
10101139 'top' ;
10111140}
1012-
1013- function getId ( legendObj ) {
1014- return legendObj . _id || 'legend' ;
1015- }
0 commit comments