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.

751 lines
21 KiB

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