@@ -25,6 +25,9 @@ var Registry = require('../../registry');
2525var helpers = require ( './helpers' ) ;
2626var constants = require ( './constants' ) ;
2727
28+ var legendSupplyDefaults = require ( '../legend/defaults' ) ;
29+ var legendDraw = require ( '../legend/draw' ) ;
30+
2831// hover labels for multiple horizontal bars get tilted by some angle,
2932// then need to be offset differently if they overlap
3033var YANGLE = constants . YANGLE ;
@@ -244,7 +247,7 @@ function _hover(gd, evt, subplot, noHoverEvent) {
244247
245248 if ( hovermode && ! supportsCompare ) hovermode = 'closest' ;
246249
247- if ( [ 'x' , 'y' , 'closest' ] . indexOf ( hovermode ) === - 1 || ! gd . calcdata ||
250+ if ( [ 'x' , 'y' , 'closest' , 'x unified' , 'y unified' ] . indexOf ( hovermode ) === - 1 || ! gd . calcdata ||
248251 gd . querySelector ( '.zoombox' ) || gd . _dragging ) {
249252 return dragElement . unhoverRaw ( gd , evt ) ;
250253 }
@@ -388,6 +391,9 @@ function _hover(gd, evt, subplot, noHoverEvent) {
388391
389392 // within one trace mode can sometimes be overridden
390393 mode = hovermode ;
394+ if ( [ 'x unified' , 'y unified' ] . indexOf ( mode ) !== - 1 ) {
395+ mode = mode . charAt ( 0 ) ;
396+ }
391397
392398 // container for new point, also used to pass info into module.hoverPoints
393399 pointData = {
@@ -661,9 +667,10 @@ function _hover(gd, evt, subplot, noHoverEvent) {
661667
662668 var hoverLabels = createHoverText ( hoverData , labelOpts , gd ) ;
663669
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+ }
667674
668675 // TODO: tagName hack is needed to appease geo.js's hack of using evt.target=true
669676 // we should improve the "fx" API so other plots can use it without these hack.
@@ -712,7 +719,7 @@ function createHoverText(hoverData, opts, gd) {
712719 var c0 = hoverData [ 0 ] ;
713720 var xa = c0 . xa ;
714721 var ya = c0 . ya ;
715- var commonAttr = hovermode === 'y' ? 'yLabel' : 'xLabel' ;
722+ var commonAttr = hovermode . charAt ( 0 ) === 'y' ? 'yLabel' : 'xLabel' ;
716723 var t0 = c0 [ commonAttr ] ;
717724 var t00 = ( String ( t0 ) || '' ) . split ( ' ' ) [ 0 ] ;
718725 var outerContainerBB = outerContainer . node ( ) . getBoundingClientRect ( ) ;
@@ -906,11 +913,113 @@ function createHoverText(hoverData, opts, gd) {
906913
907914 // remove the "close but not quite" points
908915 // 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 ) {
910921 return ( d . zLabelVal !== undefined ) ||
911922 ( d [ commonAttr ] || '' ) . split ( ' ' ) [ 0 ] === t00 ;
912923 } ) ;
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+ }
9141023
9151024 // show all the individual labels
9161025
@@ -941,8 +1050,6 @@ function createHoverText(hoverData, opts, gd) {
9411050 // and figure out sizes
9421051 hoverLabels . each ( function ( d ) {
9431052 var g = d3 . select ( this ) . attr ( 'transform' , '' ) ;
944- var name = '' ;
945- var text = '' ;
9461053
9471054 // combine possible non-opaque trace color with bgColor
9481055 var color0 = d . bgcolor || d . color ;
@@ -959,72 +1066,9 @@ function createHoverText(hoverData, opts, gd) {
9591066 // find a contrasting color for border and text
9601067 var contrastColor = d . borderColor || Color . contrast ( numsColor ) ;
9611068
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 ] ;
10281072
10291073 // main label
10301074 var tx = g . select ( 'text.nums' )
@@ -1123,6 +1167,78 @@ function createHoverText(hoverData, opts, gd) {
11231167 return hoverLabels ;
11241168}
11251169
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+
11261242// Make groups of touching points, and within each group
11271243// move each point so that no labels overlap, but the average
11281244// label position is the same as it was before moving. Indicentally,
0 commit comments