@@ -25,6 +25,9 @@ var Registry = require('../../registry');
25
25
var helpers = require ( './helpers' ) ;
26
26
var constants = require ( './constants' ) ;
27
27
28
+ var legendSupplyDefaults = require ( '../legend/defaults' ) ;
29
+ var legendDraw = require ( '../legend/draw' ) ;
30
+
28
31
// hover labels for multiple horizontal bars get tilted by some angle,
29
32
// then need to be offset differently if they overlap
30
33
var YANGLE = constants . YANGLE ;
@@ -244,7 +247,7 @@ function _hover(gd, evt, subplot, noHoverEvent) {
244
247
245
248
if ( hovermode && ! supportsCompare ) hovermode = 'closest' ;
246
249
247
- if ( [ 'x' , 'y' , 'closest' ] . indexOf ( hovermode ) === - 1 || ! gd . calcdata ||
250
+ if ( [ 'x' , 'y' , 'closest' , 'x unified' , 'y unified' ] . indexOf ( hovermode ) === - 1 || ! gd . calcdata ||
248
251
gd . querySelector ( '.zoombox' ) || gd . _dragging ) {
249
252
return dragElement . unhoverRaw ( gd , evt ) ;
250
253
}
@@ -388,6 +391,9 @@ function _hover(gd, evt, subplot, noHoverEvent) {
388
391
389
392
// within one trace mode can sometimes be overridden
390
393
mode = hovermode ;
394
+ if ( [ 'x unified' , 'y unified' ] . indexOf ( mode ) !== - 1 ) {
395
+ mode = mode . charAt ( 0 ) ;
396
+ }
391
397
392
398
// container for new point, also used to pass info into module.hoverPoints
393
399
pointData = {
@@ -661,9 +667,10 @@ function _hover(gd, evt, subplot, noHoverEvent) {
661
667
662
668
var hoverLabels = createHoverText ( hoverData , labelOpts , gd ) ;
663
669
664
- hoverAvoidOverlaps ( hoverLabels , rotateLabels ? 'xa' : 'ya' , fullLayout ) ;
665
-
666
- alignHoverText ( hoverLabels , rotateLabels ) ;
670
+ if ( [ 'x unified' , 'y unified' ] . indexOf ( hovermode ) === - 1 ) {
671
+ hoverAvoidOverlaps ( hoverLabels , rotateLabels ? 'xa' : 'ya' , fullLayout ) ;
672
+ alignHoverText ( hoverLabels , rotateLabels ) ;
673
+ }
667
674
668
675
// TODO: tagName hack is needed to appease geo.js's hack of using evt.target=true
669
676
// we should improve the "fx" API so other plots can use it without these hack.
@@ -712,7 +719,7 @@ function createHoverText(hoverData, opts, gd) {
712
719
var c0 = hoverData [ 0 ] ;
713
720
var xa = c0 . xa ;
714
721
var ya = c0 . ya ;
715
- var commonAttr = hovermode === 'y' ? 'yLabel' : 'xLabel' ;
722
+ var commonAttr = hovermode . charAt ( 0 ) === 'y' ? 'yLabel' : 'xLabel' ;
716
723
var t0 = c0 [ commonAttr ] ;
717
724
var t00 = ( String ( t0 ) || '' ) . split ( ' ' ) [ 0 ] ;
718
725
var outerContainerBB = outerContainer . node ( ) . getBoundingClientRect ( ) ;
@@ -906,11 +913,113 @@ function createHoverText(hoverData, opts, gd) {
906
913
907
914
// remove the "close but not quite" points
908
915
// because of error bars, only take up to a space
909
- hoverData = hoverData . filter ( function ( d ) {
916
+ hoverData = filterClosePoints ( hoverData ) ;
917
+ } ) ;
918
+
919
+ function filterClosePoints ( hoverData ) {
920
+ return hoverData . filter ( function ( d ) {
910
921
return ( d . zLabelVal !== undefined ) ||
911
922
( d [ commonAttr ] || '' ) . split ( ' ' ) [ 0 ] === t00 ;
912
923
} ) ;
913
- } ) ;
924
+ }
925
+
926
+ // Show a single hover label
927
+ if ( [ 'x unified' , 'y unified' ] . indexOf ( hovermode ) !== - 1 ) {
928
+ // Delete leftover hover labels from other hovermodes
929
+ container . selectAll ( 'g.hovertext' ) . remove ( ) ;
930
+
931
+ // similarly to compare mode, we remove the "close but not quite together" points
932
+ if ( ( t0 !== undefined ) && ( c0 . distance <= opts . hoverdistance ) ) hoverData = filterClosePoints ( hoverData ) ;
933
+
934
+ // Return early if nothing is hovered on
935
+ if ( hoverData . length === 0 ) return ;
936
+
937
+ // mock legend
938
+ var mockLayoutIn = {
939
+ showlegend : true ,
940
+ legend : {
941
+ title : { text : t0 , font : fullLayout . font } ,
942
+ font : fullLayout . font ,
943
+ bgcolor : fullLayout . paper_bgcolor ,
944
+ borderwidth : 1 ,
945
+ tracegroupgap : 7 ,
946
+ traceorder : fullLayout . legend ? fullLayout . legend . traceorder : undefined ,
947
+ orientation : 'v'
948
+ }
949
+ } ;
950
+ var mockLayoutOut = { } ;
951
+ legendSupplyDefaults ( mockLayoutIn , mockLayoutOut , gd . _fullData ) ;
952
+ var legendOpts = mockLayoutOut . legend ;
953
+
954
+ // prepare items for the legend
955
+ legendOpts . entries = [ ] ;
956
+ for ( var j = 0 ; j < hoverData . length ; j ++ ) {
957
+ var texts = getHoverLabelText ( hoverData [ j ] , true , hovermode , fullLayout , t0 ) ;
958
+ var text = texts [ 0 ] ;
959
+ var name = texts [ 1 ] ;
960
+ var pt = hoverData [ j ] ;
961
+ pt . name = name ;
962
+ if ( name !== '' ) {
963
+ pt . text = name + ' : ' + text ;
964
+ } else {
965
+ pt . text = text ;
966
+ }
967
+
968
+ // pass through marker's calcdata to style legend items
969
+ var cd = pt . cd [ pt . index ] ;
970
+ if ( cd ) {
971
+ if ( cd . mc ) pt . mc = cd . mc ;
972
+ if ( cd . mcc ) pt . mc = cd . mcc ;
973
+ if ( cd . mlc ) pt . mlc = cd . mlc ;
974
+ if ( cd . mlcc ) pt . mlc = cd . mlcc ;
975
+ if ( cd . mlw ) pt . mlw = cd . mlw ;
976
+ if ( cd . mrc ) pt . mrc = cd . mrc ;
977
+ if ( cd . dir ) pt . dir = cd . dir ;
978
+ }
979
+ pt . _distinct = true ;
980
+
981
+ legendOpts . entries . push ( [ pt ] ) ;
982
+ }
983
+ legendOpts . entries . sort ( function ( a , b ) { return a [ 0 ] . trace . index - b [ 0 ] . trace . index ; } ) ;
984
+ legendOpts . layer = container ;
985
+
986
+ // Draw unified hover label
987
+ legendDraw ( gd , legendOpts ) ;
988
+
989
+ // Position the hover
990
+ var ly = Lib . mean ( hoverData . map ( function ( c ) { return ( c . y0 + c . y1 ) / 2 ; } ) ) ;
991
+ var lx = Lib . mean ( hoverData . map ( function ( c ) { return ( c . x0 + c . x1 ) / 2 ; } ) ) ;
992
+ var legendContainer = container . select ( 'g.legend' ) ;
993
+ var tbb = legendContainer . node ( ) . getBoundingClientRect ( ) ;
994
+ lx += xa . _offset ;
995
+ ly += ya . _offset - tbb . height / 2 ;
996
+
997
+ // Change horizontal alignment to end up on screen
998
+ var txWidth = tbb . width + 2 * HOVERTEXTPAD ;
999
+ var anchorStartOK = lx + txWidth <= outerWidth ;
1000
+ var anchorEndOK = lx - txWidth >= 0 ;
1001
+ if ( ! anchorStartOK && anchorEndOK ) {
1002
+ lx -= txWidth ;
1003
+ } else {
1004
+ lx += 2 * HOVERTEXTPAD ;
1005
+ }
1006
+
1007
+ // Change vertical alignement to end up on screen
1008
+ var txHeight = tbb . height + 2 * HOVERTEXTPAD ;
1009
+ var overflowTop = ly <= outerTop ;
1010
+ var overflowBottom = ly + txHeight >= outerHeight ;
1011
+ var canFit = txHeight <= outerHeight ;
1012
+ if ( canFit ) {
1013
+ if ( overflowTop ) {
1014
+ ly = ya . _offset + 2 * HOVERTEXTPAD ;
1015
+ } else if ( overflowBottom ) {
1016
+ ly = outerHeight - txHeight ;
1017
+ }
1018
+ }
1019
+ legendContainer . attr ( 'transform' , 'translate(' + lx + ',' + ly + ')' ) ;
1020
+
1021
+ return legendContainer ;
1022
+ }
914
1023
915
1024
// show all the individual labels
916
1025
@@ -941,8 +1050,6 @@ function createHoverText(hoverData, opts, gd) {
941
1050
// and figure out sizes
942
1051
hoverLabels . each ( function ( d ) {
943
1052
var g = d3 . select ( this ) . attr ( 'transform' , '' ) ;
944
- var name = '' ;
945
- var text = '' ;
946
1053
947
1054
// combine possible non-opaque trace color with bgColor
948
1055
var color0 = d . bgcolor || d . color ;
@@ -959,72 +1066,9 @@ function createHoverText(hoverData, opts, gd) {
959
1066
// find a contrasting color for border and text
960
1067
var contrastColor = d . borderColor || Color . contrast ( numsColor ) ;
961
1068
962
- // to get custom 'name' labels pass cleanPoint
963
- if ( d . nameOverride !== undefined ) d . name = d . nameOverride ;
964
-
965
- if ( d . name ) {
966
- if ( d . trace . _meta ) {
967
- d . name = Lib . templateString ( d . name , d . trace . _meta ) ;
968
- }
969
- name = plainText ( d . name , d . nameLength ) ;
970
- }
971
-
972
- if ( d . zLabel !== undefined ) {
973
- if ( d . xLabel !== undefined ) text += 'x: ' + d . xLabel + '<br>' ;
974
- if ( d . yLabel !== undefined ) text += 'y: ' + d . yLabel + '<br>' ;
975
- if ( d . trace . type !== 'choropleth' && d . trace . type !== 'choroplethmapbox' ) {
976
- text += ( text ? 'z: ' : '' ) + d . zLabel ;
977
- }
978
- } else if ( showCommonLabel && d [ hovermode + 'Label' ] === t0 ) {
979
- text = d [ ( hovermode === 'x' ? 'y' : 'x' ) + 'Label' ] || '' ;
980
- } else if ( d . xLabel === undefined ) {
981
- if ( d . yLabel !== undefined && d . trace . type !== 'scattercarpet' ) {
982
- text = d . yLabel ;
983
- }
984
- } else if ( d . yLabel === undefined ) text = d . xLabel ;
985
- else text = '(' + d . xLabel + ', ' + d . yLabel + ')' ;
986
-
987
- if ( ( d . text || d . text === 0 ) && ! Array . isArray ( d . text ) ) {
988
- text += ( text ? '<br>' : '' ) + d . text ;
989
- }
990
-
991
- // used by other modules (initially just ternary) that
992
- // manage their own hoverinfo independent of cleanPoint
993
- // the rest of this will still apply, so such modules
994
- // can still put things in (x|y|z)Label, text, and name
995
- // and hoverinfo will still determine their visibility
996
- if ( d . extraText !== undefined ) text += ( text ? '<br>' : '' ) + d . extraText ;
997
-
998
- // if 'text' is empty at this point,
999
- // and hovertemplate is not defined,
1000
- // put 'name' in main label and don't show secondary label
1001
- if ( text === '' && ! d . hovertemplate ) {
1002
- // if 'name' is also empty, remove entire label
1003
- if ( name === '' ) g . remove ( ) ;
1004
- text = name ;
1005
- }
1006
-
1007
- // hovertemplate
1008
- var d3locale = fullLayout . _d3locale ;
1009
- var hovertemplate = d . hovertemplate || false ;
1010
- var hovertemplateLabels = d . hovertemplateLabels || d ;
1011
- var eventData = d . eventData [ 0 ] || { } ;
1012
- if ( hovertemplate ) {
1013
- text = Lib . hovertemplateString (
1014
- hovertemplate ,
1015
- hovertemplateLabels ,
1016
- d3locale ,
1017
- eventData ,
1018
- d . trace . _meta
1019
- ) ;
1020
-
1021
- text = text . replace ( EXTRA_STRING_REGEX , function ( match , extra ) {
1022
- // assign name for secondary text label
1023
- name = plainText ( extra , d . nameLength ) ;
1024
- // remove from main text label
1025
- return '' ;
1026
- } ) ;
1027
- }
1069
+ var texts = getHoverLabelText ( d , showCommonLabel , hovermode , fullLayout , t0 , g ) ;
1070
+ var text = texts [ 0 ] ;
1071
+ var name = texts [ 1 ] ;
1028
1072
1029
1073
// main label
1030
1074
var tx = g . select ( 'text.nums' )
@@ -1123,6 +1167,78 @@ function createHoverText(hoverData, opts, gd) {
1123
1167
return hoverLabels ;
1124
1168
}
1125
1169
1170
+ function getHoverLabelText ( d , showCommonLabel , hovermode , fullLayout , t0 , g ) {
1171
+ var name = '' ;
1172
+ var text = '' ;
1173
+ // to get custom 'name' labels pass cleanPoint
1174
+ if ( d . nameOverride !== undefined ) d . name = d . nameOverride ;
1175
+
1176
+ if ( d . name ) {
1177
+ if ( d . trace . _meta ) {
1178
+ d . name = Lib . templateString ( d . name , d . trace . _meta ) ;
1179
+ }
1180
+ name = plainText ( d . name , d . nameLength ) ;
1181
+ }
1182
+
1183
+ if ( d . zLabel !== undefined ) {
1184
+ if ( d . xLabel !== undefined ) text += 'x: ' + d . xLabel + '<br>' ;
1185
+ if ( d . yLabel !== undefined ) text += 'y: ' + d . yLabel + '<br>' ;
1186
+ if ( d . trace . type !== 'choropleth' && d . trace . type !== 'choroplethmapbox' ) {
1187
+ text += ( text ? 'z: ' : '' ) + d . zLabel ;
1188
+ }
1189
+ } else if ( showCommonLabel && d [ hovermode . charAt ( 0 ) + 'Label' ] === t0 ) {
1190
+ text = d [ ( hovermode . charAt ( 0 ) === 'x' ? 'y' : 'x' ) + 'Label' ] || '' ;
1191
+ } else if ( d . xLabel === undefined ) {
1192
+ if ( d . yLabel !== undefined && d . trace . type !== 'scattercarpet' ) {
1193
+ text = d . yLabel ;
1194
+ }
1195
+ } else if ( d . yLabel === undefined ) text = d . xLabel ;
1196
+ else text = '(' + d . xLabel + ', ' + d . yLabel + ')' ;
1197
+
1198
+ if ( ( d . text || d . text === 0 ) && ! Array . isArray ( d . text ) ) {
1199
+ text += ( text ? '<br>' : '' ) + d . text ;
1200
+ }
1201
+
1202
+ // used by other modules (initially just ternary) that
1203
+ // manage their own hoverinfo independent of cleanPoint
1204
+ // the rest of this will still apply, so such modules
1205
+ // can still put things in (x|y|z)Label, text, and name
1206
+ // and hoverinfo will still determine their visibility
1207
+ if ( d . extraText !== undefined ) text += ( text ? '<br>' : '' ) + d . extraText ;
1208
+
1209
+ // if 'text' is empty at this point,
1210
+ // and hovertemplate is not defined,
1211
+ // put 'name' in main label and don't show secondary label
1212
+ if ( g && text === '' && ! d . hovertemplate ) {
1213
+ // if 'name' is also empty, remove entire label
1214
+ if ( name === '' ) g . remove ( ) ;
1215
+ text = name ;
1216
+ }
1217
+
1218
+ // hovertemplate
1219
+ var d3locale = fullLayout . _d3locale ;
1220
+ var hovertemplate = d . hovertemplate || false ;
1221
+ var hovertemplateLabels = d . hovertemplateLabels || d ;
1222
+ var eventData = d . eventData [ 0 ] || { } ;
1223
+ if ( hovertemplate ) {
1224
+ text = Lib . hovertemplateString (
1225
+ hovertemplate ,
1226
+ hovertemplateLabels ,
1227
+ d3locale ,
1228
+ eventData ,
1229
+ d . trace . _meta
1230
+ ) ;
1231
+
1232
+ text = text . replace ( EXTRA_STRING_REGEX , function ( match , extra ) {
1233
+ // assign name for secondary text label
1234
+ name = plainText ( extra , d . nameLength ) ;
1235
+ // remove from main text label
1236
+ return '' ;
1237
+ } ) ;
1238
+ }
1239
+ return [ text , name ] ;
1240
+ }
1241
+
1126
1242
// Make groups of touching points, and within each group
1127
1243
// move each point so that no labels overlap, but the average
1128
1244
// label position is the same as it was before moving. Indicentally,
0 commit comments