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.

720 lines
21 KiB

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