@@ -1284,7 +1284,6 @@ describe('Test gl2d interactions', function() {
12841284 } ) ;
12851285
12861286 it ( 'data-referenced annotations should update on drag' , function ( done ) {
1287-
12881287 function drag ( start , end ) {
12891288 mouseEvent ( 'mousemove' , start [ 0 ] , start [ 1 ] ) ;
12901289 mouseEvent ( 'mousedown' , start [ 0 ] , start [ 1 ] , { buttons : 1 } ) ;
@@ -1329,3 +1328,336 @@ describe('Test gl2d interactions', function() {
13291328 . then ( done ) ;
13301329 } ) ;
13311330} ) ;
1331+
1332+ describe ( 'Test gl3d annotations' , function ( ) {
1333+ var gd ;
1334+
1335+ beforeAll ( function ( ) {
1336+ jasmine . addMatchers ( customMatchers ) ;
1337+ } ) ;
1338+
1339+ beforeEach ( function ( ) {
1340+ gd = createGraphDiv ( ) ;
1341+ } ) ;
1342+
1343+ afterEach ( function ( ) {
1344+ Plotly . purge ( gd ) ;
1345+ destroyGraphDiv ( ) ;
1346+ } ) ;
1347+
1348+ function assertAnnotationText ( expectations , msg ) {
1349+ var anns = d3 . selectAll ( 'g.annotation-text-g' ) ;
1350+
1351+ expect ( anns . size ( ) ) . toBe ( expectations . length , msg ) ;
1352+
1353+ anns . each ( function ( _ , i ) {
1354+ var tx = d3 . select ( this ) . select ( 'text' ) . text ( ) ;
1355+ expect ( tx ) . toEqual ( expectations [ i ] , msg + ' - ann ' + i ) ;
1356+ } ) ;
1357+ }
1358+
1359+ function assertAnnotationsXY ( expectations , msg ) {
1360+ var TOL = 1.5 ;
1361+ var anns = d3 . selectAll ( 'g.annotation-text-g' ) ;
1362+
1363+ expect ( anns . size ( ) ) . toBe ( expectations . length , msg ) ;
1364+
1365+ anns . each ( function ( _ , i ) {
1366+ var ann = d3 . select ( this ) . select ( 'g' ) ;
1367+ var translate = Drawing . getTranslate ( ann ) ;
1368+
1369+ expect ( translate . x ) . toBeWithin ( expectations [ i ] [ 0 ] , TOL , msg + ' - ann ' + i + ' x' ) ;
1370+ expect ( translate . y ) . toBeWithin ( expectations [ i ] [ 1 ] , TOL , msg + ' - ann ' + i + ' y' ) ;
1371+ } ) ;
1372+ }
1373+
1374+ // more robust (especially on CI) than update camera via mouse events
1375+ function updateCamera ( x , y , z ) {
1376+ return new Promise ( function ( resolve ) {
1377+ var scene = gd . _fullLayout . scene . _scene ;
1378+ var camera = scene . getCamera ( ) ;
1379+
1380+ camera . eye = { x : x , y : y , z : z } ;
1381+ scene . setCamera ( camera ) ;
1382+
1383+ setTimeout ( resolve , 100 ) ;
1384+ } ) ;
1385+ }
1386+
1387+ it ( 'should move with camera' , function ( done ) {
1388+ Plotly . plot ( gd , [ {
1389+ type : 'scatter3d' ,
1390+ x : [ 1 , 2 , 3 ] ,
1391+ y : [ 1 , 2 , 3 ] ,
1392+ z : [ 1 , 2 , 1 ]
1393+ } ] , {
1394+ scene : {
1395+ camera : { eye : { x : 2.1 , y : 0.1 , z : 0.9 } } ,
1396+ annotations : [ {
1397+ text : 'hello' ,
1398+ x : 1 , y : 1 , z : 1
1399+ } , {
1400+ text : 'sup?' ,
1401+ x : 1 , y : 1 , z : 2
1402+ } , {
1403+ text : 'look!' ,
1404+ x : 2 , y : 2 , z : 1
1405+ } ]
1406+ }
1407+ } )
1408+ . then ( function ( ) {
1409+ assertAnnotationsXY ( [ [ 262 , 199 ] , [ 257 , 135 ] , [ 325 , 233 ] ] , 'base 0' ) ;
1410+
1411+ return updateCamera ( 1.5 , 2.5 , 1.5 ) ;
1412+ } )
1413+ . then ( function ( ) {
1414+ assertAnnotationsXY ( [ [ 340 , 187 ] , [ 341 , 142 ] , [ 325 , 221 ] ] , 'after camera update' ) ;
1415+
1416+ return updateCamera ( 2.1 , 0.1 , 0.9 ) ;
1417+ } )
1418+ . then ( function ( ) {
1419+ assertAnnotationsXY ( [ [ 262 , 199 ] , [ 257 , 135 ] , [ 325 , 233 ] ] , 'base 0' ) ;
1420+ } )
1421+ . catch ( fail )
1422+ . then ( done ) ;
1423+ } ) ;
1424+
1425+ it ( 'should be removed when beyond the scene axis ranges' , function ( done ) {
1426+ var mock = Lib . extendDeep ( { } , require ( '@mocks/gl3d_annotations' ) ) ;
1427+
1428+ // replace text with something easier to identify
1429+ mock . layout . scene . annotations . forEach ( function ( ann , i ) { ann . text = String ( i ) ; } ) ;
1430+
1431+ Plotly . plot ( gd , mock ) . then ( function ( ) {
1432+ assertAnnotationText ( [ '0' , '1' , '2' , '3' ] , 'base' ) ;
1433+
1434+ return Plotly . relayout ( gd , 'scene.yaxis.range' , [ 0.5 , 1.5 ] ) ;
1435+ } )
1436+ . then ( function ( ) {
1437+ assertAnnotationText ( [ '1' ] , 'after yaxis range relayout' ) ;
1438+
1439+ return Plotly . relayout ( gd , 'scene.yaxis.range' , null ) ;
1440+ } )
1441+ . then ( function ( ) {
1442+ assertAnnotationText ( [ '0' , '1' , '2' , '3' ] , 'back to base after yaxis range relayout' ) ;
1443+
1444+ return Plotly . relayout ( gd , 'scene.zaxis.range' , [ 0 , 3 ] ) ;
1445+ } )
1446+ . then ( function ( ) {
1447+ assertAnnotationText ( [ '0' ] , 'after zaxis range relayout' ) ;
1448+
1449+ return Plotly . relayout ( gd , 'scene.zaxis.range' , null ) ;
1450+ } )
1451+ . then ( function ( ) {
1452+ assertAnnotationText ( [ '0' , '1' , '2' , '3' ] , 'back to base after zaxis range relayout' ) ;
1453+ } )
1454+ . catch ( fail )
1455+ . then ( done ) ;
1456+ } ) ;
1457+
1458+ it ( 'should be able to add/remove and hide/unhide themselves via relayout' , function ( done ) {
1459+ var mock = Lib . extendDeep ( { } , require ( '@mocks/gl3d_annotations' ) ) ;
1460+
1461+ // replace text with something easier to identify
1462+ mock . layout . scene . annotations . forEach ( function ( ann , i ) { ann . text = String ( i ) ; } ) ;
1463+
1464+ var annNew = {
1465+ x : '2017-03-01' ,
1466+ y : 'C' ,
1467+ z : 3 ,
1468+ text : 'new!'
1469+ } ;
1470+
1471+ Plotly . plot ( gd , mock ) . then ( function ( ) {
1472+ assertAnnotationText ( [ '0' , '1' , '2' , '3' ] , 'base' ) ;
1473+
1474+ return Plotly . relayout ( gd , 'scene.annotations[1].visible' , false ) ;
1475+ } )
1476+ . then ( function ( ) {
1477+ assertAnnotationText ( [ '0' , '2' , '3' ] , 'after [1].visible:false' ) ;
1478+
1479+ return Plotly . relayout ( gd , 'scene.annotations[1].visible' , true ) ;
1480+ } )
1481+ . then ( function ( ) {
1482+ assertAnnotationText ( [ '0' , '1' , '2' , '3' ] , 'back to base (1)' ) ;
1483+
1484+ return Plotly . relayout ( gd , 'scene.annotations[0]' , null ) ;
1485+ } )
1486+ . then ( function ( ) {
1487+ assertAnnotationText ( [ '1' , '2' , '3' ] , 'after [0] null' ) ;
1488+
1489+ return Plotly . relayout ( gd , 'scene.annotations[0]' , annNew ) ;
1490+ } )
1491+ . then ( function ( ) {
1492+ assertAnnotationText ( [ 'new!' , '1' , '2' , '3' ] , 'after add new (1)' ) ;
1493+
1494+ return Plotly . relayout ( gd , 'scene.annotations' , null ) ;
1495+ } )
1496+ . then ( function ( ) {
1497+ assertAnnotationText ( [ ] , 'after rm all' ) ;
1498+
1499+ return Plotly . relayout ( gd , 'scene.annotations[0]' , annNew ) ;
1500+ } )
1501+ . then ( function ( ) {
1502+ assertAnnotationText ( [ 'new!' ] , 'after add new (2)' ) ;
1503+ } )
1504+ . catch ( fail )
1505+ . then ( done ) ;
1506+ } ) ;
1507+
1508+ it ( 'should work across multiple scenes' , function ( done ) {
1509+ function assertAnnotationCntPerScene ( id , cnt ) {
1510+ expect ( d3 . selectAll ( 'g.annotation-' + id ) . size ( ) ) . toEqual ( cnt ) ;
1511+ }
1512+
1513+ Plotly . plot ( gd , [ {
1514+ type : 'scatter3d' ,
1515+ x : [ 1 , 2 , 3 ] ,
1516+ y : [ 1 , 2 , 3 ] ,
1517+ z : [ 1 , 2 , 1 ]
1518+ } , {
1519+ type : 'scatter3d' ,
1520+ x : [ 1 , 2 , 3 ] ,
1521+ y : [ 1 , 2 , 3 ] ,
1522+ z : [ 2 , 1 , 2 ] ,
1523+ scene : 'scene2'
1524+ } ] , {
1525+ scene : {
1526+ annotations : [ {
1527+ text : 'hello' ,
1528+ x : 1 , y : 1 , z : 1
1529+ } ]
1530+ } ,
1531+ scene2 : {
1532+ annotations : [ {
1533+ text : 'sup?' ,
1534+ x : 1 , y : 1 , z : 2
1535+ } , {
1536+ text : 'look!' ,
1537+ x : 2 , y : 2 , z : 1
1538+ } ]
1539+ }
1540+ } )
1541+ . then ( function ( ) {
1542+ assertAnnotationCntPerScene ( 'scene' , 1 ) ;
1543+ assertAnnotationCntPerScene ( 'scene2' , 2 ) ;
1544+
1545+ return Plotly . deleteTraces ( gd , [ 1 ] ) ;
1546+ } )
1547+ . then ( function ( ) {
1548+ assertAnnotationCntPerScene ( 'scene' , 1 ) ;
1549+ assertAnnotationCntPerScene ( 'scene2' , 0 ) ;
1550+
1551+ return Plotly . deleteTraces ( gd , [ 0 ] ) ;
1552+ } )
1553+ . then ( function ( ) {
1554+ assertAnnotationCntPerScene ( 'scene' , 0 ) ;
1555+ assertAnnotationCntPerScene ( 'scene2' , 0 ) ;
1556+ } )
1557+ . catch ( fail )
1558+ . then ( done ) ;
1559+ } ) ;
1560+
1561+ it ( 'should contribute to scene axis autorange' , function ( done ) {
1562+ function assertSceneAxisRanges ( xRange , yRange , zRange ) {
1563+ var sceneLayout = gd . _fullLayout . scene ;
1564+
1565+ expect ( sceneLayout . xaxis . range ) . toBeCloseToArray ( xRange , 1 , 'xaxis range' ) ;
1566+ expect ( sceneLayout . yaxis . range ) . toBeCloseToArray ( yRange , 1 , 'yaxis range' ) ;
1567+ expect ( sceneLayout . zaxis . range ) . toBeCloseToArray ( zRange , 1 , 'zaxis range' ) ;
1568+ }
1569+
1570+ Plotly . plot ( gd , [ {
1571+ type : 'scatter3d' ,
1572+ x : [ 1 , 2 , 3 ] ,
1573+ y : [ 1 , 2 , 3 ] ,
1574+ z : [ 1 , 2 , 1 ]
1575+ } ] , {
1576+ scene : {
1577+ annotations : [ {
1578+ text : 'hello' ,
1579+ x : 1 , y : 1 , z : 3
1580+ } ]
1581+ }
1582+ } )
1583+ . then ( function ( ) {
1584+ assertSceneAxisRanges ( [ 0.9375 , 3.0625 ] , [ 0.9375 , 3.0625 ] , [ 0.9375 , 3.0625 ] ) ;
1585+
1586+ return Plotly . relayout ( gd , 'scene.annotations[0].z' , 10 ) ;
1587+ } )
1588+ . then ( function ( ) {
1589+ assertSceneAxisRanges ( [ 0.9375 , 3.0625 ] , [ 0.9375 , 3.0625 ] , [ 0.7187 , 10.2813 ] ) ;
1590+ } )
1591+ . catch ( fail )
1592+ . then ( done ) ;
1593+ } ) ;
1594+
1595+ it ( 'should allow text and tail position edits under `editable: true`' , function ( done ) {
1596+ function editText ( newText , expectation ) {
1597+ return new Promise ( function ( resolve ) {
1598+ gd . once ( 'plotly_relayout' , function ( eventData ) {
1599+ expect ( eventData ) . toEqual ( expectation ) ;
1600+ setTimeout ( resolve , 0 ) ;
1601+ } ) ;
1602+
1603+ var clickNode = d3 . select ( 'g.annotation-text-g' ) . select ( 'g' ) . node ( ) ;
1604+ clickNode . dispatchEvent ( new window . MouseEvent ( 'click' ) ) ;
1605+
1606+ var editNode = d3 . select ( '.plugin-editable.editable' ) . node ( ) ;
1607+ editNode . dispatchEvent ( new window . FocusEvent ( 'focus' ) ) ;
1608+
1609+ editNode . textContent = newText ;
1610+ editNode . dispatchEvent ( new window . FocusEvent ( 'focus' ) ) ;
1611+ editNode . dispatchEvent ( new window . FocusEvent ( 'blur' ) ) ;
1612+ } ) ;
1613+ }
1614+
1615+ function moveArrowTail ( dx , dy , expectation ) {
1616+ var px = 243 ;
1617+ var py = 150 ;
1618+
1619+ return new Promise ( function ( resolve ) {
1620+ gd . once ( 'plotly_relayout' , function ( eventData ) {
1621+ expect ( eventData ) . toEqual ( expectation ) ;
1622+ resolve ( ) ;
1623+ } ) ;
1624+
1625+ mouseEvent ( 'mousemove' , px , py ) ;
1626+ mouseEvent ( 'mousedown' , px , py ) ;
1627+ mouseEvent ( 'mousemove' , px + dx , py + dy ) ;
1628+ mouseEvent ( 'mouseup' , px + dx , py + dy ) ;
1629+ } ) ;
1630+ }
1631+
1632+ Plotly . plot ( gd , [ {
1633+ type : 'scatter3d' ,
1634+ x : [ 1 , 2 , 3 ] ,
1635+ y : [ 1 , 2 , 3 ] ,
1636+ z : [ 1 , 2 , 1 ]
1637+ } ] , {
1638+ scene : {
1639+ annotations : [ {
1640+ text : 'hello' ,
1641+ x : 2 , y : 2 , z : 2 ,
1642+ font : { size : 30 }
1643+ } ]
1644+ } ,
1645+ margin : { l : 0 , t : 0 , r : 0 , b : 0 } ,
1646+ width : 500 ,
1647+ height : 500
1648+ } , {
1649+ editable : true
1650+ } )
1651+ . then ( function ( ) {
1652+ return editText ( 'allo' , { 'scene.annotations[0].text' : 'allo' } ) ;
1653+ } )
1654+ . then ( function ( ) {
1655+ return moveArrowTail ( - 100 , - 50 , {
1656+ 'scene.annotations[0].ax' : - 110 ,
1657+ 'scene.annotations[0].ay' : - 80
1658+ } ) ;
1659+ } )
1660+ . catch ( fail )
1661+ . then ( done ) ;
1662+ } ) ;
1663+ } ) ;
0 commit comments