@@ -14,6 +14,8 @@ var interactConstants = require('../../constants/interactions');
14
14
15
15
var OPPOSITE_SIDE = require ( '../../constants/alignment' ) . OPPOSITE_SIDE ;
16
16
var numStripRE = / [ X Y ] [ 0 - 9 ] * / ;
17
+ var SUBTITLE_PADDING_MATHJAX_EM = 1.6 ;
18
+ var SUBTITLE_PADDING_EM = 1.6 ;
17
19
18
20
/**
19
21
* Titles - (re)draw titles on the axes and plot:
@@ -48,6 +50,8 @@ var numStripRE = / [XY][0-9]* /;
48
50
* @return {selection } d3 selection of title container group
49
51
*/
50
52
function draw ( gd , titleClass , options ) {
53
+ var fullLayout = gd . _fullLayout ;
54
+
51
55
var cont = options . propContainer ;
52
56
var prop = options . propName ;
53
57
var placeholder = options . placeholder ;
@@ -56,13 +60,10 @@ function draw(gd, titleClass, options) {
56
60
var attributes = options . attributes ;
57
61
var transform = options . transform ;
58
62
var group = options . containerGroup ;
59
-
60
- var fullLayout = gd . _fullLayout ;
61
-
62
63
var opacity = 1 ;
63
- var isplaceholder = false ;
64
64
var title = cont . title ;
65
65
var txt = ( title && title . text ? title . text : '' ) . trim ( ) ;
66
+ var titleIsPlaceholder = false ;
66
67
67
68
var font = title && title . font ? title . font : { } ;
68
69
var fontFamily = font . family ;
@@ -75,23 +76,58 @@ function draw(gd, titleClass, options) {
75
76
var fontLineposition = font . lineposition ;
76
77
var fontShadow = font . shadow ;
77
78
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
+
78
99
// only make this title editable if we positively identify its property
79
100
// as one that has editing enabled.
101
+ // Subtitle is editable if and only if title is editable
80
102
var editAttr ;
81
103
if ( prop === 'title.text' ) editAttr = 'titleText' ;
82
104
else if ( prop . indexOf ( 'axis' ) !== - 1 ) editAttr = 'axisTitleText' ;
83
105
else if ( prop . indexOf ( 'colorbar' !== - 1 ) ) editAttr = 'colorbarTitleText' ;
84
106
var editable = gd . _context . edits [ editAttr ] ;
85
107
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
+
86
117
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 ) ) {
94
119
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
+ }
95
131
}
96
132
97
133
if ( options . _meta ) {
@@ -100,15 +136,15 @@ function draw(gd, titleClass, options) {
100
136
txt = Lib . templateString ( txt , fullLayout . _meta ) ;
101
137
}
102
138
103
- var elShouldExist = txt || editable ;
139
+ var elShouldExist = txt || subtitleTxt || editable ;
104
140
105
141
var hColorbarMoveTitle ;
106
142
if ( ! group ) {
107
143
group = Lib . ensureSingle ( fullLayout . _infolayer , 'g' , 'g-' + titleClass ) ;
108
144
hColorbarMoveTitle = fullLayout . _hColorbarMoveTitle ;
109
145
}
110
146
111
- var el = group . selectAll ( 'text' )
147
+ var el = group . selectAll ( 'text.' + titleClass )
112
148
. data ( elShouldExist ? [ 0 ] : [ ] ) ;
113
149
el . enter ( ) . append ( 'text' ) ;
114
150
el . text ( txt )
@@ -120,13 +156,29 @@ function draw(gd, titleClass, options) {
120
156
. attr ( 'class' , titleClass ) ;
121
157
el . exit ( ) . remove ( ) ;
122
158
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
+
123
172
if ( ! elShouldExist ) return group ;
124
173
125
- function titleLayout ( titleEl ) {
126
- Lib . syncOrAsync ( [ drawTitle , scootTitle ] , titleEl ) ;
174
+ function titleLayout ( titleEl , subtitleEl ) {
175
+ Lib . syncOrAsync ( [ drawTitle , scootTitle ] , { title : titleEl , subtitle : subtitleEl } ) ;
127
176
}
128
177
129
- function drawTitle ( titleEl ) {
178
+ function drawTitle ( titleAndSubtitleEls ) {
179
+ var titleEl = titleAndSubtitleEls . title ;
180
+ var subtitleEl = titleAndSubtitleEls . subtitle ;
181
+
130
182
var transformVal ;
131
183
132
184
if ( ! transform && hColorbarMoveTitle ) {
@@ -147,6 +199,23 @@ function draw(gd, titleClass, options) {
147
199
148
200
titleEl . attr ( 'transform' , transformVal ) ;
149
201
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
+
150
219
titleEl . style ( 'opacity' , opacity * Color . opacity ( fontColor ) )
151
220
. call ( Drawing . font , {
152
221
color : Color . rgb ( fontColor ) ,
@@ -160,12 +229,43 @@ function draw(gd, titleClass, options) {
160
229
lineposition : fontLineposition ,
161
230
} )
162
231
. 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
+ }
164
263
165
264
return Plots . previousPromises ( gd ) ;
166
265
}
167
266
168
- function scootTitle ( titleElIn ) {
267
+ function scootTitle ( titleAndSubtitleEls ) {
268
+ var titleElIn = titleAndSubtitleEls . title ;
169
269
var titleGroup = d3 . select ( titleElIn . node ( ) . parentNode ) ;
170
270
171
271
if ( avoid && avoid . selection && avoid . side && txt ) {
@@ -239,12 +339,10 @@ function draw(gd, titleClass, options) {
239
339
}
240
340
}
241
341
242
- el . call ( titleLayout ) ;
342
+ el . call ( titleLayout , subtitleEl ) ;
243
343
244
- function setPlaceholder ( ) {
245
- opacity = 0 ;
246
- isplaceholder = true ;
247
- el . text ( placeholder )
344
+ function setPlaceholder ( element , placeholderText ) {
345
+ element . text ( placeholderText )
248
346
. on ( 'mouseover.opacity' , function ( ) {
249
347
d3 . select ( this ) . transition ( )
250
348
. duration ( interactConstants . SHOW_PLACEHOLDER ) . style ( 'opacity' , 1 ) ;
@@ -256,8 +354,10 @@ function draw(gd, titleClass, options) {
256
354
}
257
355
258
356
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 ) ;
261
361
262
362
el . call ( svgTextUtils . makeEditable , { gd : gd } )
263
363
. on ( 'edit' , function ( text ) {
@@ -275,12 +375,43 @@ function draw(gd, titleClass, options) {
275
375
this . text ( d || ' ' )
276
376
. call ( svgTextUtils . positionText , attributes . x , attributes . y ) ;
277
377
} ) ;
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
+ }
278
405
}
279
- el . classed ( 'js-placeholder' , isplaceholder ) ;
406
+
407
+ el . classed ( 'js-placeholder' , titleIsPlaceholder ) ;
408
+ if ( subtitleEl ) subtitleEl . classed ( 'js-placeholder' , subtitleIsPlaceholder ) ;
280
409
281
410
return group ;
282
411
}
283
412
284
413
module . exports = {
285
- draw : draw
414
+ draw : draw ,
415
+ SUBTITLE_PADDING_EM : SUBTITLE_PADDING_EM ,
416
+ SUBTITLE_PADDING_MATHJAX_EM : SUBTITLE_PADDING_MATHJAX_EM ,
286
417
} ;
0 commit comments