@@ -696,6 +696,101 @@ describe('sankey tests', function () {
696696 . then ( done , done . fail ) ;
697697 } ) ;
698698
699+ describe ( 'explicit node.(x|y) positions' , ( ) => {
700+ // Node 1 has no links, so it is absent from the rendered graph
701+ const makeFixedMock = ( ) => ( {
702+ data : [
703+ {
704+ type : 'sankey' ,
705+ arrangement : 'fixed' ,
706+ node : {
707+ label : [ 'a' , 'unlinked' , 'b' , 'c' , 'd' ] ,
708+ x : [ 0.1 , 0.5 , 0.5 , 0.9 , 0.9 ] ,
709+ y : [ 0.2 , 0.5 , 0.3 , 0.8 , 0.1 ] ,
710+ pad : 5
711+ } ,
712+ link : {
713+ source : [ 0 , 0 , 2 ] ,
714+ target : [ 2 , 3 , 4 ] ,
715+ value : [ 1 , 1 , 1 ]
716+ }
717+ }
718+ ] ,
719+ layout : { width : 600 , height : 400 }
720+ } ) ;
721+
722+ const getCentersByIndex = ( ) => {
723+ const centers = { } ;
724+ // d3 v3 binds the element to `this`, so this callback stays a function
725+ d3SelectAll ( '.sankey-node' ) . each ( function ( d ) {
726+ // measure the rect because the label shifts the group bounding box
727+ centers [ d . index ] = getNodeCoords ( this . querySelector ( '.node-rect' ) ) ;
728+ } ) ;
729+ return centers ;
730+ } ;
731+
732+ const assertPositions = ( trace , centers , indices , msg ) => {
733+ const size = gd . _fullLayout . _size ;
734+ const ref = indices [ 0 ] ;
735+ indices . forEach ( ( i ) => {
736+ const dx = ( trace . node . x [ i ] - trace . node . x [ ref ] ) * size . w ;
737+ const dy = ( trace . node . y [ i ] - trace . node . y [ ref ] ) * size . h ;
738+ expect ( centers [ i ] . x - centers [ ref ] . x ) . toBeCloseTo ( dx , 0 , msg + ': node ' + i + ' x' ) ;
739+ expect ( centers [ i ] . y - centers [ ref ] . y ) . toBeCloseTo ( dy , 0 , msg + ': node ' + i + ' y' ) ;
740+ } ) ;
741+ } ;
742+
743+ it ( 'applies positions to the right nodes when a node is unlinked' , ( done ) => {
744+ const fig = makeFixedMock ( ) ;
745+
746+ Plotly . newPlot ( gd , fig )
747+ . then ( ( ) => {
748+ assertPositions ( fig . data [ 0 ] , getCentersByIndex ( ) , [ 0 , 2 , 3 , 4 ] , 'fixed' ) ;
749+ } )
750+ . then ( done , done . fail ) ;
751+ } ) ;
752+
753+ it ( 'applies positions to the right nodes when nodes are grouped' , ( done ) => {
754+ const fig = makeFixedMock ( ) ;
755+ // Grouping nodes 3 and 4 leaves them unlinked and adds group node 5
756+ fig . data [ 0 ] . node . groups = [ [ 3 , 4 ] ] ;
757+
758+ Plotly . newPlot ( gd , fig )
759+ . then ( ( ) => {
760+ assertPositions ( fig . data [ 0 ] , getCentersByIndex ( ) , [ 0 , 2 ] , 'grouped' ) ;
761+ } )
762+ . then ( done , done . fail ) ;
763+ } ) ;
764+
765+ it ( 'keeps the positions of the other nodes after a drag when a node is unlinked' , ( done ) => {
766+ const fig = makeFixedMock ( ) ;
767+ fig . data [ 0 ] . arrangement = 'freeform' ;
768+ let before ;
769+
770+ Plotly . newPlot ( gd , fig )
771+ . then ( ( ) => {
772+ before = getCentersByIndex ( ) ;
773+ const node = d3SelectAll ( '.sankey-node' )
774+ . filter ( ( d ) => d . index === 2 )
775+ . node ( ) ;
776+ return drag ( { node : node , dpos : [ 0 , 30 ] } ) ;
777+ } )
778+ . then ( ( ) => {
779+ const { x } = gd . _fullData [ 0 ] . node ;
780+ expect ( x . length ) . toBe ( 5 , 'x covers the unlinked node' ) ;
781+ expect ( x [ 1 ] ) . toBeCloseTo ( 0.5 , 5 , 'unlinked node x is kept' ) ;
782+
783+ const after = getCentersByIndex ( ) ;
784+ [ 0 , 3 , 4 ] . forEach ( ( i ) => {
785+ expect ( after [ i ] . x ) . toBeCloseTo ( before [ i ] . x , 0 , 'node ' + i + ' x' ) ;
786+ expect ( after [ i ] . y ) . toBeCloseTo ( before [ i ] . y , 0 , 'node ' + i + ' y' ) ;
787+ } ) ;
788+ expect ( after [ 2 ] . y ) . toBeCloseTo ( before [ 2 ] . y + 30 , 0 , 'dragged node y' ) ;
789+ } )
790+ . then ( done , done . fail ) ;
791+ } ) ;
792+ } ) ;
793+
699794 it ( 'resets each subplot to its initial view (ie. x, y groups) via modebar button' , function ( done ) {
700795 var mockCopy = Lib . extendDeep ( { } , require ( '../../image/mocks/sankey_subplots_circular' ) ) ;
701796
0 commit comments