1+ /**
2+ * @module Graph/helper
3+ * @description
4+ * Offers a series of methods that isolate logic of Graph component and also from Graph rendering methods.
5+ */
6+ /**
7+ * @typedef {Object } Link
8+ * @property {string } source - the node id of the source in the link.
9+ * @property {string } target - the node id of the target in the link.
10+ * @memberof Graph/helper
11+ */
12+ /**
13+ * @typedef {Object } Node
14+ * @property {string } id - the id of the node.
15+ * @property {string } [color] - color of the node (optional).
16+ * @property {string } [size] - size of the node (optional).
17+ * @property {string } [symbolType] - symbol type of the node (optional).
18+ * @memberof Graph/helper
19+ */
20+ import {
21+ forceX as d3ForceX ,
22+ forceY as d3ForceY ,
23+ forceSimulation as d3ForceSimulation ,
24+ forceManyBody as d3ForceManyBody
25+ } from 'd3-force' ;
26+
27+ import CONST from './const' ;
28+ import DEFAULT_CONFIG from './config' ;
29+ import ERRORS from '../../err' ;
30+
31+ import utils from '../../utils' ;
32+
33+ /**
34+ * Create d3 forceSimulation to be applied on the graph.<br/>
35+ * {@link https://github.com/d3/d3-force#forceSimulation|d3-force#forceSimulation}<br/>
36+ * {@link https://github.com/d3/d3-force#simulation_force|d3-force#simulation_force}<br/>
37+ * Wtf is a force? {@link https://github.com/d3/d3-force#forces| here}
38+ * @param {number } width - the width of the container area of the graph.
39+ * @param {number } height - the height of the container area of the graph.
40+ * @returns {Object } returns the simulation instance to be consumed.
41+ * @memberof Graph/helper
42+ */
43+ function _createForceSimulation ( width , height ) {
44+ const frx = d3ForceX ( width / 2 ) . strength ( CONST . FORCE_X ) ;
45+ const fry = d3ForceY ( height / 2 ) . strength ( CONST . FORCE_Y ) ;
46+
47+ return d3ForceSimulation ( )
48+ . force ( 'charge' , d3ForceManyBody ( ) . strength ( CONST . FORCE_IDEAL_STRENGTH ) )
49+ . force ( 'x' , frx )
50+ . force ( 'y' , fry ) ;
51+ }
52+
53+ /**
54+ * Get the correct node opacity in order to properly make decisions based on context such as currently highlighted node.
55+ * @param {Object } node - the node object for whom we will generate properties.
56+ * @param {string } highlightedNode - same as {@link #buildGraph|highlightedNode in buildGraph}.
57+ * @param {Object } highlightedLink - same as {@link #buildGraph|highlightedLink in buildGraph}.
58+ * @param {Object } config - same as {@link #buildGraph|config in buildGraph}.
59+ * @returns {number } the opacity value for the given node.
60+ * @memberof Graph/helper
61+ */
62+ function _getNodeOpacity ( node , highlightedNode , highlightedLink , config ) {
63+ const highlight = node . highlighted
64+ || node . id === ( highlightedLink && highlightedLink . source )
65+ || node . id === ( highlightedLink && highlightedLink . target ) ;
66+ const someNodeHighlighted = ! ! ( highlightedNode
67+ || highlightedLink && highlightedLink . source && highlightedLink . target ) ;
68+ let opacity ;
69+
70+ if ( someNodeHighlighted && config . highlightDegree === 0 ) {
71+ opacity = highlight ? config . node . opacity : config . highlightOpacity ;
72+ } else if ( someNodeHighlighted ) {
73+ opacity = highlight ? config . node . opacity : config . highlightOpacity ;
74+ } else {
75+ opacity = config . node . opacity ;
76+ }
77+
78+ return opacity ;
79+ }
80+
81+ /**
82+ * Receives a matrix of the graph with the links source and target as concrete node instances and it transforms it
83+ * in a lightweight matrix containing only links with source and target being strings representative of some node id
84+ * and the respective link value (if non existent will default to 1).
85+ * @param {Array.<Link> } graphLinks - an array of all graph links.
86+ * @returns {Object.<string, Object> } an object containing a matrix of connections of the graph, for each nodeId,
87+ * there is an object that maps adjacent nodes ids (string) and their values (number).
88+ * @memberof Graph/helper
89+ */
90+ function _initializeLinks ( graphLinks ) {
91+ return graphLinks . reduce ( ( links , l ) => {
92+ const source = l . source . id || l . source ;
93+ const target = l . target . id || l . target ;
94+
95+ if ( ! links [ source ] ) {
96+ links [ source ] = { } ;
97+ }
98+
99+ if ( ! links [ target ] ) {
100+ links [ target ] = { } ;
101+ }
102+
103+ // @TODO : If the graph is directed this should be adapted
104+ links [ source ] [ target ] = links [ target ] [ source ] = l . value || 1 ;
105+
106+ return links ;
107+ } , { } ) ;
108+ }
109+
110+ /**
111+ * Method that initialize graph nodes provided by rd3g consumer and adds additional default mandatory properties
112+ * that are optional for the user. Also it generates an index mapping, this maps nodes ids the their index in the array
113+ * of nodes. This is needed because d3 callbacks such as node click and link click return the index of the node.
114+ * @param {Array.<Node> } graphNodes - the array of nodes provided by the rd3g consumer.
115+ * @returns {Object.<string, Object> } returns the nodes ready to be used within rd3g with additional properties such as x, y
116+ * and highlighted values.
117+ * @memberof Graph/helper
118+ */
119+ function _initializeNodes ( graphNodes ) {
120+ let nodes = { } ;
121+ const n = graphNodes . length ;
122+
123+ for ( let i = 0 ; i < n ; i ++ ) {
124+ const node = graphNodes [ i ] ;
125+
126+ node . highlighted = false ;
127+
128+ if ( ! node . hasOwnProperty ( 'x' ) ) { node [ 'x' ] = 0 ; }
129+ if ( ! node . hasOwnProperty ( 'y' ) ) { node [ 'y' ] = 0 ; }
130+
131+ nodes [ node . id . toString ( ) ] = node ;
132+ }
133+
134+ return nodes ;
135+ }
136+
137+ /**
138+ * Some integrity validations on links and nodes structure. If some validation fails the function will
139+ * throw an error.
140+ * @param {Object } data - Same as {@link #initializeGraphState|data in initializeGraphState}.
141+ * @memberof Graph/helper
142+ * @throws can throw the following error msg:
143+ * INSUFFICIENT_DATA - msg if no nodes are provided
144+ * INVALID_LINKS - if links point to nonexistent nodes
145+ * @returns {undefined }
146+ */
147+ function _validateGraphData ( data ) {
148+ if ( ! data . nodes || ! data . nodes . length ) {
149+ utils . throwErr ( 'Graph' , ERRORS . INSUFFICIENT_DATA ) ;
150+ }
151+
152+ const n = data . links . length ;
153+
154+ for ( let i = 0 ; i < n ; i ++ ) {
155+ const l = data . links [ i ] ;
156+
157+ if ( ! data . nodes . find ( n => n . id === l . source ) ) {
158+ utils . throwErr ( 'Graph' , `${ ERRORS . INVALID_LINKS } - "${ l . source } " is not a valid source node id` ) ;
159+ }
160+ if ( ! data . nodes . find ( n => n . id === l . target ) ) {
161+ utils . throwErr ( 'Graph' , `${ ERRORS . INVALID_LINKS } - "${ l . target } " is not a valid target node id` ) ;
162+ }
163+ }
164+ }
165+
166+ /**
167+ * Build some Link properties based on given parameters.
168+ * @param {string } source - the id of the source node (from).
169+ * @param {string } target - the id of the target node (to).
170+ * @param {Object.<string, Object> } nodes - same as {@link #buildGraph|nodes in buildGraph}.
171+ * @param {Object.<string, Object> } links - same as {@link #buildGraph|links in buildGraph}.
172+ * @param {Object } config - same as {@link #buildGraph|config in buildGraph}.
173+ * @param {Function[] } linkCallbacks - same as {@link #buildGraph|linkCallbacks in buildGraph}.
174+ * @param {string } highlightedNode - same as {@link #buildGraph|highlightedNode in buildGraph}.
175+ * @param {Object } highlightedLink - same as {@link #buildGraph|highlightedLink in buildGraph}.
176+ * @param {number } transform - value that indicates the amount of zoom transformation.
177+ * @returns {Object } returns an object that aggregates all props for creating respective Link component instance.
178+ * @memberof Graph/helper
179+ */
180+ function buildLinkProps ( source , target , nodes , links , config , linkCallbacks , highlightedNode ,
181+ highlightedLink , transform ) {
182+ const x1 = nodes [ source ] && nodes [ source ] . x || 0 ;
183+ const y1 = nodes [ source ] && nodes [ source ] . y || 0 ;
184+ const x2 = nodes [ target ] && nodes [ target ] . x || 0 ;
185+ const y2 = nodes [ target ] && nodes [ target ] . y || 0 ;
186+
187+ let mainNodeParticipates = false ;
188+
189+ switch ( config . highlightDegree ) {
190+ case 0 :
191+ break ;
192+ case 2 :
193+ mainNodeParticipates = true ;
194+ break ;
195+ default : // 1st degree is the fallback behavior
196+ mainNodeParticipates = source === highlightedNode || target === highlightedNode ;
197+ break ;
198+ }
199+
200+ const reasonNode = mainNodeParticipates && nodes [ source ] . highlighted && nodes [ target ] . highlighted ;
201+ const reasonLink = source === ( highlightedLink && highlightedLink . source )
202+ && target === ( highlightedLink && highlightedLink . target ) ;
203+ const highlight = reasonNode || reasonLink ;
204+
205+ let opacity = config . link . opacity ;
206+
207+ if ( highlightedNode || ( highlightedLink && highlightedLink . source ) ) {
208+ opacity = highlight ? config . link . opacity : config . highlightOpacity ;
209+ }
210+
211+ let stroke = config . link . color ;
212+
213+ if ( highlight ) {
214+ stroke = config . link . highlightColor === CONST . KEYWORDS . SAME ? config . link . color
215+ : config . link . highlightColor ;
216+ }
217+
218+ let strokeWidth = config . link . strokeWidth * ( 1 / transform ) ;
219+
220+ if ( config . link . semanticStrokeWidth ) {
221+ const linkValue = links [ source ] [ target ] || links [ target ] [ source ] || 1 ;
222+
223+ strokeWidth += ( linkValue * strokeWidth ) / 10 ;
224+ }
225+
226+ return {
227+ source,
228+ target,
229+ x1,
230+ y1,
231+ x2,
232+ y2,
233+ strokeWidth,
234+ stroke,
235+ className : CONST . LINK_CLASS_NAME ,
236+ opacity,
237+ onClickLink : linkCallbacks . onClickLink ,
238+ onMouseOverLink : linkCallbacks . onMouseOverLink ,
239+ onMouseOutLink : linkCallbacks . onMouseOutLink
240+ } ;
241+ }
242+
243+ /**
244+ * Build some Node properties based on given parameters.
245+ * @param {Object } node - the node object for whom we will generate properties.
246+ * @param {Object } config - same as {@link #buildGraph|config in buildGraph}.
247+ * @param {Function[] } nodeCallbacks - same as {@link #buildGraph|nodeCallbacks in buildGraph}.
248+ * @param {string } highlightedNode - same as {@link #buildGraph|highlightedNode in buildGraph}.
249+ * @param {Object } highlightedLink - same as {@link #buildGraph|highlightedLink in buildGraph}.
250+ * @param {number } transform - value that indicates the amount of zoom transformation.
251+ * @returns {Object } returns object that contain Link props ready to be feeded to the Link component.
252+ * @memberof Graph/helper
253+ */
254+ function buildNodeProps ( node , config , nodeCallbacks = { } , highlightedNode , highlightedLink , transform ) {
255+ const highlight = node . highlighted
256+ || ( node . id === ( highlightedLink && highlightedLink . source )
257+ || node . id === ( highlightedLink && highlightedLink . target ) ) ;
258+ const opacity = _getNodeOpacity ( node , highlightedNode , highlightedLink , config ) ;
259+ let fill = node . color || config . node . color ;
260+
261+ if ( highlight && config . node . highlightColor !== CONST . KEYWORDS . SAME ) {
262+ fill = config . node . highlightColor ;
263+ }
264+
265+ let stroke = config . node . strokeColor ;
266+
267+ if ( highlight && config . node . highlightStrokeColor !== CONST . KEYWORDS . SAME ) {
268+ stroke = config . node . highlightStrokeColor ;
269+ }
270+
271+ const t = 1 / transform ;
272+ const nodeSize = node . size || config . node . size ;
273+ const fontSize = highlight ? config . node . highlightFontSize : config . node . fontSize ;
274+ const dx = ( fontSize * t ) + ( nodeSize / 100 ) + 1.5 ;
275+ const strokeWidth = highlight ? config . node . highlightStrokeWidth : config . node . strokeWidth ;
276+
277+ return {
278+ className : CONST . NODE_CLASS_NAME ,
279+ cursor : config . node . mouseCursor ,
280+ cx : node && node . x || '0' ,
281+ cy : node && node . y || '0' ,
282+ fill,
283+ fontSize : fontSize * t ,
284+ dx,
285+ fontWeight : highlight ? config . node . highlightFontWeight : config . node . fontWeight ,
286+ id : node . id ,
287+ label : node [ config . node . labelProperty ] || node . id ,
288+ onClickNode : nodeCallbacks . onClickNode ,
289+ onMouseOverNode : nodeCallbacks . onMouseOverNode ,
290+ onMouseOut : nodeCallbacks . onMouseOut ,
291+ opacity,
292+ renderLabel : config . node . renderLabel ,
293+ size : nodeSize * t ,
294+ stroke,
295+ strokeWidth : strokeWidth * t ,
296+ type : node . symbolType || config . node . symbolType
297+ } ;
298+ }
299+
300+ /**
301+ * Encapsulates common procedures to initialize graph.
302+ * @param {Object } props - Graph component props, object that holds data, id and config.
303+ * @param {Object } props.data - Data object holds links (array of **Link**) and nodes (array of **Node**).
304+ * @param {string } props.id - the graph id.
305+ * @param {Object } props.config - same as {@link #buildGraph|config in buildGraph}.
306+ * @param {Object } state - Graph component current state (same format as returned object on this function).
307+ * @returns {Object } a fully (re)initialized graph state object.
308+ * @memberof Graph/helper
309+ */
310+ function initializeGraphState ( { data, id, config} , state ) {
311+ let graph ;
312+
313+ _validateGraphData ( data ) ;
314+
315+ if ( state && state . nodes && state . links ) {
316+ // absorb existent positioning
317+ graph = {
318+ nodes : data . nodes . map ( n => Object . assign ( { } , n , state . nodes [ n . id ] ) ) ,
319+ links : { }
320+ } ;
321+ } else {
322+ graph = {
323+ nodes : data . nodes . map ( n => Object . assign ( { } , n ) ) ,
324+ links : { }
325+ } ;
326+ }
327+
328+ graph . links = data . links . map ( l => Object . assign ( { } , l ) ) ;
329+
330+ let newConfig = Object . assign ( { } , utils . merge ( DEFAULT_CONFIG , config || { } ) ) ;
331+ let nodes = _initializeNodes ( graph . nodes ) ;
332+ let links = _initializeLinks ( graph . links ) ; // matrix of graph connections
333+ const { nodes : d3Nodes , links : d3Links } = graph ;
334+ const formatedId = id . replace ( / / g, '_' ) ;
335+ const simulation = _createForceSimulation ( newConfig . width , newConfig . height ) ;
336+
337+ return {
338+ id : formatedId ,
339+ config : newConfig ,
340+ links,
341+ d3Links,
342+ nodes,
343+ d3Nodes,
344+ highlightedNode : '' ,
345+ simulation,
346+ newGraphElements : false ,
347+ configUpdated : false ,
348+ transform : 1
349+ } ;
350+ }
351+
352+ export {
353+ buildLinkProps ,
354+ buildNodeProps ,
355+ initializeGraphState
356+ } ;
0 commit comments