Newton Graph Library

Newton Graph Library

source

graph/views/nodes.js

const d3 = require('d3')
const View = require('./view')

/**
 * @typedef {Object} Node
 *
 * A node is an `Object` with node properties e.g. name, status, etc. These attributes are user defined.
 * The standard and minimum attributes include:
 *
 * ```javascript
 * let node = {
 * 	status: 'up',				// used for styling
 * 	id: 'web-frontend',			// identifier used in in links
 * 	label: 'Web Frontend'		// text label
 * }
 * ```
 * Note that `id`s must be unique and are used in referencing {@link Link}s.
 *
 * @property {String} id - Identifier referenced in {@link Link}s.
 * @property {String} label - Text label for the Node.
 * @property {String} status - This can be `up`, `down` or `deploying` and is used for styling.
 */

/**
 * Encapsulates what is needed to create the nodes of a network graph,
 * namely rendering, positioning and animation of nodes based on data.
 *
 * @extends View
 */
class Nodes extends View {
	/**
	 * @param {Object} options
	 * @param {String} [options.dom = window.document] - DOM reference, required for testing
	 * @param {String} [options.container] - HTML identifier used by for d3
	 */
	constructor (options = {}) {
		super(options)
	}

	/**
	 * Renders network graphs and passes hooks to d3.js's general update pattern via events.
	 * For more information on the general update pattern, see:
	 * [https://bost.ocks.org/mike/join/](https://bost.ocks.org/mike/join/)
	 *
	 * @param {Object} data - Network data with `nodes` attribute
	 */
	render (data) {
		// console.log(' nodes render()')
		if (data.nodes === undefined) { throw 'Error: missing `nodes` attribute on parameter.' }

		let nodes = d3.select(this.dom)
			.select(this.container)
			.selectAll('.node')
			.data(data.nodes, (d) => d.id)

		/**
		 * An `Array` of exiting Nodes, i.e. nodes to be removed per d3.js general update pattern.
		 *
		 * @event Nodes#exit
		 * @type {Array}
		 */
		this.emit('exit', nodes.exit())
		nodes.exit()
			.remove()

		/**
		 * An `Array` of enter Nodes, i.e. new nodes per d3.js general update pattern.
		 *
		 * @event Nodes#enter
		 * @type {Array}
		 */
		this.emit('enter', nodes.enter())
		nodes = nodes.enter()
			.append('circle')
				.attr('id', (n) => 'node-' + n.id)
			.merge(nodes)
				.attr('data-title', (n) => n.label)
				.attr('class', (n) => 'node status-' + n.status)
				.attr('r', (n) => n.status === 'up' ? 6 : 10)
				.on('mouseover', (n) => this.onMouseover(n))
				.on('mouseout', (n) => this.onMouseout(n))
				.on('click', (n) => this.onClick(n))

		/**
		 * An `Array` of combined existing and new Nodes, per d3.js general update pattern.
		 *
		 * @event Nodes#update
		 * @type {Array}
		 */
		this.emit('update', nodes)
		this.selection = nodes
	}

	/**
	 * Positions node `<circle>` at center
	 */
	position () {
		this.selection
			.attr('cx', (d) => d.x)
			.attr('cy', (d) => d.y)
	}

	/**
	 * Event handler for browser `mouseover` event. Handler is defined in [render()]{@link Nodes#render}.
	 *
	 * @param {Object} n - Node
	 * @emits Nodes#mouseover
	 */
	onMouseover (n) {
		/**
		 * Note: this is a custom event and _not_ a browser event.
		 *
		 * @event Nodes#mouseover
		 * @type {Node}
		 * @example
		 * nodes.on('mouseover', (node) => {
			* 	console.log('User hovered on node ' + node.name)
			* })
		 */
		this.emit('mouseover', n)
	}

	/**
	 * Event handler for browser `mouseout` event. Handler is defined in [render()]{@link Nodes#render}.
	 * @param {Object} n - Node
	 */
	onMouseout (n) {

		/**
		 * Note: this is a custom event and _not_ a browser event.
		 *
		 * @event Nodes#mouseout
		 * @type {Node}
		 * @example
		 * nodes.on('mouseout', (node) => {
			* 	console.log('mouse left node ' + node.name)
			* })
		 */
		this.emit('mouseout', n)
	}

	/**
	 * Event handler for browser `click` event. Handler is defined in [render()]{@link Nodes#render}.
	 * @param {Node} n - clicked on node
	 */
	onClick (n) {

		/**
		 * Note: this is a custom event and _not_ a browser event.
		 *
		 * @event Nodes#click
		 * @type {Node}
		 * @example
		 * nodes.on('click', (node) => {
		 * 	console.log('User clicked on node ' + node.name)
		 * })
		 */
		this.emit('click', n)
	}
}

module.exports = Nodes