1. 1 : /**
  2. 2 : * Mandala is a conceptual visualization that shows the relationships between terms and documents.
  3. 3 : *
  4. 4 : * @example
  5. 5 : *
  6. 6 : * let config = {
  7. 7 : * "labels": true,
  8. 8 : * "query": null,
  9. 9 : * "stopList": "auto",
  10. 10 : * };
  11. 11 : *
  12. 12 : * loadCorpus("austen").tool("Mandala", config);
  13. 13 : *
  14. 14 : * @class Mandala
  15. 15 : * @tutorial mandala
  16. 16 : * @memberof Tools
  17. 17 : */
  18. 18 : Ext.define('Voyant.panel.Mandala', {
  19. 19 : extend: 'Ext.panel.Panel',
  20. 20 : mixins: ['Voyant.panel.Panel'],
  21. 21 : alias: 'widget.mandala',
  22. 22 : statics: {
  23. 23 : i18n: {
  24. 24 : },
  25. 25 : api: {
  26. 26 : /**
  27. 27 : * @memberof Tools.Mandala
  28. 28 : * @instance
  29. 29 : * @property {stopList}
  30. 30 : * @default
  31. 31 : */
  32. 32 : stopList: 'auto',
  33. 33 :
  34. 34 : /**
  35. 35 : * @memberof Tools.Mandala
  36. 36 : * @instance
  37. 37 : * @property {query}
  38. 38 : */
  39. 39 : query: undefined,
  40. 40 :
  41. 41 : /**
  42. 42 : * @memberof Tools.Mandala
  43. 43 : * @instance
  44. 44 : * @property {Boolean} labels Whether or not labels should be shown.
  45. 45 : * @default
  46. 46 : */
  47. 47 : labels: true
  48. 48 :
  49. 49 : },
  50. 50 : glyph: 'xf1db@FontAwesome'
  51. 51 : },
  52. 52 :
  53. 53 : gutter: 5,
  54. 54 :
  55. 55 : textFont: '12px sans-serif',
  56. 56 :
  57. 57 : config: {
  58. 58 : options: [{xtype: 'stoplistoption'},{xtype: 'categoriesoption'}]
  59. 59 : },
  60. 60 :
  61. 61 : constructor: function() {
  62. 62 :
  63. 63 : this.mixins['Voyant.util.Localization'].constructor.apply(this, arguments);
  64. 64 : Ext.apply(this, {
  65. 65 : title: this.localize('title'),
  66. 66 : html: '<div style="text-align: center"><canvas width="800" height="600"></canvas></div>',
  67. 67 : dockedItems: [{
  68. 68 : dock: 'bottom',
  69. 69 : xtype: 'toolbar',
  70. 70 : overflowHandler: 'scroller',
  71. 71 : items: [{
  72. 72 : text: this.localize('add'),
  73. 73 : glyph: 'xf067@FontAwesome',
  74. 74 : handler: function() {
  75. 75 : this.editMagnet();
  76. 76 : },
  77. 77 : scope: this
  78. 78 : },{
  79. 79 : text: this.localize('clear'),
  80. 80 : glyph: 'xf014@FontAwesome',
  81. 81 : handler: function() {
  82. 82 : this.setApiParam('query', undefined);
  83. 83 : this.updateFromQueries(true);
  84. 84 : this.editMagnet();
  85. 85 : },
  86. 86 : scope: this
  87. 87 : },{
  88. 88 : xtype: 'checkbox',
  89. 89 : boxLabel: this.localize('labels'),
  90. 90 : listeners: {
  91. 91 : render: function(cmp) {
  92. 92 : cmp.setValue(this.getApiParam("labels")===true);
  93. 93 : Ext.tip.QuickTipManager.register({
  94. 94 : target: cmp.getEl(),
  95. 95 : text: this.localize('labelsTip')
  96. 96 : });
  97. 97 :
  98. 98 : },
  99. 99 : beforedestroy: function(cmp) {
  100. 100 : Ext.tip.QuickTipManager.unregister(cmp.getEl());
  101. 101 : },
  102. 102 : change: function(cmp, val) {
  103. 103 : this.setApiParam('labels', val);
  104. 104 : this.draw();
  105. 105 : },
  106. 106 : scope: this
  107. 107 : }
  108. 108 : }]
  109. 109 : }]
  110. 110 : });
  111. 111 : this.callParent(arguments);
  112. 112 : this.mixins['Voyant.panel.Panel'].constructor.apply(this, arguments);
  113. 113 :
  114. 114 : this.on('boxready', function(cmp) {
  115. 115 : var canvas = this.getTargetEl().dom.querySelector("canvas");
  116. 116 : var me = this;
  117. 117 : canvas.addEventListener('mousemove', function(evt) {
  118. 118 : var rect = canvas.getBoundingClientRect(), x = evt.clientX - rect.left, y = evt.clientY - rect.top,
  119. 119 : change = false, docRadius = parseInt(me.textFont)/2;
  120. 120 : if (me.documents) {
  121. 121 : me.documents.forEach(function(doc) {
  122. 122 : var isHovering = x > doc.x-docRadius && x < doc.x+docRadius && y > doc.y-docRadius && y < doc.y+docRadius;
  123. 123 : if (isHovering!=doc.isHovering) {change = true;}
  124. 124 : doc.isHovering = isHovering;
  125. 125 : })
  126. 126 : }
  127. 127 : radius = parseInt(me.textFont)/2;
  128. 128 : for (term in me.magnets) {
  129. 129 : var isHovering = x > me.magnets[term].x-radius && x < me.magnets[term].x+radius && y > me.magnets[term].y-radius && y < me.magnets[term].y+radius;
  130. 130 : if (isHovering!=me.magnets[term].isHovering) {change = true;}
  131. 131 : me.magnets[term].isHovering = isHovering;
  132. 132 : }
  133. 133 : if (change) {
  134. 134 : me.draw();
  135. 135 : }
  136. 136 : }, false);
  137. 137 : canvas.addEventListener('click', function(evt) {
  138. 138 : var rect = canvas.getBoundingClientRect(), x = evt.clientX - rect.left, y = evt.clientY - rect.top,
  139. 139 : docRadius = parseInt(me.textFont)/2;
  140. 140 : for (term in me.magnets) {
  141. 141 : if (x > me.magnets[term].x-radius && x < me.magnets[term].x+radius && y > me.magnets[term].y-radius && y < me.magnets[term].y+radius) {
  142. 142 : me.editMagnet(term);
  143. 143 : }
  144. 144 : }
  145. 145 : }, false);
  146. 146 : })
  147. 147 :
  148. 148 : this.on('loadedCorpus', function(src, corpus) {
  149. 149 : this.documents = [];
  150. 150 : var canvas = this.getTargetEl().dom.querySelector("canvas"), ctx = canvas.getContext("2d"), radius = canvas.width/2;
  151. 151 : ctx.font = this.textFont;
  152. 152 : corpus.getDocuments().each(function(document) {
  153. 153 : var label = document.getTinyTitle();
  154. 154 : this.documents.push({
  155. 155 : doc: document,
  156. 156 : label: label,
  157. 157 : width: ctx.measureText(label).width,
  158. 158 : x: radius,
  159. 159 : y: radius,
  160. 160 : matches: [],
  161. 161 : isHovering: false
  162. 162 : });
  163. 163 : }, this);
  164. 164 : this.updateDocs(canvas);
  165. 165 : this.draw();
  166. 166 : this.updateFromQueries();
  167. 167 : }, this);
  168. 168 :
  169. 169 : this.on("resize", function() {
  170. 170 : var canvas = this.getTargetEl().dom.querySelector("canvas"),
  171. 171 : diam = Math.min(this.getTargetEl().getWidth(), this.getTargetEl().getHeight());
  172. 172 : canvas.width = diam;
  173. 173 : canvas.height = diam;
  174. 174 : this.updateMagnets();
  175. 175 : this.updateDocs();
  176. 176 : this.draw(canvas)
  177. 177 : })
  178. 178 : },
  179. 179 :
  180. 180 : editMagnet: function(term) {
  181. 181 : var me = this, currentTerms = Ext.Array.from(me.getApiParam('query'));
  182. 182 : Ext.create('Ext.window.Window', {
  183. 183 : title: this.localize("EditMagnet"),
  184. 184 : modal: true,
  185. 185 : items: {
  186. 186 : xtype: 'form',
  187. 187 : width: 300,
  188. 188 : items: [{
  189. 189 : xtype: 'querysearchfield',
  190. 190 : corpus: this.getCorpus(),
  191. 191 : store: this.getCorpus().getCorpusTerms({
  192. 192 : proxy: {
  193. 193 : extraParams: {
  194. 194 : stopList: this.getApiParam('stopList')
  195. 195 : }
  196. 196 : }
  197. 197 : }),
  198. 198 : stopList: this.getApiParam('stopList'),
  199. 199 : listeners: {
  200. 200 : afterrender: function(field) {
  201. 201 : if (term) {
  202. 202 : var termObj = new Ext.create("Voyant.data.model.CorpusTerm", {
  203. 203 : term: term
  204. 204 : });
  205. 205 : field.getStore().loadData(termObj, true)
  206. 206 : field.setValue(termObj);
  207. 207 : }
  208. 208 : }
  209. 209 : }
  210. 210 : },{
  211. 211 : xtype: "numberfield",
  212. 212 : fieldLabel: me.localize('rotateClockwise'),
  213. 213 : minValue: 0,
  214. 214 : maxValue: currentTerms.length-1,
  215. 215 : value: 0,
  216. 216 : stepValue: 1,
  217. 217 : width: 200,
  218. 218 : name: "rotate"
  219. 219 : }],
  220. 220 : buttons: [{
  221. 221 : text: this.localize("remove"),
  222. 222 : glyph: 'xf0e2@FontAwesome',
  223. 223 : flex: 1,
  224. 224 : ui: 'default-toolbar',
  225. 225 : handler: function(btn) {
  226. 226 : var queries = Ext.Array.filter(Ext.Array.from(me.getApiParam('query')), function(query) {
  227. 227 : return query!=term
  228. 228 : });
  229. 229 : me.setApiParam('query', queries);
  230. 230 : me.updateFromQueries(queries.length==0);
  231. 231 : btn.up('window').close();
  232. 232 : },
  233. 233 : scope: this
  234. 234 : },{xtype: 'tbfill'}, {
  235. 235 : text: this.localize("cancel"),
  236. 236 : ui: 'default-toolbar',
  237. 237 : glyph: 'xf00d@FontAwesome',
  238. 238 : flex: 1,
  239. 239 : handler: function(btn) {
  240. 240 : btn.up('window').close();
  241. 241 : }
  242. 242 : },{
  243. 243 : text: this.localize("update"),
  244. 244 : glyph: 'xf00c@FontAwesome',
  245. 245 : flex: 1,
  246. 246 : handler: function(btn) {
  247. 247 : var val = btn.up('window').down('querysearchfield').getValue().join("|")
  248. 248 : if (val) {
  249. 249 :
  250. 250 : // start by updating the term in place
  251. 251 : var position = -1;
  252. 252 : for (var i=0; i<currentTerms.length; i++) {
  253. 253 : if (term==currentTerms[i]) {
  254. 254 : position=i;
  255. 255 : currentTerms[i]=val;
  256. 256 :
  257. 257 : // see if we need to shift
  258. 258 : var rotate = btn.up('window').down('numberfield').getValue();
  259. 259 : if (rotate) {
  260. 260 : currentTerms.splice(i, 1);
  261. 261 : var newpos = i+rotate;
  262. 262 : if (newpos>currentTerms.length) {newpos-=currentTerms.length+1;}
  263. 263 : currentTerms.splice(newpos, 0, val);
  264. 264 : }
  265. 265 : break
  266. 266 :
  267. 267 : }
  268. 268 : }
  269. 269 : if (position==-1) { // not sure why it couldn't be found
  270. 270 : currentTerms.push(val);
  271. 271 : }
  272. 272 : }
  273. 273 :
  274. 274 : me.setApiParam('query', currentTerms);
  275. 275 : me.updateFromQueries(currentTerms.length==0);
  276. 276 : btn.up('window').close();
  277. 277 : },
  278. 278 : scope: this
  279. 279 : }]
  280. 280 : },
  281. 281 : bodyPadding: 5
  282. 282 : }).show()
  283. 283 : },
  284. 284 :
  285. 285 : updateFromQueries: function(allowEmpty) {
  286. 286 : this.magnets = undefined;
  287. 287 : this.documents.forEach(function(doc) {doc.matches=[]})
  288. 288 : this.updateDocs();
  289. 289 : this.draw();
  290. 290 : if (this.documents) {
  291. 291 : var params = this.getApiParams();
  292. 292 : if (!params.query) {params.limit=10;}
  293. 293 : var queries = Ext.Array.from(this.getApiParam('query'));
  294. 294 : if (!allowEmpty || queries.length>0) {
  295. 295 : this.getCorpus().getCorpusTerms().load({
  296. 296 : params: Ext.apply(params, {withDistributions: true}),
  297. 297 : callback: function(records) {
  298. 298 : var canvas = this.getTargetEl().dom.querySelector("canvas"), ctx = canvas.getContext("2d");
  299. 299 : diam = canvas.width, rad = diam /2;
  300. 300 : ctx.font = this.textFont;
  301. 301 : var magnets = {};
  302. 302 : for (var i=0, len=records.length; i<len; i++) {
  303. 303 : var term = records[i].getTerm();
  304. 304 : records[i].getDistributions().forEach(function(val, i) {
  305. 305 : if (val>0) {
  306. 306 : this.documents[i].matches.push(term)
  307. 307 : }
  308. 308 : }, this);
  309. 309 : magnets[term] = {
  310. 310 : record: records[i],
  311. 311 : colour: this.getApplication().getColor(i),
  312. 312 : width: ctx.measureText(term).width,
  313. 313 : isHovering: false
  314. 314 : }
  315. 315 : }
  316. 316 :
  317. 317 : this.magnets = {};
  318. 318 :
  319. 319 : // try ordering by queries
  320. 320 : queries.forEach(function(query) {
  321. 321 : if (magnets[query]) {
  322. 322 : this.magnets[query] = magnets[query]
  323. 323 : delete magnets[query]
  324. 324 : }
  325. 325 : }, this);
  326. 326 :
  327. 327 : // now for any leftovers
  328. 328 : for (term in magnets) {
  329. 329 : this.magnets[term] = magnets[term]
  330. 330 : }
  331. 331 :
  332. 332 : this.setApiParam('query', Object.keys(this.magnets))
  333. 333 : this.updateMagnets();
  334. 334 : this.updateDocs();
  335. 335 : this.draw();
  336. 336 : },
  337. 337 : scope: this
  338. 338 : })
  339. 339 : }
  340. 340 : }
  341. 341 : },
  342. 342 :
  343. 343 : updateMagnets: function(canvas) {
  344. 344 : var canvas = this.getTargetEl().dom.querySelector("canvas"), diam = canvas.width, rad = diam /2;
  345. 345 : var len = Object.keys(this.magnets || {}).length;
  346. 346 : var i = 0;
  347. 347 : for (var term in this.magnets) {
  348. 348 : Ext.apply(this.magnets[term], {
  349. 349 : x: rad+((rad-this.gutter-50) * Math.cos(2 * Math.PI * i / len)),
  350. 350 : y: rad+((rad-this.gutter-50) * Math.sin(2 * Math.PI * i / len))
  351. 351 : })
  352. 352 : i++;
  353. 353 : }
  354. 354 : },
  355. 355 :
  356. 356 : updateDocs: function(canvas) {
  357. 357 : canvas = canvas || this.getTargetEl().dom.querySelector("canvas"), diam = canvas.width, rad = diam /2;
  358. 358 : var notMatching = [];
  359. 359 : if (this.documents) {
  360. 360 : this.documents.forEach(function(doc, i) {
  361. 361 : if (Ext.Array.from(doc.matches).length==0) {notMatching.push(i);} // will be set around perimeter below
  362. 362 : else if (Ext.Array.from(doc.matches).length==1) { // try to set it away from magnet
  363. 363 : var x = (Math.random()*15)+15, y = (Math.random()*15)+15;
  364. 364 : doc.targetX = this.magnets[doc.matches[0]].x + (Math.round(Math.random())==0 ? x : -x);
  365. 365 : doc.targetY = this.magnets[doc.matches[0]].y + (Math.round(Math.random())==0 ? y : -y);
  366. 366 : } else {
  367. 367 : // determine the weighted position
  368. 368 : var x = 0, y = 0,
  369. 369 : vals = doc.matches.map(function(term) {return this.magnets[term].record.getDistributions()[i]}, this),
  370. 370 : min = Ext.Array.min(vals), max = Ext.Array.max(vals);
  371. 371 : var weights = 0;
  372. 372 : doc.matches.forEach(function(term, j) {
  373. 373 : weight = max==min ? 1 : ((vals[j]-min)+min)/((max-min)+min);
  374. 374 : weights += weight;
  375. 375 : x += this.magnets[term].x*weight;
  376. 376 : y += this.magnets[term].y*weight;
  377. 377 :
  378. 378 : }, this)
  379. 379 : doc.targetX = x/weights
  380. 380 : doc.targetY = y/weights
  381. 381 : }
  382. 382 : }, this);
  383. 383 :
  384. 384 : // set around perimeter
  385. 385 : for (var i=0, len=notMatching.length; i<len; i++) {
  386. 386 : Ext.apply(this.documents[i], {
  387. 387 : targetX: rad+((rad-this.gutter) * Math.cos(2 * Math.PI * i / len)),
  388. 388 : targetY: rad+((rad-this.gutter) * Math.sin(2 * Math.PI * i / len))
  389. 389 : })
  390. 390 : }
  391. 391 : }
  392. 392 : },
  393. 393 :
  394. 394 : draw: function(canvas, ctx) {
  395. 395 : canvas = canvas || this.getTargetEl().dom.querySelector("canvas");
  396. 396 : ctx = ctx || canvas.getContext("2d");
  397. 397 : ctx.font = this.textFont;
  398. 398 : var radius = canvas.width/2;
  399. 399 : ctx.clearRect(0,0,canvas.width,canvas.height);
  400. 400 : var labels = this.getApiParam('labels');
  401. 401 :
  402. 402 : // draw circle
  403. 403 : ctx.beginPath();
  404. 404 : ctx.strokeStyle = "rgba(0,0,0,.1)"
  405. 405 : ctx.fillStyle = "rgba(0,0,0,.02)"
  406. 406 : ctx.arc(radius, radius, radius-this.gutter, 0, 2 * Math.PI, false);
  407. 407 : ctx.fill();
  408. 408 : ctx.lineWidth = 2;
  409. 409 : ctx.stroke();
  410. 410 :
  411. 411 : // determine if we're animating a move and need to come back
  412. 412 : var needRedraw = false;
  413. 413 :
  414. 414 : // draw documents
  415. 415 :
  416. 416 : if (this.documents && this.documents.length>0) {
  417. 417 : var needMove = false;
  418. 418 :
  419. 419 : var noHovering = Ext.Array.each(this.documents, function(doc) {
  420. 420 : return !doc.isHovering
  421. 421 : }, this);
  422. 422 :
  423. 423 : if (noHovering===true) {
  424. 424 : noHovering = Ext.Array.each(Object.keys(this.magnets || {}), function(term) {
  425. 425 : return !this.magnets[term].isHovering
  426. 426 : }, this);
  427. 427 : }
  428. 428 : // go through a first time to draw connecting lines underneath
  429. 429 : var hoveringTerms = {}; hoveringDocs = [];
  430. 430 : this.documents.forEach(function(document, j) {
  431. 431 : document.matches.forEach(function(term, i) {
  432. 432 : ctx.beginPath();
  433. 433 : ctx.moveTo(document.x, document.y);
  434. 434 : ctx.lineTo(this.magnets[term].x, this.magnets[term].y);
  435. 435 : if (noHovering===true) {
  436. 436 : ctx.strokeStyle = "rgba("+this.magnets[term].colour.join(",")+",.1)";
  437. 437 : } else {
  438. 438 : if (document.isHovering || this.magnets[term].isHovering) {
  439. 439 : hoveringDocs[j]=true;
  440. 440 : hoveringTerms[term]=true;
  441. 441 : ctx.strokeStyle = "rgba("+this.magnets[term].colour.join(",")+",.5)";
  442. 442 : } else {
  443. 443 : ctx.strokeStyle = "rgba(0,0,0,.02)";
  444. 444 : }
  445. 445 : }
  446. 446 : ctx.stroke();
  447. 447 : }, this);
  448. 448 : }, this);
  449. 449 :
  450. 450 : // now a second time for labels/markers
  451. 451 : var halfSize = parseInt(this.textFont)/2, height = parseInt(this.textFont)+4;
  452. 452 : this.documents.forEach(function(document, i) {
  453. 453 :
  454. 454 : // draw marker/label
  455. 455 : if (labels || document.isHovering || hoveringDocs[i]==true) {
  456. 456 : var width = document.width+4;
  457. 457 : ctx.fillStyle = document.isHovering || hoveringDocs[i]==true || noHovering===true ? "white" : "rgba(255,255,255,.05)"
  458. 458 : ctx.fillRect(document.x-(width/2), document.y-(height/2), width, height);
  459. 459 : ctx.strokeStyle = document.isHovering || hoveringDocs[i]==true || noHovering===true ? "rgba(0,0,0,.2)" : "rgba(0,0,0,.05)"
  460. 460 : ctx.strokeRect(document.x-(width/2), document.y-(height/2), width, height);
  461. 461 : ctx.textAlign = "center";
  462. 462 : ctx.fillStyle = document.isHovering || hoveringDocs[i]==true || noHovering===true ? "rgba(0,0,0,.8)" : "rgba(0,0,0,.05)";
  463. 463 : ctx.fillText(document.label, document.x, document.y);
  464. 464 : } else {
  465. 465 : ctx.beginPath();
  466. 466 : ctx.fillStyle = "rgba(0,0,0,.8)"
  467. 467 : ctx.arc(document.x, document.y, halfSize, 0, 2 * Math.PI);
  468. 468 : ctx.fill();
  469. 469 : ctx.stroke();
  470. 470 : }
  471. 471 :
  472. 472 : // determine if we need to move
  473. 473 : var dx = Math.abs(document.x - document.targetX), dy = Math.abs(document.y- document.targetY)
  474. 474 : if (dx!=0 || dy!=0) {
  475. 475 : if (dx<1) {document.x = document.targetX}
  476. 476 : else {
  477. 477 : dx/=2;
  478. 478 : document.x = document.x > document.targetX ? document.x-dx : document.x+dx;
  479. 479 : }
  480. 480 : if (dy<1) {document.y = document.targetY}
  481. 481 : else {
  482. 482 : dy/=2;
  483. 483 : document.y = document.y > document.targetY ? document.y-dy : document.y+dy;
  484. 484 : }
  485. 485 : needRedraw = true;
  486. 486 : }
  487. 487 : }, this);
  488. 488 :
  489. 489 : // now magnets
  490. 490 : var i = 0, height = parseInt(this.textFont)+4;
  491. 491 : ctx.textAlign = "center";
  492. 492 : ctx.textBaseline="middle";
  493. 493 : for (var term in this.magnets) {
  494. 494 : if (labels || term in hoveringTerms || this.magnets[term].isHovering) {
  495. 495 : var width = this.magnets[term].width+4;
  496. 496 : ctx.fillStyle = term in hoveringTerms || this.magnets[term].isHovering || noHovering===true ? "white" : "rgba(255,255,255,.05)";
  497. 497 : ctx.fillRect(this.magnets[term].x-(width/2), this.magnets[term].y-(height/2), width, height);
  498. 498 : ctx.strokeStyle = term in hoveringTerms || this.magnets[term].isHovering || noHovering===true ? "rgb("+this.magnets[term].colour.join(",")+")" : "rgba(0,0,0,.05)";
  499. 499 : ctx.strokeRect(this.magnets[term].x-(width/2), this.magnets[term].y-(height/2), width, height);
  500. 500 : ctx.textAlign = "center";
  501. 501 : ctx.fillStyle = term in hoveringTerms || this.magnets[term].isHovering || noHovering===true ?"rgba(0,0,0,.8)" : "rgba(0,0,0,.05)";
  502. 502 : ctx.fillText(term, this.magnets[term].x, this.magnets[term].y);
  503. 503 : } else {
  504. 504 : ctx.beginPath();
  505. 505 : ctx.fillStyle = "rgb("+this.magnets[term].colour.join(",")+")"
  506. 506 : ctx.strokeStyle = "rgb("+this.magnets[term].colour.join(",")+")"
  507. 507 : ctx.arc(this.magnets[term].x, this.magnets[term].y, 12, 0, 2 * Math.PI);
  508. 508 : ctx.fill();
  509. 509 : ctx.stroke();
  510. 510 : }
  511. 511 : }
  512. 512 : }
  513. 513 :
  514. 514 : if (needRedraw) {
  515. 515 : var me = this;
  516. 516 : setTimeout(function() {
  517. 517 : me.draw();
  518. 518 : }, 100);
  519. 519 : } else if (this.documents) {
  520. 520 : var minDist = Math.max(radius/this.documents.length, 50), spring = .1
  521. 521 : for (var i=0, len=this.documents.length; i<len; i++) {
  522. 522 : for (var j=0; j<len; j++) {
  523. 523 : if (i<j) {
  524. 524 :
  525. 525 : var dx = this.documents[i].x - this.documents[j].x,
  526. 526 : dy = this.documents[i].y - this.documents[j].y,
  527. 527 : dist = Math.sqrt(dx * dx + dy * dy);
  528. 528 : if (dist < minDist) {
  529. 529 : var ax = dx * spring, ay = dy * spring;
  530. 530 : this.documents[i].targetX += ax;
  531. 531 : this.documents[j].targetX -= ax;
  532. 532 : this.documents[i].targetY += ay;
  533. 533 : this.documents[j].targetY -= ay;
  534. 534 : needRedraw = true;
  535. 535 : }
  536. 536 : }
  537. 537 : }
  538. 538 : }
  539. 539 : if (needRedraw) {
  540. 540 : var me = this;
  541. 541 : setTimeout(function() {
  542. 542 : me.draw();
  543. 543 : }, 100);
  544. 544 : }
  545. 545 : }
  546. 546 : }
  547. 547 :
  548. 548 : });