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.

735 lines
20 KiB

  1. /**
  2. * Created by Alex on 3/26/2015.
  3. */
  4. var util = require('../../util');
  5. import ColorPicker from './components/ColorPicker'
  6. class ConfigurationSystem {
  7. constructor(network) {
  8. this.network = network;
  9. this.changedOptions = [];
  10. this.possibleOptions = {
  11. nodes: {
  12. borderWidth: [1, 0, 10, 1],
  13. borderWidthSelected: [2, 0, 10, 1],
  14. color: {
  15. border: ['color','#2B7CE9'],
  16. background: ['color','#97C2FC'],
  17. highlight: {
  18. border: ['color','#2B7CE9'],
  19. background: ['color','#D2E5FF']
  20. },
  21. hover: {
  22. border: ['color','#2B7CE9'],
  23. background: ['color','#D2E5FF']
  24. }
  25. },
  26. fixed: {
  27. x: false,
  28. y: false
  29. },
  30. font: {
  31. color: ['color','#343434'],
  32. size: [14, 0, 100, 1], // px
  33. face: ['arial', 'verdana', 'tahoma'],
  34. background: ['color','none'],
  35. stroke: [0, 0, 50, 1], // px
  36. strokeColor: ['color','#ffffff']
  37. },
  38. //group: 'string',
  39. hidden: false,
  40. //icon: {
  41. // face: 'string', //'FontAwesome',
  42. // code: 'string', //'\uf007',
  43. // size: [50, 0, 200, 1], //50,
  44. // color: ['color','#2B7CE9'] //'#aa00ff'
  45. //},
  46. //image: 'string', // --> URL
  47. physics: true,
  48. scaling: {
  49. min: [10, 0, 200, 1],
  50. max: [30, 0, 200, 1],
  51. label: {
  52. enabled: true,
  53. min: [14, 0, 200, 1],
  54. max: [30, 0, 200, 1],
  55. maxVisible: [30, 0, 200, 1],
  56. drawThreshold: [3, 0, 20, 1]
  57. }
  58. },
  59. shape: ['ellipse', 'box', 'circle', 'database', 'diamond', 'dot', 'square', 'star', 'text', 'triangle', 'triangleDown'],
  60. size: [25, 0, 200, 1]
  61. },
  62. edges: {
  63. arrows: {
  64. to: {enabled: false, scaleFactor: [1, 0, 3, 0.05]}, // boolean / {arrowScaleFactor:1} / {enabled: false, arrowScaleFactor:1}
  65. middle: {enabled: false, scaleFactor: [1, 0, 3, 0.05]},
  66. from: {enabled: false, scaleFactor: [1, 0, 3, 0.05]}
  67. },
  68. color: {
  69. color: ['color','#848484'],
  70. highlight: ['color','#848484'],
  71. hover: ['color','#848484'],
  72. inherit: {
  73. enabled: true,
  74. source: ['from', 'to'], // from / to
  75. useGradients: false
  76. },
  77. opacity: [1, 0, 1, 0.05]
  78. },
  79. dashes: {
  80. enabled: false,
  81. length: [5, 0, 50, 1],
  82. gap: [5, 0, 50, 1],
  83. altLength: [5, 0, 50, 1]
  84. },
  85. font: {
  86. color: ['color','#343434'],
  87. size: [14, 0, 100, 1], // px
  88. face: ['arial', 'verdana', 'tahoma'],
  89. background: ['color','none'],
  90. stroke: [1, 0, 50, 1], // px
  91. strokeColor: ['color','#ffffff'],
  92. align: ['horizontal', 'top', 'middle', 'bottom']
  93. },
  94. hidden: false,
  95. hoverWidth: [1.5, 0, 10, 0.1],
  96. physics: true,
  97. scaling: {
  98. min: [1, 0, 100, 1],
  99. max: [15, 0, 100, 1],
  100. label: {
  101. enabled: true,
  102. min: [14, 0, 200, 1],
  103. max: [30, 0, 200, 1],
  104. maxVisible: [30, 0, 200, 1],
  105. drawThreshold: [3, 0, 20, 1]
  106. }
  107. },
  108. selfReferenceSize: [20, 0, 200, 1],
  109. smooth: {
  110. enabled: true,
  111. dynamic: true,
  112. type: ['continuous', 'discrete', 'diagonalCross', 'straightCross', 'horizontal', 'vertical', 'curvedCW', 'curvedCCW'],
  113. roundness: [0.5, 0, 1, 0.05]
  114. },
  115. width: [1, 0, 30, 1],
  116. widthSelectionMultiplier: [2, 0, 5, 0.1]
  117. },
  118. layout: {
  119. randomSeed: [0, 0, 500, 1],
  120. hierarchical: {
  121. enabled: false,
  122. levelSeparation: [150, 20, 500, 5],
  123. direction: ['UD', 'DU', 'LR', 'RL'], // UD, DU, LR, RL
  124. sortMethod: ['hubsize', 'directed'] // hubsize, directed
  125. }
  126. },
  127. interaction: {
  128. dragNodes: true,
  129. dragView: true,
  130. zoomView: true,
  131. hoverEnabled: false,
  132. showNavigationIcons: false,
  133. tooltip: {
  134. delay: [300, 0, 1000, 25],
  135. fontColor: ['color','#000000'],
  136. fontSize: [14, 0, 40, 1], // px
  137. fontFace: ['arial', 'verdana', 'tahoma'],
  138. color: {
  139. border: ['color','#666666'],
  140. background: ['color','#FFFFC6']
  141. }
  142. },
  143. keyboard: {
  144. enabled: false,
  145. speed: {x: [10, 0, 40, 1], y: [10, 0, 40, 1], zoom: [0.02, 0, 0.1, 0.005]},
  146. bindToWindow: true
  147. }
  148. },
  149. manipulation: {
  150. enabled: false,
  151. initiallyVisible: false,
  152. locale: ['en', 'nl'],
  153. functionality: {
  154. addNode: true,
  155. addEdge: true,
  156. editNode: true,
  157. editEdge: true,
  158. deleteNode: true,
  159. deleteEdge: true
  160. }
  161. },
  162. physics: {
  163. barnesHut: {
  164. theta: [0.5, 0.1, 1, 0.05],
  165. gravitationalConstant: [-2000, -30000, 0, 50],
  166. centralGravity: [0.3, 0, 10, 0.05],
  167. springLength: [95, 0, 500, 5],
  168. springConstant: [0.04, 0, 5, 0.005],
  169. damping: [0.09, 0, 1, 0.01]
  170. },
  171. repulsion: {
  172. centralGravity: [0.2, 0, 10, 0.05],
  173. springLength: [200, 0, 500, 5],
  174. springConstant: [0.05, 0, 5, 0.005],
  175. nodeDistance: [100, 0, 500, 5],
  176. damping: [0.09, 0, 1, 0.01]
  177. },
  178. hierarchicalRepulsion: {
  179. centralGravity: [0.2, 0, 10, 0.05],
  180. springLength: [100, 0, 500, 5],
  181. springConstant: [0.01, 0, 5, 0.005],
  182. nodeDistance: [120, 0, 500, 5],
  183. damping: [0.09, 0, 1, 0.01]
  184. },
  185. maxVelocity: [50, 0, 150, 1],
  186. minVelocity: [0.1, 0.01, 0.5, 0.01],
  187. solver: ['barnesHut', 'repulsion', 'hierarchicalRepulsion'],
  188. timestep: [0.5, 0, 1, 0.05]
  189. },
  190. selection: {
  191. select: true,
  192. selectConnectedEdges: true
  193. },
  194. renderer: {
  195. hideEdgesOnDrag: false,
  196. hideNodesOnDrag: false
  197. }
  198. };
  199. this.actualOptions = {
  200. nodes:{},
  201. edges:{},
  202. layout:{},
  203. interaction:{},
  204. manipulation:{},
  205. physics:{},
  206. selection:{},
  207. renderer:{},
  208. configure: false,
  209. configureContainer: undefined
  210. };
  211. this.domElements = [];
  212. this.colorPicker = new ColorPicker(this.network.canvas.pixelRatio);
  213. }
  214. /**
  215. * refresh all options.
  216. * Because all modules parse their options by themselves, we just use their options. We copy them here.
  217. *
  218. * @param options
  219. */
  220. setOptions(options) {
  221. if (options !== undefined) {
  222. util.extend(this.actualOptions, options);
  223. }
  224. this._clean();
  225. if (this.actualOptions.configure !== undefined && this.actualOptions.configure !== false) {
  226. util.deepExtend(this.actualOptions.nodes, this.network.nodesHandler.options, true);
  227. util.deepExtend(this.actualOptions.edges, this.network.edgesHandler.options, true);
  228. util.deepExtend(this.actualOptions.layout, this.network.layoutEngine.options, true);
  229. util.deepExtend(this.actualOptions.interaction, this.network.interactionHandler.options, true);
  230. util.deepExtend(this.actualOptions.manipulation, this.network.manipulation.options, true);
  231. util.deepExtend(this.actualOptions.physics, this.network.physics.options, true);
  232. util.deepExtend(this.actualOptions.selection, this.network.selectionHandler.selection, true);
  233. util.deepExtend(this.actualOptions.renderer, this.network.renderer.selection, true);
  234. if (this.actualOptions.configurationContainer !== undefined) {
  235. this.container = this.actualOptions.configurationContainer;
  236. }
  237. else {
  238. this.container = this.network.body.container;
  239. }
  240. let config;
  241. if (this.actualOptions.configure instanceof Array) {
  242. config = this.actualOptions.configure.join();
  243. }
  244. else if (typeof this.actualOptions.configure === 'string') {
  245. config = this.actualOptions.configure;
  246. }
  247. else if (typeof this.actualOptions.configure === 'boolean') {
  248. config = this.actualOptions.configure;
  249. }
  250. else {
  251. this._clean();
  252. throw new Error('the option for configure has to be either a string, boolean or an array. Supplied:' + this.options.configure);
  253. return;
  254. }
  255. this._create(config);
  256. }
  257. }
  258. /**
  259. * Create all DOM elements
  260. * @param {Boolean | String} config
  261. * @private
  262. */
  263. _create(config) {
  264. this._clean();
  265. this.changedOptions = [];
  266. let counter = 0;
  267. for (let option in this.possibleOptions) {
  268. if (this.possibleOptions.hasOwnProperty(option)) {
  269. if (config === true || config.indexOf(option) !== -1) {
  270. let optionObj = this.possibleOptions[option];
  271. // linebreak between categories
  272. if (counter > 0) {
  273. this._makeEntree([]);
  274. }
  275. // a header for the category
  276. this._makeHeader(option);
  277. // get the suboptions
  278. let path = [option];
  279. this._handleObject(optionObj, path);
  280. }
  281. counter++;
  282. }
  283. }
  284. let generateButton = document.createElement('div');
  285. generateButton.className = 'vis-network-configuration button';
  286. generateButton.innerHTML = 'generate options';
  287. generateButton.onclick = () => {this._printOptions();};
  288. generateButton.onmouseover = () => {generateButton.className = 'vis-network-configuration button hover';};
  289. generateButton.onmouseout = () => {generateButton.className = 'vis-network-configuration button';};
  290. this.optionsContainer = document.createElement('div');
  291. this.optionsContainer.className = 'vis-network-configuration optionContainer';
  292. this.domElements.push(this.optionsContainer);
  293. this.domElements.push(generateButton);
  294. this._push();
  295. this.colorPicker.insertTo(this.container);
  296. }
  297. /**
  298. * draw all DOM elements on the screen
  299. * @private
  300. */
  301. _push() {
  302. for (var i = 0; i < this.domElements.length; i++) {
  303. this.container.appendChild(this.domElements[i]);
  304. }
  305. }
  306. /**
  307. * delete all DOM elements
  308. * @private
  309. */
  310. _clean() {
  311. for (var i = 0; i < this.domElements.length; i++) {
  312. this.container.removeChild(this.domElements[i]);
  313. }
  314. this.domElements = [];
  315. }
  316. /**
  317. * get the value from the actualOptions if it exists
  318. * @param {array} path | where to look for the actual option
  319. * @returns {*}
  320. * @private
  321. */
  322. _getValue(path) {
  323. let base = this.actualOptions;
  324. for (let i = 0; i < path.length; i++) {
  325. if (base[path[i]] !== undefined) {
  326. base = base[path[i]];
  327. }
  328. else {
  329. base = undefined;
  330. break;
  331. }
  332. }
  333. return base;
  334. }
  335. /**
  336. * Copy the path and add a step. It needs to copy because the path will keep stacking otherwise.
  337. * @param path
  338. * @param newValue
  339. * @returns {Array}
  340. * @private
  341. */
  342. _addToPath(path, newValue) {
  343. let newPath = [];
  344. for (let i = 0; i < path.length; i++) {
  345. newPath.push(path[i]);
  346. }
  347. newPath.push(newValue);
  348. return newPath;
  349. }
  350. /**
  351. * all option elements are wrapped in an entree
  352. * @param path
  353. * @param domElements
  354. * @private
  355. */
  356. _makeEntree(path,...domElements) {
  357. let entree = document.createElement('div');
  358. entree.className = 'vis-network-configuration entree s' + path.length;
  359. domElements.forEach((element) => {
  360. entree.appendChild(element);
  361. });
  362. this.domElements.push(entree);
  363. }
  364. /**
  365. * header for major subjects
  366. * @param name
  367. * @private
  368. */
  369. _makeHeader(name) {
  370. let div = document.createElement('div');
  371. div.className = 'vis-network-configuration header';
  372. div.innerHTML = name;
  373. this._makeEntree([],div);
  374. }
  375. /**
  376. * make a label, if it is an object label, it gets different styling.
  377. * @param name
  378. * @param path
  379. * @param objectLabel
  380. * @returns {HTMLElement}
  381. * @private
  382. */
  383. _makeLabel(name, path, objectLabel = false) {
  384. let div = document.createElement('div');
  385. div.className = 'vis-network-configuration label s' + path.length;
  386. if (objectLabel === true) {
  387. div.innerHTML = '<i><b>' + name + ':</b></i>';
  388. }
  389. else {
  390. div.innerHTML = name + ':';
  391. }
  392. return div;
  393. }
  394. /**
  395. * make a dropdown list for multiple possible string optoins
  396. * @param arr
  397. * @param value
  398. * @param path
  399. * @private
  400. */
  401. _makeDropdown(arr, value, path) {
  402. let select = document.createElement('select');
  403. select.className = 'vis-network-configuration select';
  404. let selectedValue = 0;
  405. if (value !== undefined) {
  406. if (arr.indexOf(value) !== -1) {
  407. selectedValue = arr.indexOf(value);
  408. }
  409. }
  410. for (let i = 0; i < arr.length; i++) {
  411. let option = document.createElement('option');
  412. option.value = arr[i];
  413. if (i === selectedValue) {
  414. option.selected = 'selected';
  415. }
  416. option.innerHTML = arr[i];
  417. select.appendChild(option);
  418. }
  419. let me = this;
  420. select.onchange = function () {me._update(this.value, path);};
  421. let label = this._makeLabel(path[path.length-1], path);
  422. this._makeEntree(path, label, select);
  423. }
  424. /**
  425. * make a range object for numeric options
  426. * @param arr
  427. * @param value
  428. * @param path
  429. * @private
  430. */
  431. _makeRange(arr, value, path) {
  432. let defaultValue = arr[0];
  433. let min = arr[1];
  434. let max = arr[2];
  435. let step = arr[3];
  436. let range = document.createElement('input');
  437. range.type = 'range';
  438. range.className = 'vis-network-configuration range';
  439. range.min = min;
  440. range.max = max;
  441. range.step = step;
  442. if (value !== undefined) {
  443. if (value * 0.1 < min) {
  444. range.min = value / 10;
  445. }
  446. if (value * 2 > max && max !== 1) {
  447. range.max = value * 2;
  448. }
  449. range.value = value;
  450. }
  451. else {
  452. range.value = defaultValue;
  453. }
  454. let input = document.createElement('input');
  455. input.className = 'vis-network-configuration rangeinput';
  456. input.value = range.value;
  457. var me = this;
  458. range.onchange = function () {input.value = this.value; me._update(this.value, path);};
  459. range.oninput = function () {input.value = this.value; };
  460. let label = this._makeLabel(path[path.length-1], path);
  461. this._makeEntree(path, label, range, input);
  462. }
  463. /**
  464. * make a checkbox for boolean options.
  465. * @param defaultValue
  466. * @param value
  467. * @param path
  468. * @private
  469. */
  470. _makeCheckbox(defaultValue, value, path) {
  471. var checkbox = document.createElement('input');
  472. checkbox.type = 'checkbox';
  473. checkbox.className = 'vis-network-configuration checkbox';
  474. checkbox.checked = defaultValue;
  475. if (value !== undefined) {
  476. checkbox.checked = value;
  477. if (value !== defaultValue) {
  478. if (typeof defaultValue === 'object') {
  479. if (value !== defaultValue.enabled) {
  480. this.changedOptions.push({path:path, value:value});
  481. }
  482. }
  483. else {
  484. this.changedOptions.push({path:path, value:value});
  485. }
  486. }
  487. }
  488. let me = this;
  489. checkbox.onchange = function() {me._update(this.checked, path)};
  490. let label = this._makeLabel(path[path.length-1], path);
  491. this._makeEntree(path, label, checkbox);
  492. }
  493. /**
  494. * make a color field with a color picker for color fields
  495. * @param arr
  496. * @param value
  497. * @param path
  498. * @private
  499. */
  500. _makeColorField(arr, value, path) {
  501. let defaultColor = arr[1];
  502. let div = document.createElement('div');
  503. value = value === undefined ? defaultColor : value;
  504. if (value !== 'none') {
  505. div.className = 'vis-network-configuration colorBlock';
  506. div.style.backgroundColor = value;
  507. }
  508. else {
  509. div.className = 'vis-network-configuration colorBlock none';
  510. }
  511. value = value === undefined ? defaultColor : value;
  512. div.onclick = () => {
  513. this._showColorPicker(value,div,path);
  514. }
  515. let label = this._makeLabel(path[path.length-1], path);
  516. this._makeEntree(path,label, div);
  517. }
  518. /**
  519. * used by the color buttons to call the color picker.
  520. * @param event
  521. * @param value
  522. * @param div
  523. * @param path
  524. * @private
  525. */
  526. _showColorPicker(value, div, path) {
  527. let rect = div.getBoundingClientRect();
  528. let bodyRect = document.body.getBoundingClientRect();
  529. let pickerX = rect.left + rect.width + 5;
  530. let pickerY = rect.top - bodyRect.top + rect.height*0.5;
  531. this.colorPicker.show(pickerX,pickerY);
  532. this.colorPicker.setColor(value);
  533. this.colorPicker.setCallback((color) => {
  534. let colorString = 'rgba(' + color.r + ',' + color.g + ',' + color.b + ',' + color.a + ')';
  535. div.style.backgroundColor = colorString;
  536. this._update(colorString,path);
  537. })
  538. }
  539. /**
  540. * parse an object and draw the correct entrees
  541. * @param obj
  542. * @param path
  543. * @private
  544. */
  545. _handleObject(obj, path = []) {
  546. for (let subObj in obj) {
  547. if (obj.hasOwnProperty(subObj)) {
  548. let item = obj[subObj];
  549. let newPath = this._addToPath(path, subObj);
  550. let value = this._getValue(newPath);
  551. if (item instanceof Array) {
  552. this._handleArray(item, value, newPath);
  553. }
  554. else if (typeof item === 'string') {
  555. this._handleString(item, value, newPath);
  556. }
  557. else if (typeof item === 'boolean') {
  558. this._makeCheckbox(item, value, newPath);
  559. }
  560. else if (item instanceof Object) {
  561. // collapse the physics options that are not enabled
  562. let draw = true;
  563. if (path.indexOf('physics') !== -1) {
  564. if (this.actualOptions.physics.solver !== subObj) {
  565. draw = false;
  566. }
  567. }
  568. if (draw === true) {
  569. // initially collapse options with an disabled enabled option.
  570. if (item.enabled !== undefined) {
  571. let enabledPath = this._addToPath(newPath, 'enabled');
  572. let enabledValue = this._getValue(enabledPath);
  573. if (enabledValue === true) {
  574. let label = this._makeLabel(subObj, newPath, true);
  575. this._makeEntree(newPath, label);
  576. this._handleObject(item, newPath);
  577. }
  578. else {
  579. this._makeCheckbox(item, enabledValue, newPath);
  580. }
  581. }
  582. else {
  583. let label = this._makeLabel(subObj, newPath, true);
  584. this._makeEntree(newPath, label);
  585. this._handleObject(item, newPath);
  586. }
  587. }
  588. }
  589. else {
  590. console.error('dont know how to handle', item, subObj, newPath);
  591. }
  592. }
  593. }
  594. }
  595. /**
  596. * handle the array type of option
  597. * @param optionName
  598. * @param arr
  599. * @param value
  600. * @param path
  601. * @private
  602. */
  603. _handleArray(arr, value, path) {
  604. if (typeof arr[0] === 'string' && arr[0] === 'color') {
  605. this._makeColorField(arr, value, path);
  606. if (arr[1] !== value) {this.changedOptions.push({path:path, value:value});}
  607. }
  608. else if (typeof arr[0] === 'string') {
  609. this._makeDropdown(arr, value, path);
  610. if (arr[0] !== value) {this.changedOptions.push({path:path, value:value});}
  611. }
  612. else if (typeof arr[0] === 'number') {
  613. this._makeRange(arr, value, path);
  614. if (arr[0] !== value) {this.changedOptions.push({path:path, value:value});}
  615. }
  616. }
  617. /**
  618. * handle the string type of option.
  619. * TODO: Not sure what to do with this
  620. * @param optionName
  621. * @param string
  622. * @param value
  623. * @param path
  624. * @private
  625. */
  626. _handleString(string, value, path) {
  627. if (string === 'string') {
  628. }
  629. else {
  630. //this._makeLabel(optionName, path);
  631. //console.log('string', string, value, path);
  632. }
  633. }
  634. /**
  635. * called to update the network with the new settings.
  636. * @param value
  637. * @param path
  638. * @private
  639. */
  640. _update(value, path) {
  641. let options = this._constructOptions(value,path);
  642. this.network.setOptions(options);
  643. }
  644. _constructOptions(value,path, optionsObj = {}) {
  645. let pointer = optionsObj;
  646. for (let i = 0; i < path.length; i++) {
  647. if (pointer[path[i]] === undefined) {
  648. pointer[path[i]] = {};
  649. }
  650. if (i !== path.length -1) {
  651. pointer = pointer[path[i]];
  652. }
  653. else {
  654. pointer[path[i]] = value;
  655. }
  656. }
  657. return optionsObj;
  658. }
  659. _printOptions() {
  660. let options = {};
  661. for (var i = 0; i < this.changedOptions.length; i++) {
  662. this._constructOptions(this.changedOptions[i].value, this.changedOptions[i].path, options)
  663. }
  664. this.optionsContainer.innerHTML = '<pre>var options = ' + JSON.stringify(options, null, 2) + '</pre>';
  665. }
  666. }
  667. export default ConfigurationSystem;