vis.js is a dynamic, browser-based visualization library
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

982 lines
22 KiB

Add arrow types for EndPoints.js (#3839) * Add arrowhead support As described in TODO, dotparser.js does not support 'arrowhead' attribubte of edge. This update is for adding 'dot' and 'tee'(bar) styles. * Add example for arrow styles * Add box arrowhead To support box arrowhead of graphviz, add Box endpoint class in EndPoints.js and box attribute in dotparser.js. * Add diamond arrowhead To support diamond arrowhead of graphviz, add Diamond endpoint class in EndPoints.js and diamond attribute in dotparser.js. * Add crow arrowhead To support crow arrowhead of graphviz, add Crow endpoint class in EndPoints.js and crow attribute in dotparser.js. * Add normal arrowhead To support normal arrowhead of graphviz, add Triangle endpoint class in EndPoints.js and normal attribute in dotparser.js. * Add curve arrowhead To support curve arrowhead of graphviz, add Curve endpoint class in EndPoints.js and curve attribute in dotparser.js. * Add inverted curve arrowhead To support inverted curve arrowhead of graphviz, add InvertedCurve endpoint class in EndPoints.js and icurve attribute in dotparser.js. * Add vee arrowhead To support vee arrowhead of graphviz, add Vee endpoint class in EndPoints.js and vee attribute in dotparser.js. * Add arrowhead examples * Fix bug for accessing null attribute In createEdge(), accessing 'attr' causes an error if the edge has no attribute and the value is null. This update fixes bug for accessing null 'attr'. * Update description for arrows.to.type option Add followingn options for 'arrows.to.type'. * box * crow * curve * diamond * inv_curve * triangle * inv_triangle * vee * Update edgeStyle example for arrow types Add arrow types for the example. 'box', 'crow', 'curve', 'inv_curve', 'diamond', 'triangle', 'inv_triangle', 'vee'
6 years ago
Add arrow types for EndPoints.js (#3839) * Add arrowhead support As described in TODO, dotparser.js does not support 'arrowhead' attribubte of edge. This update is for adding 'dot' and 'tee'(bar) styles. * Add example for arrow styles * Add box arrowhead To support box arrowhead of graphviz, add Box endpoint class in EndPoints.js and box attribute in dotparser.js. * Add diamond arrowhead To support diamond arrowhead of graphviz, add Diamond endpoint class in EndPoints.js and diamond attribute in dotparser.js. * Add crow arrowhead To support crow arrowhead of graphviz, add Crow endpoint class in EndPoints.js and crow attribute in dotparser.js. * Add normal arrowhead To support normal arrowhead of graphviz, add Triangle endpoint class in EndPoints.js and normal attribute in dotparser.js. * Add curve arrowhead To support curve arrowhead of graphviz, add Curve endpoint class in EndPoints.js and curve attribute in dotparser.js. * Add inverted curve arrowhead To support inverted curve arrowhead of graphviz, add InvertedCurve endpoint class in EndPoints.js and icurve attribute in dotparser.js. * Add vee arrowhead To support vee arrowhead of graphviz, add Vee endpoint class in EndPoints.js and vee attribute in dotparser.js. * Add arrowhead examples * Fix bug for accessing null attribute In createEdge(), accessing 'attr' causes an error if the edge has no attribute and the value is null. This update fixes bug for accessing null 'attr'. * Update description for arrows.to.type option Add followingn options for 'arrows.to.type'. * box * crow * curve * diamond * inv_curve * triangle * inv_triangle * vee * Update edgeStyle example for arrow types Add arrow types for the example. 'box', 'crow', 'curve', 'inv_curve', 'diamond', 'triangle', 'inv_triangle', 'vee'
6 years ago
Add arrow types for EndPoints.js (#3839) * Add arrowhead support As described in TODO, dotparser.js does not support 'arrowhead' attribubte of edge. This update is for adding 'dot' and 'tee'(bar) styles. * Add example for arrow styles * Add box arrowhead To support box arrowhead of graphviz, add Box endpoint class in EndPoints.js and box attribute in dotparser.js. * Add diamond arrowhead To support diamond arrowhead of graphviz, add Diamond endpoint class in EndPoints.js and diamond attribute in dotparser.js. * Add crow arrowhead To support crow arrowhead of graphviz, add Crow endpoint class in EndPoints.js and crow attribute in dotparser.js. * Add normal arrowhead To support normal arrowhead of graphviz, add Triangle endpoint class in EndPoints.js and normal attribute in dotparser.js. * Add curve arrowhead To support curve arrowhead of graphviz, add Curve endpoint class in EndPoints.js and curve attribute in dotparser.js. * Add inverted curve arrowhead To support inverted curve arrowhead of graphviz, add InvertedCurve endpoint class in EndPoints.js and icurve attribute in dotparser.js. * Add vee arrowhead To support vee arrowhead of graphviz, add Vee endpoint class in EndPoints.js and vee attribute in dotparser.js. * Add arrowhead examples * Fix bug for accessing null attribute In createEdge(), accessing 'attr' causes an error if the edge has no attribute and the value is null. This update fixes bug for accessing null 'attr'. * Update description for arrows.to.type option Add followingn options for 'arrows.to.type'. * box * crow * curve * diamond * inv_curve * triangle * inv_triangle * vee * Update edgeStyle example for arrow types Add arrow types for the example. 'box', 'crow', 'curve', 'inv_curve', 'diamond', 'triangle', 'inv_triangle', 'vee'
6 years ago
Add arrow types for EndPoints.js (#3839) * Add arrowhead support As described in TODO, dotparser.js does not support 'arrowhead' attribubte of edge. This update is for adding 'dot' and 'tee'(bar) styles. * Add example for arrow styles * Add box arrowhead To support box arrowhead of graphviz, add Box endpoint class in EndPoints.js and box attribute in dotparser.js. * Add diamond arrowhead To support diamond arrowhead of graphviz, add Diamond endpoint class in EndPoints.js and diamond attribute in dotparser.js. * Add crow arrowhead To support crow arrowhead of graphviz, add Crow endpoint class in EndPoints.js and crow attribute in dotparser.js. * Add normal arrowhead To support normal arrowhead of graphviz, add Triangle endpoint class in EndPoints.js and normal attribute in dotparser.js. * Add curve arrowhead To support curve arrowhead of graphviz, add Curve endpoint class in EndPoints.js and curve attribute in dotparser.js. * Add inverted curve arrowhead To support inverted curve arrowhead of graphviz, add InvertedCurve endpoint class in EndPoints.js and icurve attribute in dotparser.js. * Add vee arrowhead To support vee arrowhead of graphviz, add Vee endpoint class in EndPoints.js and vee attribute in dotparser.js. * Add arrowhead examples * Fix bug for accessing null attribute In createEdge(), accessing 'attr' causes an error if the edge has no attribute and the value is null. This update fixes bug for accessing null 'attr'. * Update description for arrows.to.type option Add followingn options for 'arrows.to.type'. * box * crow * curve * diamond * inv_curve * triangle * inv_triangle * vee * Update edgeStyle example for arrow types Add arrow types for the example. 'box', 'crow', 'curve', 'inv_curve', 'diamond', 'triangle', 'inv_triangle', 'vee'
6 years ago
  1. /**
  2. * Parse a text source containing data in DOT language into a JSON object.
  3. * The object contains two lists: one with nodes and one with edges.
  4. *
  5. * DOT language reference: http://www.graphviz.org/doc/info/lang.html
  6. *
  7. * DOT language attributes: http://graphviz.org/content/attrs
  8. *
  9. * @param {string} data Text containing a graph in DOT-notation
  10. * @return {Object} graph An object containing two parameters:
  11. * {Object[]} nodes
  12. * {Object[]} edges
  13. *
  14. * -------------------------------------------
  15. * TODO
  16. * ====
  17. *
  18. * For label handling, this is an incomplete implementation. From docs (quote #3015):
  19. *
  20. * > the escape sequences "\n", "\l" and "\r" divide the label into lines, centered,
  21. * > left-justified, and right-justified, respectively.
  22. *
  23. * Source: http://www.graphviz.org/content/attrs#kescString
  24. *
  25. * > As another aid for readability, dot allows double-quoted strings to span multiple physical
  26. * > lines using the standard C convention of a backslash immediately preceding a newline
  27. * > character
  28. * > In addition, double-quoted strings can be concatenated using a '+' operator.
  29. * > As HTML strings can contain newline characters, which are used solely for formatting,
  30. * > the language does not allow escaped newlines or concatenation operators to be used
  31. * > within them.
  32. *
  33. * - Currently, only '\\n' is handled
  34. * - Note that text explicitly says 'labels'; the dot parser currently handles escape
  35. * sequences in **all** strings.
  36. */
  37. function parseDOT (data) {
  38. dot = data;
  39. return parseGraph();
  40. }
  41. // mapping of attributes from DOT (the keys) to vis.js (the values)
  42. var NODE_ATTR_MAPPING = {
  43. 'fontsize': 'font.size',
  44. 'fontcolor': 'font.color',
  45. 'labelfontcolor': 'font.color',
  46. 'fontname': 'font.face',
  47. 'color': ['color.border', 'color.background'],
  48. 'fillcolor': 'color.background',
  49. 'tooltip': 'title',
  50. 'labeltooltip': 'title'
  51. };
  52. var EDGE_ATTR_MAPPING = Object.create(NODE_ATTR_MAPPING);
  53. EDGE_ATTR_MAPPING.color = 'color.color';
  54. EDGE_ATTR_MAPPING.style = 'dashes';
  55. // token types enumeration
  56. var TOKENTYPE = {
  57. NULL : 0,
  58. DELIMITER : 1,
  59. IDENTIFIER: 2,
  60. UNKNOWN : 3
  61. };
  62. // map with all delimiters
  63. var DELIMITERS = {
  64. '{': true,
  65. '}': true,
  66. '[': true,
  67. ']': true,
  68. ';': true,
  69. '=': true,
  70. ',': true,
  71. '->': true,
  72. '--': true
  73. };
  74. var dot = ''; // current dot file
  75. var index = 0; // current index in dot file
  76. var c = ''; // current token character in expr
  77. var token = ''; // current token
  78. var tokenType = TOKENTYPE.NULL; // type of the token
  79. /**
  80. * Get the first character from the dot file.
  81. * The character is stored into the char c. If the end of the dot file is
  82. * reached, the function puts an empty string in c.
  83. */
  84. function first() {
  85. index = 0;
  86. c = dot.charAt(0);
  87. }
  88. /**
  89. * Get the next character from the dot file.
  90. * The character is stored into the char c. If the end of the dot file is
  91. * reached, the function puts an empty string in c.
  92. */
  93. function next() {
  94. index++;
  95. c = dot.charAt(index);
  96. }
  97. /**
  98. * Preview the next character from the dot file.
  99. * @return {string} cNext
  100. */
  101. function nextPreview() {
  102. return dot.charAt(index + 1);
  103. }
  104. var regexAlphaNumeric = /[a-zA-Z_0-9.:#]/;
  105. /**
  106. * Test whether given character is alphabetic or numeric
  107. * @param {string} c
  108. * @return {Boolean} isAlphaNumeric
  109. */
  110. function isAlphaNumeric(c) {
  111. return regexAlphaNumeric.test(c);
  112. }
  113. /**
  114. * Merge all options of object b into object b
  115. * @param {Object} a
  116. * @param {Object} b
  117. * @return {Object} a
  118. */
  119. function merge (a, b) {
  120. if (!a) {
  121. a = {};
  122. }
  123. if (b) {
  124. for (var name in b) {
  125. if (b.hasOwnProperty(name)) {
  126. a[name] = b[name];
  127. }
  128. }
  129. }
  130. return a;
  131. }
  132. /**
  133. * Set a value in an object, where the provided parameter name can be a
  134. * path with nested parameters. For example:
  135. *
  136. * var obj = {a: 2};
  137. * setValue(obj, 'b.c', 3); // obj = {a: 2, b: {c: 3}}
  138. *
  139. * @param {Object} obj
  140. * @param {string} path A parameter name or dot-separated parameter path,
  141. * like "color.highlight.border".
  142. * @param {*} value
  143. */
  144. function setValue(obj, path, value) {
  145. var keys = path.split('.');
  146. var o = obj;
  147. while (keys.length) {
  148. var key = keys.shift();
  149. if (keys.length) {
  150. // this isn't the end point
  151. if (!o[key]) {
  152. o[key] = {};
  153. }
  154. o = o[key];
  155. }
  156. else {
  157. // this is the end point
  158. o[key] = value;
  159. }
  160. }
  161. }
  162. /**
  163. * Add a node to a graph object. If there is already a node with
  164. * the same id, their attributes will be merged.
  165. * @param {Object} graph
  166. * @param {Object} node
  167. */
  168. function addNode(graph, node) {
  169. var i, len;
  170. var current = null;
  171. // find root graph (in case of subgraph)
  172. var graphs = [graph]; // list with all graphs from current graph to root graph
  173. var root = graph;
  174. while (root.parent) {
  175. graphs.push(root.parent);
  176. root = root.parent;
  177. }
  178. // find existing node (at root level) by its id
  179. if (root.nodes) {
  180. for (i = 0, len = root.nodes.length; i < len; i++) {
  181. if (node.id === root.nodes[i].id) {
  182. current = root.nodes[i];
  183. break;
  184. }
  185. }
  186. }
  187. if (!current) {
  188. // this is a new node
  189. current = {
  190. id: node.id
  191. };
  192. if (graph.node) {
  193. // clone default attributes
  194. current.attr = merge(current.attr, graph.node);
  195. }
  196. }
  197. // add node to this (sub)graph and all its parent graphs
  198. for (i = graphs.length - 1; i >= 0; i--) {
  199. var g = graphs[i];
  200. if (!g.nodes) {
  201. g.nodes = [];
  202. }
  203. if (g.nodes.indexOf(current) === -1) {
  204. g.nodes.push(current);
  205. }
  206. }
  207. // merge attributes
  208. if (node.attr) {
  209. current.attr = merge(current.attr, node.attr);
  210. }
  211. }
  212. /**
  213. * Add an edge to a graph object
  214. * @param {Object} graph
  215. * @param {Object} edge
  216. */
  217. function addEdge(graph, edge) {
  218. if (!graph.edges) {
  219. graph.edges = [];
  220. }
  221. graph.edges.push(edge);
  222. if (graph.edge) {
  223. var attr = merge({}, graph.edge); // clone default attributes
  224. edge.attr = merge(attr, edge.attr); // merge attributes
  225. }
  226. }
  227. /**
  228. * Create an edge to a graph object
  229. * @param {Object} graph
  230. * @param {string | number | Object} from
  231. * @param {string | number | Object} to
  232. * @param {string} type
  233. * @param {Object | null} attr
  234. * @return {Object} edge
  235. */
  236. function createEdge(graph, from, to, type, attr) {
  237. var edge = {
  238. from: from,
  239. to: to,
  240. type: type
  241. };
  242. if (graph.edge) {
  243. edge.attr = merge({}, graph.edge); // clone default attributes
  244. }
  245. edge.attr = merge(edge.attr || {}, attr); // merge attributes
  246. // Move arrows attribute from attr to edge temporally created in
  247. // parseAttributeList().
  248. if (attr != null) {
  249. if (attr.hasOwnProperty('arrows')) {
  250. edge['arrows'] = {to: {enabled: true, type: attr.arrows.type}};
  251. attr['arrows'] = null;
  252. }
  253. }
  254. return edge;
  255. }
  256. /**
  257. * Get next token in the current dot file.
  258. * The token and token type are available as token and tokenType
  259. */
  260. function getToken() {
  261. tokenType = TOKENTYPE.NULL;
  262. token = '';
  263. // skip over whitespaces
  264. while (c === ' ' || c === '\t' || c === '\n' || c === '\r') { // space, tab, enter
  265. next();
  266. }
  267. do {
  268. var isComment = false;
  269. // skip comment
  270. if (c === '#') {
  271. // find the previous non-space character
  272. var i = index - 1;
  273. while (dot.charAt(i) === ' ' || dot.charAt(i) === '\t') {
  274. i--;
  275. }
  276. if (dot.charAt(i) === '\n' || dot.charAt(i) === '') {
  277. // the # is at the start of a line, this is indeed a line comment
  278. while (c != '' && c != '\n') {
  279. next();
  280. }
  281. isComment = true;
  282. }
  283. }
  284. if (c === '/' && nextPreview() === '/') {
  285. // skip line comment
  286. while (c != '' && c != '\n') {
  287. next();
  288. }
  289. isComment = true;
  290. }
  291. if (c === '/' && nextPreview() === '*') {
  292. // skip block comment
  293. while (c != '') {
  294. if (c === '*' && nextPreview() === '/') {
  295. // end of block comment found. skip these last two characters
  296. next();
  297. next();
  298. break;
  299. }
  300. else {
  301. next();
  302. }
  303. }
  304. isComment = true;
  305. }
  306. // skip over whitespaces
  307. while (c === ' ' || c === '\t' || c === '\n' || c === '\r') { // space, tab, enter
  308. next();
  309. }
  310. }
  311. while (isComment);
  312. // check for end of dot file
  313. if (c === '') {
  314. // token is still empty
  315. tokenType = TOKENTYPE.DELIMITER;
  316. return;
  317. }
  318. // check for delimiters consisting of 2 characters
  319. var c2 = c + nextPreview();
  320. if (DELIMITERS[c2]) {
  321. tokenType = TOKENTYPE.DELIMITER;
  322. token = c2;
  323. next();
  324. next();
  325. return;
  326. }
  327. // check for delimiters consisting of 1 character
  328. if (DELIMITERS[c]) {
  329. tokenType = TOKENTYPE.DELIMITER;
  330. token = c;
  331. next();
  332. return;
  333. }
  334. // check for an identifier (number or string)
  335. // TODO: more precise parsing of numbers/strings (and the port separator ':')
  336. if (isAlphaNumeric(c) || c === '-') {
  337. token += c;
  338. next();
  339. while (isAlphaNumeric(c)) {
  340. token += c;
  341. next();
  342. }
  343. if (token === 'false') {
  344. token = false; // convert to boolean
  345. }
  346. else if (token === 'true') {
  347. token = true; // convert to boolean
  348. }
  349. else if (!isNaN(Number(token))) {
  350. token = Number(token); // convert to number
  351. }
  352. tokenType = TOKENTYPE.IDENTIFIER;
  353. return;
  354. }
  355. // check for a string enclosed by double quotes
  356. if (c === '"') {
  357. next();
  358. while (c != '' && (c != '"' || (c === '"' && nextPreview() === '"'))) {
  359. if (c === '"') { // skip the escape character
  360. token += c;
  361. next();
  362. } else if (c === '\\' && nextPreview() === 'n') { // Honor a newline escape sequence
  363. token += '\n';
  364. next();
  365. } else {
  366. token += c;
  367. }
  368. next();
  369. }
  370. if (c != '"') {
  371. throw newSyntaxError('End of string " expected');
  372. }
  373. next();
  374. tokenType = TOKENTYPE.IDENTIFIER;
  375. return;
  376. }
  377. // something unknown is found, wrong characters, a syntax error
  378. tokenType = TOKENTYPE.UNKNOWN;
  379. while (c != '') {
  380. token += c;
  381. next();
  382. }
  383. throw new SyntaxError('Syntax error in part "' + chop(token, 30) + '"');
  384. }
  385. /**
  386. * Parse a graph.
  387. * @returns {Object} graph
  388. */
  389. function parseGraph() {
  390. var graph = {};
  391. first();
  392. getToken();
  393. // optional strict keyword
  394. if (token === 'strict') {
  395. graph.strict = true;
  396. getToken();
  397. }
  398. // graph or digraph keyword
  399. if (token === 'graph' || token === 'digraph') {
  400. graph.type = token;
  401. getToken();
  402. }
  403. // optional graph id
  404. if (tokenType === TOKENTYPE.IDENTIFIER) {
  405. graph.id = token;
  406. getToken();
  407. }
  408. // open angle bracket
  409. if (token != '{') {
  410. throw newSyntaxError('Angle bracket { expected');
  411. }
  412. getToken();
  413. // statements
  414. parseStatements(graph);
  415. // close angle bracket
  416. if (token != '}') {
  417. throw newSyntaxError('Angle bracket } expected');
  418. }
  419. getToken();
  420. // end of file
  421. if (token !== '') {
  422. throw newSyntaxError('End of file expected');
  423. }
  424. getToken();
  425. // remove temporary default options
  426. delete graph.node;
  427. delete graph.edge;
  428. delete graph.graph;
  429. return graph;
  430. }
  431. /**
  432. * Parse a list with statements.
  433. * @param {Object} graph
  434. */
  435. function parseStatements (graph) {
  436. while (token !== '' && token != '}') {
  437. parseStatement(graph);
  438. if (token === ';') {
  439. getToken();
  440. }
  441. }
  442. }
  443. /**
  444. * Parse a single statement. Can be a an attribute statement, node
  445. * statement, a series of node statements and edge statements, or a
  446. * parameter.
  447. * @param {Object} graph
  448. */
  449. function parseStatement(graph) {
  450. // parse subgraph
  451. var subgraph = parseSubgraph(graph);
  452. if (subgraph) {
  453. // edge statements
  454. parseEdge(graph, subgraph);
  455. return;
  456. }
  457. // parse an attribute statement
  458. var attr = parseAttributeStatement(graph);
  459. if (attr) {
  460. return;
  461. }
  462. // parse node
  463. if (tokenType != TOKENTYPE.IDENTIFIER) {
  464. throw newSyntaxError('Identifier expected');
  465. }
  466. var id = token; // id can be a string or a number
  467. getToken();
  468. if (token === '=') {
  469. // id statement
  470. getToken();
  471. if (tokenType != TOKENTYPE.IDENTIFIER) {
  472. throw newSyntaxError('Identifier expected');
  473. }
  474. graph[id] = token;
  475. getToken();
  476. // TODO: implement comma separated list with "a_list: ID=ID [','] [a_list] "
  477. }
  478. else {
  479. parseNodeStatement(graph, id);
  480. }
  481. }
  482. /**
  483. * Parse a subgraph
  484. * @param {Object} graph parent graph object
  485. * @return {Object | null} subgraph
  486. */
  487. function parseSubgraph (graph) {
  488. var subgraph = null;
  489. // optional subgraph keyword
  490. if (token === 'subgraph') {
  491. subgraph = {};
  492. subgraph.type = 'subgraph';
  493. getToken();
  494. // optional graph id
  495. if (tokenType === TOKENTYPE.IDENTIFIER) {
  496. subgraph.id = token;
  497. getToken();
  498. }
  499. }
  500. // open angle bracket
  501. if (token === '{') {
  502. getToken();
  503. if (!subgraph) {
  504. subgraph = {};
  505. }
  506. subgraph.parent = graph;
  507. subgraph.node = graph.node;
  508. subgraph.edge = graph.edge;
  509. subgraph.graph = graph.graph;
  510. // statements
  511. parseStatements(subgraph);
  512. // close angle bracket
  513. if (token != '}') {
  514. throw newSyntaxError('Angle bracket } expected');
  515. }
  516. getToken();
  517. // remove temporary default options
  518. delete subgraph.node;
  519. delete subgraph.edge;
  520. delete subgraph.graph;
  521. delete subgraph.parent;
  522. // register at the parent graph
  523. if (!graph.subgraphs) {
  524. graph.subgraphs = [];
  525. }
  526. graph.subgraphs.push(subgraph);
  527. }
  528. return subgraph;
  529. }
  530. /**
  531. * parse an attribute statement like "node [shape=circle fontSize=16]".
  532. * Available keywords are 'node', 'edge', 'graph'.
  533. * The previous list with default attributes will be replaced
  534. * @param {Object} graph
  535. * @returns {String | null} keyword Returns the name of the parsed attribute
  536. * (node, edge, graph), or null if nothing
  537. * is parsed.
  538. */
  539. function parseAttributeStatement (graph) {
  540. // attribute statements
  541. if (token === 'node') {
  542. getToken();
  543. // node attributes
  544. graph.node = parseAttributeList();
  545. return 'node';
  546. }
  547. else if (token === 'edge') {
  548. getToken();
  549. // edge attributes
  550. graph.edge = parseAttributeList();
  551. return 'edge';
  552. }
  553. else if (token === 'graph') {
  554. getToken();
  555. // graph attributes
  556. graph.graph = parseAttributeList();
  557. return 'graph';
  558. }
  559. return null;
  560. }
  561. /**
  562. * parse a node statement
  563. * @param {Object} graph
  564. * @param {string | number} id
  565. */
  566. function parseNodeStatement(graph, id) {
  567. // node statement
  568. var node = {
  569. id: id
  570. };
  571. var attr = parseAttributeList();
  572. if (attr) {
  573. node.attr = attr;
  574. }
  575. addNode(graph, node);
  576. // edge statements
  577. parseEdge(graph, id);
  578. }
  579. /**
  580. * Parse an edge or a series of edges
  581. * @param {Object} graph
  582. * @param {string | number} from Id of the from node
  583. */
  584. function parseEdge(graph, from) {
  585. while (token === '->' || token === '--') {
  586. var to;
  587. var type = token;
  588. getToken();
  589. var subgraph = parseSubgraph(graph);
  590. if (subgraph) {
  591. to = subgraph;
  592. }
  593. else {
  594. if (tokenType != TOKENTYPE.IDENTIFIER) {
  595. throw newSyntaxError('Identifier or subgraph expected');
  596. }
  597. to = token;
  598. addNode(graph, {
  599. id: to
  600. });
  601. getToken();
  602. }
  603. // parse edge attributes
  604. var attr = parseAttributeList();
  605. // create edge
  606. var edge = createEdge(graph, from, to, type, attr);
  607. addEdge(graph, edge);
  608. from = to;
  609. }
  610. }
  611. /**
  612. * Parse a set with attributes,
  613. * for example [label="1.000", shape=solid]
  614. * @return {Object | null} attr
  615. */
  616. function parseAttributeList() {
  617. var attr = null;
  618. // edge styles of dot and vis
  619. var edgeStyles = {
  620. 'dashed': true,
  621. 'solid': false,
  622. 'dotted': [1, 5]
  623. };
  624. while (token === '[') {
  625. getToken();
  626. attr = {};
  627. while (token !== '' && token != ']') {
  628. if (tokenType != TOKENTYPE.IDENTIFIER) {
  629. throw newSyntaxError('Attribute name expected');
  630. }
  631. var name = token;
  632. getToken();
  633. if (token != '=') {
  634. throw newSyntaxError('Equal sign = expected');
  635. }
  636. getToken();
  637. if (tokenType != TOKENTYPE.IDENTIFIER) {
  638. throw newSyntaxError('Attribute value expected');
  639. }
  640. var value = token;
  641. // convert from dot style to vis
  642. if (name === 'style') {
  643. value = edgeStyles[value];
  644. }
  645. // Define arrow types.
  646. // vis.js currently supports types defined in 'arrowTypes'.
  647. // Details of arrow shapes are described in
  648. // http://www.graphviz.org/content/arrow-shapes
  649. var arrowTypes = {
  650. dot: 'circle',
  651. box: 'box',
  652. crow: 'crow',
  653. curve: 'curve',
  654. icurve: 'inv_curve',
  655. normal: 'triangle',
  656. inv: 'inv_triangle',
  657. diamond: 'diamond',
  658. tee: 'bar',
  659. vee: 'vee'
  660. };
  661. if (name === 'arrowhead') {
  662. var arrowType = arrowTypes[value];
  663. name = 'arrows';
  664. value = {to: {enabled:true, type: arrowType}};
  665. }
  666. setValue(attr, name, value); // name can be a path
  667. getToken();
  668. if (token ==',') {
  669. getToken();
  670. }
  671. }
  672. if (token != ']') {
  673. throw newSyntaxError('Bracket ] expected');
  674. }
  675. getToken();
  676. }
  677. return attr;
  678. }
  679. /**
  680. * Create a syntax error with extra information on current token and index.
  681. * @param {string} message
  682. * @returns {SyntaxError} err
  683. */
  684. function newSyntaxError(message) {
  685. return new SyntaxError(message + ', got "' + chop(token, 30) + '" (char ' + index + ')');
  686. }
  687. /**
  688. * Chop off text after a maximum length
  689. * @param {string} text
  690. * @param {number} maxLength
  691. * @returns {String}
  692. */
  693. function chop (text, maxLength) {
  694. return (text.length <= maxLength) ? text : (text.substr(0, 27) + '...');
  695. }
  696. /**
  697. * Execute a function fn for each pair of elements in two arrays
  698. * @param {Array | *} array1
  699. * @param {Array | *} array2
  700. * @param {function} fn
  701. */
  702. function forEach2(array1, array2, fn) {
  703. if (Array.isArray(array1)) {
  704. array1.forEach(function (elem1) {
  705. if (Array.isArray(array2)) {
  706. array2.forEach(function (elem2) {
  707. fn(elem1, elem2);
  708. });
  709. }
  710. else {
  711. fn(elem1, array2);
  712. }
  713. });
  714. }
  715. else {
  716. if (Array.isArray(array2)) {
  717. array2.forEach(function (elem2) {
  718. fn(array1, elem2);
  719. });
  720. }
  721. else {
  722. fn(array1, array2);
  723. }
  724. }
  725. }
  726. /**
  727. * Set a nested property on an object
  728. * When nested objects are missing, they will be created.
  729. * For example setProp({}, 'font.color', 'red') will return {font: {color: 'red'}}
  730. * @param {Object} object
  731. * @param {string} path A dot separated string like 'font.color'
  732. * @param {*} value Value for the property
  733. * @return {Object} Returns the original object, allows for chaining.
  734. */
  735. function setProp(object, path, value) {
  736. var names = path.split('.');
  737. var prop = names.pop();
  738. // traverse over the nested objects
  739. var obj = object;
  740. for (var i = 0; i < names.length; i++) {
  741. var name = names[i];
  742. if (!(name in obj)) {
  743. obj[name] = {};
  744. }
  745. obj = obj[name];
  746. }
  747. // set the property value
  748. obj[prop] = value;
  749. return object;
  750. }
  751. /**
  752. * Convert an object with DOT attributes to their vis.js equivalents.
  753. * @param {Object} attr Object with DOT attributes
  754. * @param {Object} mapping
  755. * @return {Object} Returns an object with vis.js attributes
  756. */
  757. function convertAttr (attr, mapping) {
  758. var converted = {};
  759. for (var prop in attr) {
  760. if (attr.hasOwnProperty(prop)) {
  761. var visProp = mapping[prop];
  762. if (Array.isArray(visProp)) {
  763. visProp.forEach(function (visPropI) {
  764. setProp(converted, visPropI, attr[prop]);
  765. })
  766. }
  767. else if (typeof visProp === 'string') {
  768. setProp(converted, visProp, attr[prop]);
  769. }
  770. else {
  771. setProp(converted, prop, attr[prop]);
  772. }
  773. }
  774. }
  775. return converted;
  776. }
  777. /**
  778. * Convert a string containing a graph in DOT language into a map containing
  779. * with nodes and edges in the format of graph.
  780. * @param {string} data Text containing a graph in DOT-notation
  781. * @return {Object} graphData
  782. */
  783. function DOTToGraph (data) {
  784. // parse the DOT file
  785. var dotData = parseDOT(data);
  786. var graphData = {
  787. nodes: [],
  788. edges: [],
  789. options: {}
  790. };
  791. // copy the nodes
  792. if (dotData.nodes) {
  793. dotData.nodes.forEach(function (dotNode) {
  794. var graphNode = {
  795. id: dotNode.id,
  796. label: String(dotNode.label || dotNode.id)
  797. };
  798. merge(graphNode, convertAttr(dotNode.attr, NODE_ATTR_MAPPING));
  799. if (graphNode.image) {
  800. graphNode.shape = 'image';
  801. }
  802. graphData.nodes.push(graphNode);
  803. });
  804. }
  805. // copy the edges
  806. if (dotData.edges) {
  807. /**
  808. * Convert an edge in DOT format to an edge with VisGraph format
  809. * @param {Object} dotEdge
  810. * @returns {Object} graphEdge
  811. */
  812. var convertEdge = function (dotEdge) {
  813. var graphEdge = {
  814. from: dotEdge.from,
  815. to: dotEdge.to
  816. };
  817. merge(graphEdge, convertAttr(dotEdge.attr, EDGE_ATTR_MAPPING));
  818. // Add arrows attribute to default styled arrow.
  819. // The reason why default style is not added in parseAttributeList() is
  820. // because only default is cleared before here.
  821. if (graphEdge.arrows == null && dotEdge.type === '->') {
  822. graphEdge.arrows = 'to';
  823. }
  824. return graphEdge;
  825. };
  826. dotData.edges.forEach(function (dotEdge) {
  827. var from, to;
  828. if (dotEdge.from instanceof Object) {
  829. from = dotEdge.from.nodes;
  830. }
  831. else {
  832. from = {
  833. id: dotEdge.from
  834. }
  835. }
  836. // TODO: support for attributes 'dir' (edge arrows)
  837. if (dotEdge.to instanceof Object) {
  838. to = dotEdge.to.nodes;
  839. }
  840. else {
  841. to = {
  842. id: dotEdge.to
  843. }
  844. }
  845. if (dotEdge.from instanceof Object && dotEdge.from.edges) {
  846. dotEdge.from.edges.forEach(function (subEdge) {
  847. var graphEdge = convertEdge(subEdge);
  848. graphData.edges.push(graphEdge);
  849. });
  850. }
  851. forEach2(from, to, function (from, to) {
  852. var subEdge = createEdge(graphData, from.id, to.id, dotEdge.type, dotEdge.attr);
  853. var graphEdge = convertEdge(subEdge);
  854. graphData.edges.push(graphEdge);
  855. });
  856. if (dotEdge.to instanceof Object && dotEdge.to.edges) {
  857. dotEdge.to.edges.forEach(function (subEdge) {
  858. var graphEdge = convertEdge(subEdge);
  859. graphData.edges.push(graphEdge);
  860. });
  861. }
  862. });
  863. }
  864. // copy the options
  865. if (dotData.attr) {
  866. graphData.options = dotData.attr;
  867. }
  868. return graphData;
  869. }
  870. // exports
  871. exports.parseDOT = parseDOT;
  872. exports.DOTToGraph = DOTToGraph;