@@ -14,6 +14,8 @@ var interactConstants = require('../../constants/interactions');
1414
1515var OPPOSITE_SIDE = require ( '../../constants/alignment' ) . OPPOSITE_SIDE ;
1616var numStripRE = / [ X Y ] [ 0 - 9 ] * / ;
17+ var SUBTITLE_PADDING_MATHJAX_EM = 1.6 ;
18+ var SUBTITLE_PADDING_EM = 1.6 ;
1719
1820/**
1921 * Titles - (re)draw titles on the axes and plot:
@@ -48,6 +50,8 @@ var numStripRE = / [XY][0-9]* /;
4850 * @return {selection } d3 selection of title container group
4951 */
5052function draw ( gd , titleClass , options ) {
53+ var fullLayout = gd . _fullLayout ;
54+
5155 var cont = options . propContainer ;
5256 var prop = options . propName ;
5357 var placeholder = options . placeholder ;
@@ -56,13 +60,10 @@ function draw(gd, titleClass, options) {
5660 var attributes = options . attributes ;
5761 var transform = options . transform ;
5862 var group = options . containerGroup ;
59-
60- var fullLayout = gd . _fullLayout ;
61-
6263 var opacity = 1 ;
63- var isplaceholder = false ;
6464 var title = cont . title ;
6565 var txt = ( title && title . text ? title . text : '' ) . trim ( ) ;
66+ var titleIsPlaceholder = false ;
6667
6768 var font = title && title . font ? title . font : { } ;
6869 var fontFamily = font . family ;
@@ -75,23 +76,58 @@ function draw(gd, titleClass, options) {
7576 var fontLineposition = font . lineposition ;
7677 var fontShadow = font . shadow ;
7778
79+ // Get subtitle properties
80+ var subtitleProp = options . subtitlePropName ;
81+ var subtitleEnabled = ! ! subtitleProp ;
82+ var subtitlePlaceholder = options . subtitlePlaceholder ;
83+ var subtitle = ( cont . title || { } ) . subtitle || { text : '' , font : { } } ;
84+ var subtitleTxt = subtitle . text . trim ( ) ;
85+ var subtitleIsPlaceholder = false ;
86+ var subtitleOpacity = 1 ;
87+
88+ var subtitleFont = subtitle . font ;
89+ var subFontFamily = subtitleFont . family ;
90+ var subFontSize = subtitleFont . size ;
91+ var subFontColor = subtitleFont . color ;
92+ var subFontWeight = subtitleFont . weight ;
93+ var subFontStyle = subtitleFont . style ;
94+ var subFontVariant = subtitleFont . variant ;
95+ var subFontTextcase = subtitleFont . textcase ;
96+ var subFontLineposition = subtitleFont . lineposition ;
97+ var subFontShadow = subtitleFont . shadow ;
98+
7899 // only make this title editable if we positively identify its property
79100 // as one that has editing enabled.
101+ // Subtitle is editable if and only if title is editable
80102 var editAttr ;
81103 if ( prop === 'title.text' ) editAttr = 'titleText' ;
82104 else if ( prop . indexOf ( 'axis' ) !== - 1 ) editAttr = 'axisTitleText' ;
83105 else if ( prop . indexOf ( 'colorbar' !== - 1 ) ) editAttr = 'colorbarTitleText' ;
84106 var editable = gd . _context . edits [ editAttr ] ;
85107
108+ function matchesPlaceholder ( text , placeholder ) {
109+ if ( text === undefined || placeholder === undefined ) return false ;
110+ // look for placeholder text while stripping out numbers from eg X2, Y3
111+ // this is just for backward compatibility with the old version that had
112+ // "Click to enter X2 title" and may have gotten saved in some old plots,
113+ // we don't want this to show up when these are displayed.
114+ return text . replace ( numStripRE , ' % ' ) === placeholder . replace ( numStripRE , ' % ' ) ;
115+ }
116+
86117 if ( txt === '' ) opacity = 0 ;
87- // look for placeholder text while stripping out numbers from eg X2, Y3
88- // this is just for backward compatibility with the old version that had
89- // "Click to enter X2 title" and may have gotten saved in some old plots,
90- // we don't want this to show up when these are displayed.
91- else if ( txt . replace ( numStripRE , ' % ' ) === placeholder . replace ( numStripRE , ' % ' ) ) {
92- opacity = 0.2 ;
93- isplaceholder = true ;
118+ else if ( matchesPlaceholder ( txt , placeholder ) ) {
94119 if ( ! editable ) txt = '' ;
120+ opacity = 0.2 ;
121+ titleIsPlaceholder = true ;
122+ }
123+
124+ if ( subtitleEnabled ) {
125+ if ( subtitleTxt === '' ) subtitleOpacity = 0 ;
126+ else if ( matchesPlaceholder ( subtitleTxt , subtitlePlaceholder ) ) {
127+ if ( ! editable ) subtitleTxt = '' ;
128+ subtitleOpacity = 0.2 ;
129+ subtitleIsPlaceholder = true ;
130+ }
95131 }
96132
97133 if ( options . _meta ) {
@@ -100,15 +136,15 @@ function draw(gd, titleClass, options) {
100136 txt = Lib . templateString ( txt , fullLayout . _meta ) ;
101137 }
102138
103- var elShouldExist = txt || editable ;
139+ var elShouldExist = txt || subtitleTxt || editable ;
104140
105141 var hColorbarMoveTitle ;
106142 if ( ! group ) {
107143 group = Lib . ensureSingle ( fullLayout . _infolayer , 'g' , 'g-' + titleClass ) ;
108144 hColorbarMoveTitle = fullLayout . _hColorbarMoveTitle ;
109145 }
110146
111- var el = group . selectAll ( 'text' )
147+ var el = group . selectAll ( 'text.' + titleClass )
112148 . data ( elShouldExist ? [ 0 ] : [ ] ) ;
113149 el . enter ( ) . append ( 'text' ) ;
114150 el . text ( txt )
@@ -120,13 +156,29 @@ function draw(gd, titleClass, options) {
120156 . attr ( 'class' , titleClass ) ;
121157 el . exit ( ) . remove ( ) ;
122158
159+ var subtitleEl = null ;
160+ var subtitleClass = titleClass + '-subtitle' ;
161+ var subtitleElShouldExist = subtitleTxt || editable ;
162+
163+ if ( subtitleEnabled && subtitleElShouldExist ) {
164+ subtitleEl = group . selectAll ( 'text.' + subtitleClass )
165+ . data ( subtitleElShouldExist ? [ 0 ] : [ ] ) ;
166+ subtitleEl . enter ( ) . append ( 'text' ) ;
167+ subtitleEl . text ( subtitleTxt ) . attr ( 'class' , subtitleClass ) ;
168+ subtitleEl . exit ( ) . remove ( ) ;
169+ }
170+
171+
123172 if ( ! elShouldExist ) return group ;
124173
125- function titleLayout ( titleEl ) {
126- Lib . syncOrAsync ( [ drawTitle , scootTitle ] , titleEl ) ;
174+ function titleLayout ( titleEl , subtitleEl ) {
175+ Lib . syncOrAsync ( [ drawTitle , scootTitle ] , { title : titleEl , subtitle : subtitleEl } ) ;
127176 }
128177
129- function drawTitle ( titleEl ) {
178+ function drawTitle ( titleAndSubtitleEls ) {
179+ var titleEl = titleAndSubtitleEls . title ;
180+ var subtitleEl = titleAndSubtitleEls . subtitle ;
181+
130182 var transformVal ;
131183
132184 if ( ! transform && hColorbarMoveTitle ) {
@@ -147,6 +199,23 @@ function draw(gd, titleClass, options) {
147199
148200 titleEl . attr ( 'transform' , transformVal ) ;
149201
202+ // Callback to adjust the subtitle position after mathjax is rendered
203+ // Mathjax is rendered asynchronously, which is why this step needs to be
204+ // passed as a callback
205+ function adjustSubtitlePosition ( titleElMathGroup ) {
206+ if ( ! titleElMathGroup ) return ;
207+
208+ var subtitleElement = d3 . select ( titleElMathGroup . node ( ) . parentNode ) . select ( '.' + subtitleClass ) ;
209+ if ( ! subtitleElement . empty ( ) ) {
210+ var titleElMathBbox = titleElMathGroup . node ( ) . getBBox ( ) ;
211+ if ( titleElMathBbox . height ) {
212+ // Position subtitle based on bottom of Mathjax title
213+ var subtitleY = titleElMathBbox . y + titleElMathBbox . height + ( SUBTITLE_PADDING_MATHJAX_EM * subFontSize ) ;
214+ subtitleElement . attr ( 'y' , subtitleY ) ;
215+ }
216+ }
217+ }
218+
150219 titleEl . style ( 'opacity' , opacity * Color . opacity ( fontColor ) )
151220 . call ( Drawing . font , {
152221 color : Color . rgb ( fontColor ) ,
@@ -160,12 +229,43 @@ function draw(gd, titleClass, options) {
160229 lineposition : fontLineposition ,
161230 } )
162231 . attr ( attributes )
163- . call ( svgTextUtils . convertToTspans , gd ) ;
232+ . call ( svgTextUtils . convertToTspans , gd , adjustSubtitlePosition ) ;
233+
234+ if ( subtitleEl ) {
235+ // Set subtitle y position based on bottom of title
236+ // We need to check the Mathjax group as well, in case the Mathjax
237+ // has already rendered
238+ var titleElMathGroup = group . select ( '.' + titleClass + '-math-group' ) ;
239+ var titleElBbox = titleEl . node ( ) . getBBox ( ) ;
240+ var titleElMathBbox = titleElMathGroup . node ( ) ? titleElMathGroup . node ( ) . getBBox ( ) : undefined ;
241+ var subtitleY = titleElMathBbox ? titleElMathBbox . y + titleElMathBbox . height + ( SUBTITLE_PADDING_MATHJAX_EM * subFontSize ) : titleElBbox . y + titleElBbox . height + ( SUBTITLE_PADDING_EM * subFontSize ) ;
242+
243+ var subtitleAttributes = Lib . extendFlat ( { } , attributes , {
244+ y : subtitleY
245+ } ) ;
246+
247+ subtitleEl . attr ( 'transform' , transformVal ) ;
248+ subtitleEl . style ( 'opacity' , subtitleOpacity * Color . opacity ( subFontColor ) )
249+ . call ( Drawing . font , {
250+ color : Color . rgb ( subFontColor ) ,
251+ size : d3 . round ( subFontSize , 2 ) ,
252+ family : subFontFamily ,
253+ weight : subFontWeight ,
254+ style : subFontStyle ,
255+ variant : subFontVariant ,
256+ textcase : subFontTextcase ,
257+ shadow : subFontShadow ,
258+ lineposition : subFontLineposition ,
259+ } )
260+ . attr ( subtitleAttributes )
261+ . call ( svgTextUtils . convertToTspans , gd ) ;
262+ }
164263
165264 return Plots . previousPromises ( gd ) ;
166265 }
167266
168- function scootTitle ( titleElIn ) {
267+ function scootTitle ( titleAndSubtitleEls ) {
268+ var titleElIn = titleAndSubtitleEls . title ;
169269 var titleGroup = d3 . select ( titleElIn . node ( ) . parentNode ) ;
170270
171271 if ( avoid && avoid . selection && avoid . side && txt ) {
@@ -239,12 +339,10 @@ function draw(gd, titleClass, options) {
239339 }
240340 }
241341
242- el . call ( titleLayout ) ;
342+ el . call ( titleLayout , subtitleEl ) ;
243343
244- function setPlaceholder ( ) {
245- opacity = 0 ;
246- isplaceholder = true ;
247- el . text ( placeholder )
344+ function setPlaceholder ( element , placeholderText ) {
345+ element . text ( placeholderText )
248346 . on ( 'mouseover.opacity' , function ( ) {
249347 d3 . select ( this ) . transition ( )
250348 . duration ( interactConstants . SHOW_PLACEHOLDER ) . style ( 'opacity' , 1 ) ;
@@ -256,8 +354,10 @@ function draw(gd, titleClass, options) {
256354 }
257355
258356 if ( editable ) {
259- if ( ! txt ) setPlaceholder ( ) ;
260- else el . on ( '.opacity' , null ) ;
357+ if ( ! txt ) {
358+ setPlaceholder ( el , placeholder ) ;
359+ titleIsPlaceholder = true ;
360+ } else el . on ( '.opacity' , null ) ;
261361
262362 el . call ( svgTextUtils . makeEditable , { gd : gd } )
263363 . on ( 'edit' , function ( text ) {
@@ -275,12 +375,43 @@ function draw(gd, titleClass, options) {
275375 this . text ( d || ' ' )
276376 . call ( svgTextUtils . positionText , attributes . x , attributes . y ) ;
277377 } ) ;
378+
379+ if ( subtitleEnabled ) {
380+ // Adjust subtitle position now that title placeholder has been added
381+ // Only adjust if subtitle is enabled and title text was originally empty
382+ if ( subtitleEnabled && ! txt ) {
383+ var titleElBbox = el . node ( ) . getBBox ( ) ;
384+ var subtitleY = titleElBbox . y + titleElBbox . height + ( SUBTITLE_PADDING_EM * subFontSize ) ;
385+ subtitleEl . attr ( 'y' , subtitleY ) ;
386+ }
387+
388+ if ( ! subtitleTxt ) {
389+ setPlaceholder ( subtitleEl , subtitlePlaceholder ) ;
390+ subtitleIsPlaceholder = true ;
391+ } else subtitleEl . on ( '.opacity' , null ) ;
392+ subtitleEl . call ( svgTextUtils . makeEditable , { gd : gd } )
393+ . on ( 'edit' , function ( text ) {
394+ Registry . call ( '_guiRelayout' , gd , 'title.subtitle.text' , text ) ;
395+ } )
396+ . on ( 'cancel' , function ( ) {
397+ this . text ( this . attr ( 'data-unformatted' ) )
398+ . call ( titleLayout ) ;
399+ } )
400+ . on ( 'input' , function ( d ) {
401+ this . text ( d || ' ' )
402+ . call ( svgTextUtils . positionText , subtitleEl . attr ( 'x' ) , subtitleEl . attr ( 'y' ) ) ;
403+ } ) ;
404+ }
278405 }
279- el . classed ( 'js-placeholder' , isplaceholder ) ;
406+
407+ el . classed ( 'js-placeholder' , titleIsPlaceholder ) ;
408+ if ( subtitleEl ) subtitleEl . classed ( 'js-placeholder' , subtitleIsPlaceholder ) ;
280409
281410 return group ;
282411}
283412
284413module . exports = {
285- draw : draw
414+ draw : draw ,
415+ SUBTITLE_PADDING_EM : SUBTITLE_PADDING_EM ,
416+ SUBTITLE_PADDING_MATHJAX_EM : SUBTITLE_PADDING_MATHJAX_EM ,
286417} ;
0 commit comments