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.

419 lines
12 KiB

  1. /**
  2. * Created by Alex on 3/4/2015.
  3. */
  4. var util = require("../../util");
  5. var DataSet = require('../../DataSet');
  6. var DataView = require('../../DataView');
  7. import Edge from "./components/Edge"
  8. class EdgesHandler {
  9. constructor(body, images, groups) {
  10. this.body = body;
  11. this.images = images;
  12. this.groups = groups;
  13. // create the edge API in the body container
  14. this.body.functions.createEdge = this.create.bind(this);
  15. this.edgesListeners = {
  16. 'add': (event, params) => {this.add(params.items);},
  17. 'update': (event, params) => {this.update(params.items);},
  18. 'remove': (event, params) => {this.remove(params.items);}
  19. };
  20. this.options = {};
  21. this.defaultOptions = {
  22. arrows: {
  23. to: {enabled: false, scaleFactor:1}, // boolean / {arrowScaleFactor:1} / {enabled: false, arrowScaleFactor:1}
  24. middle: {enabled: false, scaleFactor:1},
  25. from: {enabled: false, scaleFactor:1}
  26. },
  27. color: {
  28. color:'#848484',
  29. highlight:'#848484',
  30. hover: '#848484',
  31. inherit: {
  32. enabled: true,
  33. source: 'from', // from / true
  34. useGradients: false // release in 4.0
  35. },
  36. opacity:1.0
  37. },
  38. dashes:{
  39. enabled: false,
  40. preset: 'dotted',
  41. length: 10,
  42. gap: 5,
  43. altLength: undefined
  44. },
  45. font: {
  46. color: '#343434',
  47. size: 14, // px
  48. face: 'arial',
  49. background: 'none',
  50. stroke: 1, // px
  51. strokeColor: '#ffffff',
  52. align:'horizontal'
  53. },
  54. hidden: false,
  55. hoverWidth: 1.5,
  56. label: undefined,
  57. length: undefined,
  58. physics: true,
  59. scaling:{
  60. min: 1,
  61. max: 15,
  62. label: {
  63. enabled: true,
  64. min: 14,
  65. max: 30,
  66. maxVisible: 30,
  67. drawThreshold: 3
  68. },
  69. customScalingFunction: function (min,max,total,value) {
  70. if (max === min) {
  71. return 0.5;
  72. }
  73. else {
  74. var scale = 1 / (max - min);
  75. return Math.max(0,(value - min)*scale);
  76. }
  77. }
  78. },
  79. selfReferenceSize:20,
  80. smooth: {
  81. enabled: true,
  82. dynamic: true,
  83. type: "continuous",
  84. roundness: 0.5
  85. },
  86. title:undefined,
  87. width: 1,
  88. widthSelectionMultiplier: 2,
  89. value:1
  90. };
  91. util.extend(this.options, this.defaultOptions);
  92. // this allows external modules to force all dynamic curves to turn static.
  93. this.body.emitter.on("_forceDisableDynamicCurves", (type) => {
  94. let emitChange = false;
  95. for (let edgeId in this.body.edges) {
  96. if (this.body.edges.hasOwnProperty(edgeId)) {
  97. let edgeOptions = this.body.edges[edgeId].options.smooth;
  98. if (edgeOptions.enabled === true && edgeOptions.dynamic === true) {
  99. if (type === undefined) {
  100. edge.setOptions({smooth:false});
  101. }
  102. else {
  103. edge.setOptions({smooth:{dynamic:false, type:type}});
  104. }
  105. emitChange = true;
  106. }
  107. }
  108. }
  109. if (emitChange === true) {
  110. this.body.emitter.emit("_dataChanged");
  111. }
  112. });
  113. // this is called when options of EXISTING nodes or edges have changed.
  114. this.body.emitter.on("_dataUpdated", () => {
  115. this.reconnectEdges();
  116. this.markAllEdgesAsDirty();
  117. });
  118. }
  119. setOptions(options) {
  120. if (options !== undefined) {
  121. var fields = [
  122. 'font',
  123. 'from',
  124. 'hidden',
  125. 'hoverWidth',
  126. 'label',
  127. 'length',
  128. 'line',
  129. 'opacity',
  130. 'physics',
  131. 'selfReferenceSize',
  132. 'to',
  133. 'title',
  134. 'value',
  135. 'width',
  136. 'widthMin',
  137. 'widthMax',
  138. 'widthSelectionMultiplier'
  139. ];
  140. util.selectiveExtend(fields, this.options, options);
  141. util.mergeOptions(this.options, options, 'smooth');
  142. util.mergeOptions(this.options, options, 'dashes');
  143. // set the scaling options
  144. if (options.scaling !== undefined) {
  145. if (options.scaling.min !== undefined) {this.options.scaling.min = options.scaling.min;}
  146. if (options.scaling.max !== undefined) {this.options.scaling.max = options.scaling.max;}
  147. util.mergeOptions(this.options.scaling, options.scaling, 'label');
  148. }
  149. // hanlde multiple input cases for arrows
  150. if (options.arrows !== undefined) {
  151. if (typeof options.arrows === 'string') {
  152. let arrows = options.arrows.toLowerCase();
  153. if (arrows.indexOf("to") != -1) {this.options.arrows.to.enabled = true;}
  154. if (arrows.indexOf("middle") != -1) {this.options.arrows.middle.enabled = true;}
  155. if (arrows.indexOf("from") != -1) {this.options.arrows.from.enabled = true;}
  156. }
  157. else if (typeof options.arrows === 'object') {
  158. util.mergeOptions(this.options.arrows, options.arrows, 'to');
  159. util.mergeOptions(this.options.arrows, options.arrows, 'middle');
  160. util.mergeOptions(this.options.arrows, options.arrows, 'from');
  161. }
  162. else {
  163. throw new Error("The arrow options can only be an object or a string. Refer to the documentation. You used:" + JSON.stringify(options.arrows));
  164. }
  165. }
  166. // hanlde multiple input cases for color
  167. if (options.color !== undefined) {
  168. if (util.isString(options.color)) {
  169. util.assignAllKeys(this.options.color, options.color);
  170. this.options.color.inherit.enabled = false;
  171. }
  172. else {
  173. if (typeof options.color === 'object') {
  174. let colorsDefined = false;
  175. if (options.color.color !== undefined) {this.options.color.color = options.color.color; colorsDefined = true;}
  176. if (options.color.highlight !== undefined) {this.options.color.highlight = options.color.highlight; colorsDefined = true;}
  177. if (options.color.hover !== undefined) {this.options.color.hover = options.color.hover; colorsDefined = true;}
  178. if (options.color.opacity !== undefined) {this.options.color.opacity = options.color.opacity;}
  179. if (options.color.inherit === undefined && colorsDefined === true) {
  180. this.options.color.inherit.enabled = false;
  181. }
  182. }
  183. }
  184. util.mergeOptions(this.options.color, options.color, 'inherit');
  185. this.markAllEdgesAsDirty();
  186. }
  187. // update smooth settings in all edges
  188. let dataChanged = false;
  189. if (options.smooth !== undefined) {
  190. for (let edgeId in this.body.edges) {
  191. if (this.body.edges.hasOwnProperty(edgeId)) {
  192. dataChanged = this.body.edges[edgeId].updateEdgeType() || dataChanged;
  193. }
  194. }
  195. }
  196. // update fonts in all edges
  197. if (options.font) {
  198. if (typeof options.font === 'string') {
  199. let optionsArray = options.font.split(" ");
  200. this.options.font.size = optionsArray[0].replace("px",'');
  201. this.options.font.face = optionsArray[1];
  202. this.options.font.color = optionsArray[2];
  203. }
  204. else if (typeof options.font === 'object') {
  205. this.options.font = util.bridgeObject(options.font);
  206. }
  207. this.options.font.size = Number(this.options.font.size);
  208. for (let edgeId in this.body.edges) {
  209. if (this.body.edges.hasOwnProperty(edgeId)) {
  210. this.body.edges[edgeId].updateLabelModule();
  211. }
  212. }
  213. }
  214. // update the state of the variables if needed
  215. if (options.hidden !== undefined || options.physics !== undefined || dataChanged === true) {
  216. this.body.emitter.emit('_dataChanged');
  217. }
  218. }
  219. }
  220. /**
  221. * Load edges by reading the data table
  222. * @param {Array | DataSet | DataView} edges The data containing the edges.
  223. * @private
  224. * @private
  225. */
  226. setData(edges, doNotEmit = false) {
  227. var oldEdgesData = this.body.data.edges;
  228. if (edges instanceof DataSet || edges instanceof DataView) {
  229. this.body.data.edges = edges;
  230. }
  231. else if (Array.isArray(edges)) {
  232. this.body.data.edges = new DataSet();
  233. this.body.data.edges.add(edges);
  234. }
  235. else if (!edges) {
  236. this.body.data.edges = new DataSet();
  237. }
  238. else {
  239. throw new TypeError('Array or DataSet expected');
  240. }
  241. // TODO: is this null or undefined or false?
  242. if (oldEdgesData) {
  243. // unsubscribe from old dataset
  244. util.forEach(this.edgesListeners, (callback, event) => {oldEdgesData.off(event, callback);});
  245. }
  246. // remove drawn edges
  247. this.body.edges = {};
  248. // TODO: is this null or undefined or false?
  249. if (this.body.data.edges) {
  250. // subscribe to new dataset
  251. util.forEach(this.edgesListeners, (callback, event) => {this.body.data.edges.on(event, callback);});
  252. // draw all new nodes
  253. var ids = this.body.data.edges.getIds();
  254. this.add(ids, true);
  255. }
  256. if (doNotEmit === false) {
  257. this.body.emitter.emit("_dataChanged");
  258. }
  259. }
  260. /**
  261. * Add edges
  262. * @param {Number[] | String[]} ids
  263. * @private
  264. */
  265. add(ids, doNotEmit = false) {
  266. var edges = this.body.edges;
  267. var edgesData = this.body.data.edges;
  268. for (let i = 0; i < ids.length; i++) {
  269. var id = ids[i];
  270. var oldEdge = edges[id];
  271. if (oldEdge) {
  272. oldEdge.disconnect();
  273. }
  274. var data = edgesData.get(id, {"showInternalIds" : true});
  275. edges[id] = this.create(data);
  276. }
  277. if (doNotEmit === false) {
  278. this.body.emitter.emit("_dataChanged");
  279. }
  280. }
  281. /**
  282. * Update existing edges, or create them when not yet existing
  283. * @param {Number[] | String[]} ids
  284. * @private
  285. */
  286. update(ids) {
  287. var edges = this.body.edges;
  288. var edgesData = this.body.data.edges;
  289. var dataChanged = false;
  290. for (var i = 0; i < ids.length; i++) {
  291. var id = ids[i];
  292. var data = edgesData.get(id);
  293. var edge = edges[id];
  294. if (edge === null) {
  295. // update edge
  296. edge.disconnect();
  297. dataChanged = edge.setOptions(data) || dataChanged; // if a support node is added, data can be changed.
  298. edge.connect();
  299. }
  300. else {
  301. // create edge
  302. this.body.edges[id] = this.create(data);
  303. dataChanged = true;
  304. }
  305. }
  306. if (dataChanged === true) {
  307. this.body.emitter.emit("_dataChanged");
  308. }
  309. else {
  310. this.body.emitter.emit("_dataUpdated");
  311. }
  312. }
  313. /**
  314. * Remove existing edges. Non existing ids will be ignored
  315. * @param {Number[] | String[]} ids
  316. * @private
  317. */
  318. remove(ids) {
  319. var edges = this.body.edges;
  320. for (var i = 0; i < ids.length; i++) {
  321. var id = ids[i];
  322. var edge = edges[id];
  323. if (edge !== undefined) {
  324. if (edge.via != null) {
  325. delete this.body.supportNodes[edge.via.id];
  326. }
  327. edge.disconnect();
  328. delete edges[id];
  329. }
  330. }
  331. this.body.emitter.emit("_dataChanged");
  332. }
  333. create(properties) {
  334. return new Edge(properties, this.body, this.options)
  335. }
  336. markAllEdgesAsDirty() {
  337. for (var edgeId in this.body.edges) {
  338. this.body.edges[edgeId].edgeType.colorDirty = true;
  339. }
  340. }
  341. /**
  342. * Reconnect all edges
  343. * @private
  344. */
  345. reconnectEdges() {
  346. var id;
  347. var nodes = this.body.nodes;
  348. var edges = this.body.edges;
  349. for (id in nodes) {
  350. if (nodes.hasOwnProperty(id)) {
  351. nodes[id].edges = [];
  352. }
  353. }
  354. for (id in edges) {
  355. if (edges.hasOwnProperty(id)) {
  356. var edge = edges[id];
  357. edge.from = null;
  358. edge.to = null;
  359. edge.connect();
  360. }
  361. }
  362. }
  363. }
  364. export default EdgesHandler;