@@ -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 handleClick = require ( './handle_click' ) . handleClick ;
14+ var handleTitleClick = require ( './handle_click' ) . handleTitleClick ;
1415
1516var constants = require ( './constants' ) ;
1617var alignmentConstants = require ( '../../constants/alignment' ) ;
@@ -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+ var legendItem = d [ 0 ] ;
209+ var trace = legendItem . trace ;
210+
211+ // Toggle opacity of legend group titles if all items in the group are hidden
212+ if ( legendItem . groupTitle ) {
213+ var groupName = trace . legendgroup ;
214+ var shapes = ( fullLayout . shapes || [ ] ) . filter ( function ( s ) { return s . showlegend ; } ) ;
215+ var 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,7 +229,12 @@ 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 ,
@@ -221,6 +248,20 @@ function drawOne(gd, opts) {
221248 // re-calculate title position after legend width is derived. To allow for horizontal alignment
222249 if ( title . text ) {
223250 horizontalAlignTitle ( titleEl , legendObj , bw ) ;
251+
252+ // Position click target for the title after dimensions are computed
253+ if ( ! inHover && ( legendObj . titleclick || legendObj . titledoubleclick ) ) {
254+ positionTitleToggle ( scrollBox , legendObj , legendId ) ;
255+ }
256+
257+ // Toggle opacity of legend titles if all items in the legend are hidden
258+ var shapes = ( fullLayout . shapes || [ ] ) . filter ( function ( s ) { return s . showlegend ; } ) ;
259+ var anyVisible = gd . _fullData . concat ( shapes ) . some ( function ( item ) {
260+ var inThisLegend = ( item . legend || 'legend' ) === legendId ;
261+ return inThisLegend && item . visible === true ;
262+ } ) ;
263+
264+ titleEl . style ( 'opacity' , anyVisible ? 1 : 0.5 ) ;
224265 }
225266
226267 if ( ! inHover ) {
@@ -624,6 +665,92 @@ function setupTraceToggle(g, gd, legendId) {
624665 } ) ;
625666}
626667
668+ function setupTitleToggle ( scrollBox , gd , legendObj , legendId ) {
669+ // For now, skip title click for legends containing pie-like traces
670+ var hasPie = gd . _fullData . some ( function ( trace ) {
671+ var legend = trace . legend || 'legend' ;
672+ var inThisLegend = Array . isArray ( legend ) ? legend . includes ( legendId ) : legend === legendId ;
673+ return inThisLegend && Registry . traceIs ( trace , 'pie-like' ) ;
674+ } ) ;
675+ if ( hasPie ) return ;
676+
677+ var doubleClickDelay = gd . _context . doubleClickDelay ;
678+ var newMouseDownTime ;
679+ var numClicks = 1 ;
680+
681+ var titleToggle = Lib . ensureSingle ( scrollBox , 'rect' , legendId + 'titletoggle' , function ( s ) {
682+ if ( ! gd . _context . staticPlot ) {
683+ s . style ( 'cursor' , 'pointer' ) . attr ( 'pointer-events' , 'all' ) ;
684+ }
685+ s . call ( Color . fill , 'rgba(0,0,0,0)' ) ;
686+ } ) ;
687+
688+ if ( gd . _context . staticPlot ) return ;
689+
690+ titleToggle . on ( 'mousedown' , function ( ) {
691+ newMouseDownTime = ( new Date ( ) ) . getTime ( ) ;
692+ if ( newMouseDownTime - gd . _legendMouseDownTime < doubleClickDelay ) {
693+ // in a click train
694+ numClicks += 1 ;
695+ } else {
696+ // new click train
697+ numClicks = 1 ;
698+ gd . _legendMouseDownTime = newMouseDownTime ;
699+ }
700+ } ) ;
701+ titleToggle . on ( 'mouseup' , function ( ) {
702+ if ( gd . _dragged || gd . _editing ) return ;
703+
704+ if ( ( new Date ( ) ) . getTime ( ) - gd . _legendMouseDownTime > doubleClickDelay ) {
705+ numClicks = Math . max ( numClicks - 1 , 1 ) ;
706+ }
707+
708+ var evtData = {
709+ event : d3 . event ,
710+ legendId : legendId ,
711+ data : gd . data ,
712+ layout : gd . layout ,
713+ fullData : gd . _fullData ,
714+ fullLayout : gd . _fullLayout
715+ } ;
716+
717+ if ( numClicks === 1 && legendObj . titleclick ) {
718+ var clickVal = Events . triggerHandler ( gd , 'plotly_legendtitleclick' , evtData ) ;
719+ if ( clickVal === false ) return ;
720+
721+ legendObj . _titleClickTimeout = setTimeout ( function ( ) {
722+ if ( gd . _fullLayout ) handleTitleClick ( gd , legendObj , legendObj . titleclick ) ;
723+ } , doubleClickDelay ) ;
724+ } else if ( numClicks === 2 ) {
725+ if ( legendObj . _titleClickTimeout ) clearTimeout ( legendObj . _titleClickTimeout ) ;
726+ gd . _legendMouseDownTime = 0 ;
727+
728+ var dblClickVal = Events . triggerHandler ( gd , 'plotly_legendtitledoubleclick' , evtData ) ;
729+ if ( dblClickVal !== false && legendObj . titledoubleclick ) handleTitleClick ( gd , legendObj , legendObj . titledoubleclick ) ;
730+ }
731+ } ) ;
732+ }
733+
734+ function positionTitleToggle ( scrollBox , legendObj , legendId ) {
735+ var titleToggle = scrollBox . select ( '.' + legendId + 'titletoggle' ) ;
736+ if ( ! titleToggle . size ( ) ) return ;
737+
738+ var side = legendObj . title . side || 'top' ;
739+ var bw = legendObj . borderwidth ;
740+ var x = bw ;
741+ var width = legendObj . _titleWidth + 2 * constants . titlePad ;
742+ var height = legendObj . _titleHeight + 2 * constants . titlePad ;
743+
744+
745+ if ( side === 'top center' ) {
746+ x = bw + 0.5 * ( legendObj . _width - 2 * bw - width ) ;
747+ } else if ( side === 'top right' ) {
748+ x = legendObj . _width - bw - width ;
749+ }
750+
751+ titleToggle . attr ( { x : x , y : bw , width : width , height : height } ) ;
752+ }
753+
627754function textLayout ( s , g , gd , legendObj , aTitle ) {
628755 if ( legendObj . _inHover ) s . attr ( 'data-notex' , true ) ; // do not process MathJax for unified hover
629756 svgTextUtils . convertToTspans ( s , gd , function ( ) {
0 commit comments