@@ -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