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.

730 lines
20 KiB

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