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.

723 lines
21 KiB

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