diff --git a/client/app/scripts/charts/edge-container.js b/client/app/scripts/charts/edge-container.js index a3a917b538..369365f5b8 100644 --- a/client/app/scripts/charts/edge-container.js +++ b/client/app/scripts/charts/edge-container.js @@ -5,7 +5,7 @@ import { line, curveBasis } from 'd3-shape'; import { each, omit, times, constant } from 'lodash'; import { NODES_SPRING_ANIMATION_CONFIG } from '../constants/animation'; -import { EDGE_WAYPOINTS_CAP } from '../constants/styles'; +import { NODE_BASE_SIZE, EDGE_WAYPOINTS_CAP } from '../constants/styles'; import Edge from './edge'; @@ -14,8 +14,8 @@ const spline = line() .x(d => d.x) .y(d => d.y); -const transformedEdge = (props, path) => ( - +const transformedEdge = (props, path, thickness) => ( + ); // Converts a waypoints map of the format { x0: 11, y0: 22, x1: 33, y1: 44 } @@ -45,7 +45,11 @@ const waypointsArrayToMap = (waypointsArray) => { export default class EdgeContainer extends React.PureComponent { constructor(props, context) { super(props, context); - this.state = { waypointsMap: makeMap() }; + + this.state = { + waypointsMap: makeMap(), + thickness: 1, + }; } componentWillMount() { @@ -59,21 +63,32 @@ export default class EdgeContainer extends React.PureComponent { if (this.props.isAnimated && nextProps.waypoints !== this.props.waypoints) { this.prepareWaypointsForMotion(nextProps.waypoints); } + // Edge thickness will reflect the zoom scale. + const baseScale = (nextProps.scale * 0.01) * NODE_BASE_SIZE; + const thickness = (nextProps.focused ? 3 : 1) * baseScale; + this.setState({ thickness }); } render() { const { isAnimated, waypoints } = this.props; - const forwardedProps = omit(this.props, 'isAnimated', 'waypoints'); + const forwardedProps = omit(this.props, 'isAnimated', 'waypoints', 'scale'); if (!isAnimated) { - return transformedEdge(forwardedProps, waypoints.toJS()); + return transformedEdge(forwardedProps, waypoints.toJS(), this.state.thickness); } return ( // For the Motion interpolation to work, the waypoints need to be in a map format like // { x0: 11, y0: 22, x1: 33, y1: 44 } that we convert to the array format when rendering. - - {interpolated => transformedEdge(forwardedProps, waypointsMapToArray(interpolated))} + + {({ thickness, ...interpolatedWaypoints}) => transformedEdge( + forwardedProps, waypointsMapToArray(interpolatedWaypoints), thickness + )} ); } diff --git a/client/app/scripts/charts/edge.js b/client/app/scripts/charts/edge.js index 84836dd4bf..ae0320a152 100644 --- a/client/app/scripts/charts/edge.js +++ b/client/app/scripts/charts/edge.js @@ -3,7 +3,6 @@ import { connect } from 'react-redux'; import classNames from 'classnames'; import { enterEdge, leaveEdge } from '../actions/app-actions'; -import { NODE_BASE_SIZE } from '../constants/styles'; class Edge extends React.Component { @@ -14,34 +13,22 @@ class Edge extends React.Component { } render() { - const { - id, - path, - highlighted, - blurred, - focused, - scale, - source, - target - } = this.props; - const className = classNames('edge', { highlighted, blurred, focused }); - const thickness = (scale * 0.01) * NODE_BASE_SIZE; - const strokeWidth = focused ? thickness * 3 : thickness; + const { id, path, highlighted, blurred, focused, thickness, source, target } = this.props; const shouldRenderMarker = (focused || highlighted) && (source !== target); - // Draws the edge so that its thickness reflects the zoom scale. - // Edge shadow is always made 10x thicker than the edge itself. + const className = classNames('edge', { highlighted, blurred }); + return ( - + ); diff --git a/client/app/scripts/charts/nodes-chart-edges.js b/client/app/scripts/charts/nodes-chart-edges.js deleted file mode 100644 index f8b2b77621..0000000000 --- a/client/app/scripts/charts/nodes-chart-edges.js +++ /dev/null @@ -1,91 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import { Map as makeMap } from 'immutable'; - -import { searchNodeMatchesSelector } from '../selectors/search'; -import { selectedNetworkNodesIdsSelector } from '../selectors/node-networks'; -import { highlightedEdgeIdsSelector } from '../selectors/graph-view/decorators'; -import { hasSelectedNode as hasSelectedNodeFn } from '../utils/topology-utils'; -import EdgeContainer from './edge-container'; - -class NodesChartEdges extends React.Component { - constructor(props, context) { - super(props, context); - - // Edge decorators - this.edgeFocusedDecorator = this.edgeFocusedDecorator.bind(this); - this.edgeBlurredDecorator = this.edgeBlurredDecorator.bind(this); - this.edgeHighlightedDecorator = this.edgeHighlightedDecorator.bind(this); - this.edgeScaleDecorator = this.edgeScaleDecorator.bind(this); - } - - edgeHighlightedDecorator(edge) { - return edge.set('highlighted', this.props.highlightedEdgeIds.has(edge.get('id'))); - } - - edgeFocusedDecorator(edge) { - const sourceSelected = (this.props.selectedNodeId === edge.get('source')); - const targetSelected = (this.props.selectedNodeId === edge.get('target')); - return edge.set('focused', this.props.hasSelectedNode && (sourceSelected || targetSelected)); - } - - edgeBlurredDecorator(edge) { - const { selectedNodeId, searchNodeMatches, selectedNetworkNodesIds } = this.props; - const sourceSelected = (selectedNodeId === edge.get('source')); - const targetSelected = (selectedNodeId === edge.get('target')); - const otherNodesSelected = this.props.hasSelectedNode && !sourceSelected && !targetSelected; - const sourceNoMatches = searchNodeMatches.get(edge.get('source'), makeMap()).isEmpty(); - const targetNoMatches = searchNodeMatches.get(edge.get('target'), makeMap()).isEmpty(); - const notMatched = this.props.searchQuery && (sourceNoMatches || targetNoMatches); - const sourceInNetwork = selectedNetworkNodesIds.contains(edge.get('source')); - const targetInNetwork = selectedNetworkNodesIds.contains(edge.get('target')); - const notInNetwork = this.props.selectedNetwork && (!sourceInNetwork || !targetInNetwork); - return edge.set('blurred', !edge.get('highlighted') && !edge.get('focused') && - (otherNodesSelected || notMatched || notInNetwork)); - } - - edgeScaleDecorator(edge) { - return edge.set('scale', edge.get('focused') ? this.props.selectedScale : 1); - } - - render() { - const { layoutEdges, isAnimated } = this.props; - - const edgesToRender = layoutEdges.toIndexedSeq() - .map(this.edgeHighlightedDecorator) - .map(this.edgeFocusedDecorator) - .map(this.edgeBlurredDecorator) - .map(this.edgeScaleDecorator); - - return ( - - {edgesToRender.map(edge => ( - - ))} - - ); - } -} - -export default connect( - state => ({ - hasSelectedNode: hasSelectedNodeFn(state), - searchNodeMatches: searchNodeMatchesSelector(state), - selectedNetworkNodesIds: selectedNetworkNodesIdsSelector(state), - highlightedEdgeIds: highlightedEdgeIdsSelector(state), - searchQuery: state.get('searchQuery'), - selectedNetwork: state.get('selectedNetwork'), - selectedNodeId: state.get('selectedNodeId'), - }) -)(NodesChartEdges); diff --git a/client/app/scripts/charts/nodes-chart-elements.js b/client/app/scripts/charts/nodes-chart-elements.js index fb9e1eca4a..c1479ff836 100644 --- a/client/app/scripts/charts/nodes-chart-elements.js +++ b/client/app/scripts/charts/nodes-chart-elements.js @@ -1,30 +1,236 @@ import React from 'react'; import { connect } from 'react-redux'; +import { Map as makeMap, List as makeList } from 'immutable'; -import NodesChartEdges from './nodes-chart-edges'; -import NodesChartNodes from './nodes-chart-nodes'; +import NodeContainer from './node-container'; +import EdgeContainer from './edge-container'; +import { getAdjacentNodes, hasSelectedNode as hasSelectedNodeFn } from '../utils/topology-utils'; import { graphExceedsComplexityThreshSelector } from '../selectors/topology'; +import { nodeNetworksSelector, selectedNetworkNodesIdsSelector } from '../selectors/node-networks'; +import { searchNodeMatchesSelector } from '../selectors/search'; +import { nodeMetricSelector } from '../selectors/node-metric'; +import { + highlightedNodeIdsSelector, + highlightedEdgeIdsSelector +} from '../selectors/graph-view/decorators'; import { selectedScaleSelector, layoutNodesSelector, layoutEdgesSelector } from '../selectors/graph-view/layout'; +import { + BLURRED_EDGES_LAYER, + BLURRED_NODES_LAYER, + NORMAL_EDGES_LAYER, + NORMAL_NODES_LAYER, + HIGHLIGHTED_EDGES_LAYER, + HIGHLIGHTED_NODES_LAYER, + HOVERED_EDGES_LAYER, + HOVERED_NODES_LAYER, +} from '../constants/naming'; + class NodesChartElements extends React.Component { + constructor(props, context) { + super(props, context); + + this.renderNode = this.renderNode.bind(this); + this.renderEdge = this.renderEdge.bind(this); + this.renderElement = this.renderElement.bind(this); + this.nodeDisplayLayer = this.nodeDisplayLayer.bind(this); + this.edgeDisplayLayer = this.edgeDisplayLayer.bind(this); + + // Node decorators + this.nodeHighlightedDecorator = this.nodeHighlightedDecorator.bind(this); + this.nodeFocusedDecorator = this.nodeFocusedDecorator.bind(this); + this.nodeBlurredDecorator = this.nodeBlurredDecorator.bind(this); + this.nodeMatchesDecorator = this.nodeMatchesDecorator.bind(this); + this.nodeNetworksDecorator = this.nodeNetworksDecorator.bind(this); + this.nodeMetricDecorator = this.nodeMetricDecorator.bind(this); + this.nodeScaleDecorator = this.nodeScaleDecorator.bind(this); + + // Edge decorators + this.edgeFocusedDecorator = this.edgeFocusedDecorator.bind(this); + this.edgeBlurredDecorator = this.edgeBlurredDecorator.bind(this); + this.edgeHighlightedDecorator = this.edgeHighlightedDecorator.bind(this); + this.edgeScaleDecorator = this.edgeScaleDecorator.bind(this); + } + + nodeDisplayLayer(node) { + if (node.get('id') === this.props.mouseOverNodeId) { + return HOVERED_NODES_LAYER; + } else if (node.get('blurred') && !node.get('focused')) { + return BLURRED_NODES_LAYER; + } else if (node.get('highlighted')) { + return HIGHLIGHTED_NODES_LAYER; + } + return NORMAL_NODES_LAYER; + } + + edgeDisplayLayer(edge) { + if (edge.get('id') === this.props.mouseOverEdgeId) { + return HOVERED_EDGES_LAYER; + } else if (edge.get('blurred') && !edge.get('focused')) { + return BLURRED_EDGES_LAYER; + } else if (edge.get('highlighted')) { + return HIGHLIGHTED_EDGES_LAYER; + } + return NORMAL_EDGES_LAYER; + } + + nodeHighlightedDecorator(node) { + const nodeSelected = (this.props.selectedNodeId === node.get('id')); + const nodeHighlighted = this.props.highlightedNodeIds.has(node.get('id')); + return node.set('highlighted', nodeHighlighted || nodeSelected); + } + + nodeFocusedDecorator(node) { + const nodeSelected = (this.props.selectedNodeId === node.get('id')); + const isNeighborOfSelected = this.props.neighborsOfSelectedNode.includes(node.get('id')); + return node.set('focused', nodeSelected || isNeighborOfSelected); + } + + nodeBlurredDecorator(node) { + const belongsToNetwork = this.props.selectedNetworkNodesIds.contains(node.get('id')); + const noMatches = this.props.searchNodeMatches.get(node.get('id'), makeMap()).isEmpty(); + const notMatched = (this.props.searchQuery && !node.get('highlighted') && noMatches); + const notFocused = (this.props.selectedNodeId && !node.get('focused')); + const notInNetwork = (this.props.selectedNetwork && !belongsToNetwork); + return node.set('blurred', notMatched || notFocused || notInNetwork); + } + + nodeMatchesDecorator(node) { + return node.set('matches', this.props.searchNodeMatches.get(node.get('id'))); + } + + nodeNetworksDecorator(node) { + return node.set('networks', this.props.nodeNetworks.get(node.get('id'))); + } + + nodeMetricDecorator(node) { + return node.set('metric', this.props.nodeMetric.get(node.get('id'))); + } + + nodeScaleDecorator(node) { + return node.set('scale', node.get('focused') ? this.props.selectedScale : 1); + } + + edgeHighlightedDecorator(edge) { + return edge.set('highlighted', this.props.highlightedEdgeIds.has(edge.get('id'))); + } + + edgeFocusedDecorator(edge) { + const sourceSelected = (this.props.selectedNodeId === edge.get('source')); + const targetSelected = (this.props.selectedNodeId === edge.get('target')); + return edge.set('focused', this.props.hasSelectedNode && (sourceSelected || targetSelected)); + } + + edgeBlurredDecorator(edge) { + const { selectedNodeId, searchNodeMatches, selectedNetworkNodesIds } = this.props; + const sourceSelected = (selectedNodeId === edge.get('source')); + const targetSelected = (selectedNodeId === edge.get('target')); + const otherNodesSelected = this.props.hasSelectedNode && !sourceSelected && !targetSelected; + const sourceNoMatches = searchNodeMatches.get(edge.get('source'), makeMap()).isEmpty(); + const targetNoMatches = searchNodeMatches.get(edge.get('target'), makeMap()).isEmpty(); + const notMatched = this.props.searchQuery && (sourceNoMatches || targetNoMatches); + const sourceInNetwork = selectedNetworkNodesIds.contains(edge.get('source')); + const targetInNetwork = selectedNetworkNodesIds.contains(edge.get('target')); + const notInNetwork = this.props.selectedNetwork && (!sourceInNetwork || !targetInNetwork); + return edge.set('blurred', !edge.get('highlighted') && !edge.get('focused') && + (otherNodesSelected || notMatched || notInNetwork)); + } + + edgeScaleDecorator(edge) { + return edge.set('scale', edge.get('focused') ? this.props.selectedScale : 1); + } + + renderNode(node) { + const { isAnimated, contrastMode } = this.props; + return ( + + ); + } + + renderEdge(edge) { + const { isAnimated } = this.props; + return ( + + ); + } + + renderElement(element) { + // This heuristics is not ideal but it works. + return element.get('points') ? this.renderEdge(element) : this.renderNode(element); + } + render() { - const { layoutNodes, layoutEdges, selectedScale, isAnimated } = this.props; + const nodes = this.props.layoutNodes.toIndexedSeq() + .map(this.nodeHighlightedDecorator) + .map(this.nodeFocusedDecorator) + .map(this.nodeBlurredDecorator) + .map(this.nodeMatchesDecorator) + .map(this.nodeNetworksDecorator) + .map(this.nodeMetricDecorator) + .map(this.nodeScaleDecorator) + .groupBy(this.nodeDisplayLayer); + + const edges = this.props.layoutEdges.toIndexedSeq() + .map(this.edgeHighlightedDecorator) + .map(this.edgeFocusedDecorator) + .map(this.edgeBlurredDecorator) + .map(this.edgeScaleDecorator) + .groupBy(this.edgeDisplayLayer); + + // NOTE: The elements need to be arranged into a single array outside + // of DOM structure for React rendering engine to do smart rearrangements + // without unnecessary re-rendering of the elements themselves. So e.g. + // rendering the element layers individually below would be significantly slower. + const orderedElements = makeList([ + edges.get(BLURRED_EDGES_LAYER, makeList()), + nodes.get(BLURRED_NODES_LAYER, makeList()), + edges.get(NORMAL_EDGES_LAYER, makeList()), + nodes.get(NORMAL_NODES_LAYER, makeList()), + edges.get(HIGHLIGHTED_EDGES_LAYER, makeList()), + nodes.get(HIGHLIGHTED_NODES_LAYER, makeList()), + edges.get(HOVERED_EDGES_LAYER, makeList()), + nodes.get(HOVERED_NODES_LAYER, makeList()), + ]).flatten(true); return ( - - + {orderedElements.map(this.renderElement)} ); } @@ -33,10 +239,24 @@ class NodesChartElements extends React.Component { function mapStateToProps(state) { return { + hasSelectedNode: hasSelectedNodeFn(state), layoutNodes: layoutNodesSelector(state), layoutEdges: layoutEdgesSelector(state), - selectedScale: selectedScaleSelector(state), isAnimated: !graphExceedsComplexityThreshSelector(state), + highlightedNodeIds: highlightedNodeIdsSelector(state), + highlightedEdgeIds: highlightedEdgeIdsSelector(state), + selectedNetworkNodesIds: selectedNetworkNodesIdsSelector(state), + searchNodeMatches: searchNodeMatchesSelector(state), + neighborsOfSelectedNode: getAdjacentNodes(state), + nodeNetworks: nodeNetworksSelector(state), + nodeMetric: nodeMetricSelector(state), + selectedScale: selectedScaleSelector(state), + searchQuery: state.get('searchQuery'), + selectedNetwork: state.get('selectedNetwork'), + selectedNodeId: state.get('selectedNodeId'), + mouseOverNodeId: state.get('mouseOverNodeId'), + mouseOverEdgeId: state.get('mouseOverEdgeId'), + contrastMode: state.get('contrastMode'), }; } diff --git a/client/app/scripts/charts/nodes-chart-nodes.js b/client/app/scripts/charts/nodes-chart-nodes.js deleted file mode 100644 index a057c64a12..0000000000 --- a/client/app/scripts/charts/nodes-chart-nodes.js +++ /dev/null @@ -1,141 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import { Map as makeMap } from 'immutable'; - -import { nodeMetricSelector } from '../selectors/node-metric'; -import { searchNodeMatchesSelector } from '../selectors/search'; -import { highlightedNodeIdsSelector } from '../selectors/graph-view/decorators'; -import { nodeNetworksSelector, selectedNetworkNodesIdsSelector } from '../selectors/node-networks'; -import { getAdjacentNodes } from '../utils/topology-utils'; -import NodeContainer from './node-container'; - -class NodesChartNodes extends React.Component { - constructor(props, context) { - super(props, context); - - this.nodeDisplayLayer = this.nodeDisplayLayer.bind(this); - // Node decorators - // TODO: Consider moving some of these one level up (or even to global selectors) so that - // other components, like NodesChartEdges, could read more info directly from the nodes. - this.nodeHighlightedDecorator = this.nodeHighlightedDecorator.bind(this); - this.nodeFocusedDecorator = this.nodeFocusedDecorator.bind(this); - this.nodeBlurredDecorator = this.nodeBlurredDecorator.bind(this); - this.nodeMatchesDecorator = this.nodeMatchesDecorator.bind(this); - this.nodeNetworksDecorator = this.nodeNetworksDecorator.bind(this); - this.nodeMetricDecorator = this.nodeMetricDecorator.bind(this); - this.nodeScaleDecorator = this.nodeScaleDecorator.bind(this); - } - - nodeHighlightedDecorator(node) { - const nodeSelected = (this.props.selectedNodeId === node.get('id')); - const nodeHighlighted = this.props.highlightedNodeIds.has(node.get('id')); - return node.set('highlighted', nodeHighlighted || nodeSelected); - } - - nodeFocusedDecorator(node) { - const nodeSelected = (this.props.selectedNodeId === node.get('id')); - const isNeighborOfSelected = this.props.neighborsOfSelectedNode.includes(node.get('id')); - return node.set('focused', nodeSelected || isNeighborOfSelected); - } - - nodeBlurredDecorator(node) { - const belongsToNetwork = this.props.selectedNetworkNodesIds.contains(node.get('id')); - const noMatches = this.props.searchNodeMatches.get(node.get('id'), makeMap()).isEmpty(); - const notMatched = (this.props.searchQuery && !node.get('highlighted') && noMatches); - const notFocused = (this.props.selectedNodeId && !node.get('focused')); - const notInNetwork = (this.props.selectedNetwork && !belongsToNetwork); - return node.set('blurred', notMatched || notFocused || notInNetwork); - } - - nodeMatchesDecorator(node) { - return node.set('matches', this.props.searchNodeMatches.get(node.get('id'))); - } - - nodeNetworksDecorator(node) { - return node.set('networks', this.props.nodeNetworks.get(node.get('id'))); - } - - nodeMetricDecorator(node) { - return node.set('metric', this.props.nodeMetric.get(node.get('id'))); - } - - nodeScaleDecorator(node) { - return node.set('scale', node.get('focused') ? this.props.selectedScale : 1); - } - - // make sure blurred nodes are in the background - nodeDisplayLayer(node) { - if (node.get('id') === this.props.mouseOverNodeId) { - return 3; - } - if (node.get('blurred') && !node.get('focused')) { - return 0; - } - if (node.get('highlighted')) { - return 2; - } - return 1; - } - - render() { - const { layoutNodes, isAnimated, contrastMode } = this.props; - - const nodesToRender = layoutNodes.toIndexedSeq() - .map(this.nodeHighlightedDecorator) - .map(this.nodeFocusedDecorator) - .map(this.nodeBlurredDecorator) - .map(this.nodeMatchesDecorator) - .map(this.nodeNetworksDecorator) - .map(this.nodeMetricDecorator) - .map(this.nodeScaleDecorator) - .sortBy(this.nodeDisplayLayer); - - return ( - - {nodesToRender.map(node => ( - - ))} - - ); - } -} - -function mapStateToProps(state) { - return { - nodeMetric: nodeMetricSelector(state), - nodeNetworks: nodeNetworksSelector(state), - searchNodeMatches: searchNodeMatchesSelector(state), - selectedNetworkNodesIds: selectedNetworkNodesIdsSelector(state), - neighborsOfSelectedNode: getAdjacentNodes(state), - highlightedNodeIds: highlightedNodeIdsSelector(state), - mouseOverNodeId: state.get('mouseOverNodeId'), - selectedNetwork: state.get('selectedNetwork'), - selectedNodeId: state.get('selectedNodeId'), - searchQuery: state.get('searchQuery'), - contrastMode: state.get('contrastMode') - }; -} - -export default connect( - mapStateToProps, -)(NodesChartNodes); diff --git a/client/app/scripts/constants/naming.js b/client/app/scripts/constants/naming.js index 3b06a61783..58688d5c40 100644 --- a/client/app/scripts/constants/naming.js +++ b/client/app/scripts/constants/naming.js @@ -6,3 +6,13 @@ export const EDGE_ID_SEPARATOR = '---'; export const GRAPH_VIEW_MODE = 'topo'; export const TABLE_VIEW_MODE = 'grid'; export const RESOURCE_VIEW_MODE = 'resource'; + +// Named constants to avoid typos that would result in hard-to-detect bugs. +export const BLURRED_EDGES_LAYER = 'blurred-edges'; +export const BLURRED_NODES_LAYER = 'blurred-nodes'; +export const NORMAL_EDGES_LAYER = 'normal-edges'; +export const NORMAL_NODES_LAYER = 'normal-nodes'; +export const HIGHLIGHTED_EDGES_LAYER = 'highlighted-edges'; +export const HIGHLIGHTED_NODES_LAYER = 'highlighted-nodes'; +export const HOVERED_EDGES_LAYER = 'hovered-edges'; +export const HOVERED_NODES_LAYER = 'hovered-nodes'; diff --git a/client/app/styles/_base.scss b/client/app/styles/_base.scss index dc304bdb18..2344836089 100644 --- a/client/app/styles/_base.scss +++ b/client/app/styles/_base.scss @@ -399,7 +399,7 @@ fill: $text-secondary-color; } - .nodes-chart-nodes .node { + .nodes-chart-elements .node { transition: opacity .2s $base-ease; text-align: center; @@ -500,10 +500,6 @@ opacity: $edge-opacity-blurred; } - &.focused { - animation: focusing 1.5s ease-in-out; - } - .link { fill: none; stroke: $edge-color; @@ -1662,16 +1658,6 @@ } } -@keyframes focusing { - 0% { - opacity: 0; - } 33% { - opacity: 0.2; - } 100% { - opacity: 1; - } -} - @keyframes blinking { 0%, 50%, 100% { opacity: 1.0; diff --git a/client/test/actions/90-nodes-select.js b/client/test/actions/90-nodes-select.js index 5693ae01bb..ff667010fa 100644 --- a/client/test/actions/90-nodes-select.js +++ b/client/test/actions/90-nodes-select.js @@ -21,7 +21,7 @@ function clickIfVisible(list, index) { function selectNode(browser) { debug('select node'); - return browser.elementByCssSelector('.nodes-chart-nodes .node:nth-child(1) > g', function(err, el) { + return browser.elementByCssSelector('.nodes-chart-elements .node:nth-child(1) > g', function(err, el) { return el.click(); }); }