word_cloud.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309
  1. /**
  2. * QUERY SECTION
  3. *
  4. * */
  5. // SPARQL Query text -- 'prefixes' and 'thisUrlParams' defined in people.js;
  6. let queryNetwork = prefixes + " SELECT COUNT(?event1) AS ?count1 COUNT(?event2) AS ?count2 ?uri2 SAMPLE(?label2) AS ?text \
  7. WHERE { \
  8. {?event1 rdf:type crm:EL1_Exchange_Letters . \
  9. ?event_to rdfs:subClassOf ?event1; \
  10. rdf:type crm:EL2_Send_Letter ; \
  11. crm:P01_has_domain ?pc_to . \
  12. ?pc_to crm:P02_has_range ?uri . \
  13. ?uri rdfs:label ?label . \
  14. ?event_from rdfs:subClassOf ?event1; \
  15. rdf:type crm:EL3_Receive_Letter; \
  16. crm:P01_has_domain ?pc_from . \
  17. ?pc_from crm:P02_has_range ?uri2 . \
  18. ?uri2 rdfs:label ?label2 . \
  19. FILTER (?uri = <" + thisUrlParams.link + ">) \
  20. } UNION { \
  21. ?event2 rdf:type crm:EL1_Exchange_Letters . \
  22. ?event_to rdfs:subClassOf ?event2; \
  23. rdf:type crm:EL3_Receive_Letter ; \
  24. crm:P01_has_domain ?pc_from . \
  25. ?pc_from crm:P02_has_range ?uri . \
  26. ?uri rdfs:label ?label . \
  27. ?event_from rdfs:subClassOf ?event2; \
  28. rdf:type crm:EL2_Send_Letter; \
  29. crm:P01_has_domain ?pc_to . \
  30. ?pc_to crm:P02_has_range ?uri2 . \
  31. ?uri2 rdfs:label ?label2 . \
  32. FILTER (?uri = <" + thisUrlParams.link + ">) \
  33. } \
  34. }"
  35. // 'prepareQueryURL' defined in people.js
  36. let queryNet = prepareQueryURL(queryNetwork);
  37. // do the query, call data processor on successful output
  38. $.ajax({
  39. url: queryNet,
  40. dataType: "json",
  41. success: (data) => handle_word_network(data)
  42. });
  43. /**
  44. * COMPONENT CREATION SECTION
  45. *
  46. * */
  47. // Master function, takes data output from the query and creates the HTML for the Word Cloud component of the page
  48. function handle_word_network(json){
  49. // Pre-process data
  50. let words = processQuery(json);
  51. // Create page elements
  52. doListPersonNetwork(words);
  53. doWordCloud(words);
  54. }
  55. // Return structured object from raw query data for further processing
  56. function processQuery(json) {
  57. const tempArray = [];
  58. $.each(
  59. json['results']['bindings'],
  60. (index, value) => {
  61. let text = value['text']['value'];
  62. let link = value['uri2']['value'];
  63. let sent = parseInt(value['count1']['value']);
  64. let received = parseInt(value['count2']['value']);
  65. let count = sent + received;
  66. tempArray.push({'text': text, 'sent': sent, 'received': received, 'count': count, 'hlink': link});
  67. }
  68. );
  69. tempArray.sort((a, b) => b.count - a.count);
  70. return tempArray;
  71. }
  72. // Create formatted list of people in the network
  73. function doListPersonNetwork(tempArray){
  74. let ArrayNames = "";
  75. tempArray.forEach(element => {
  76. ArrayNames +=
  77. "<div class='item-place-person'>\
  78. <div class='item-place-person-label'>" +
  79. element['text'] +
  80. "<br /><span class='num_occ'>[Lettere inviate: " + element['sent'] + "]</span>\
  81. <br /><span class='num_occ'>[Lettere ricevute: " + element['received'] + "]</span>\
  82. <br /><span class='num_occ'>[Totale corrispondenza: " + element['count'] + "]</span>\
  83. </div>\
  84. <div class='item-place-person-action'>\
  85. <div class='persona' id='" + element['hlink'] +"'>\
  86. <i class='fa fa-user' style='cursor:pointer'></i>\
  87. </div>\
  88. </div>\
  89. </div>";
  90. });
  91. document.getElementById("list_person_network").innerHTML = ArrayNames;
  92. }
  93. // Create the word cloud
  94. function doWordCloud(words){
  95. // Clear word cloud div
  96. $('#myWordCloud').empty();
  97. // OVERALL GRAPHIC SETTINGS for the cloud container -- id, dimensions, position
  98. let width = 0.5*window.outerWidth;
  99. let height = 0.6*width;
  100. // append the svg object to the body of the page
  101. let svg = d3.select("#myWordCloud")
  102. .append("svg")
  103. .attr("id", "wordcloudNetwork")
  104. .attr("preserveAspectRatio", "xMinYMin meet")
  105. .attr("viewBox", "0 0 " + width + " " + height+'"')
  106. .classed("svg-content", true)
  107. .attr("width", width)
  108. .attr("height", height)
  109. .attr("style", "border:1px solid black")
  110. // In case of empty cloud, draw special page and exit
  111. if (words.length == 0){
  112. drawEmptyWordCloud();
  113. return;
  114. }
  115. // THE ACTUAL CLOUD
  116. svg = document.getElementById('wordcloudNetwork');
  117. console.log(svg);
  118. let zoom = window.devicePixelRatio;
  119. console.log('zoom', zoom);
  120. wcParameters = {
  121. svgWidth: svg.getBoundingClientRect().width,
  122. svgHeight: svg.getBoundingClientRect().height,
  123. maxFontSize: 30/((1+zoom)/2),
  124. minFontSize: 10/((1+zoom)/2)
  125. }
  126. let svgWords = hackWords(words, wcParameters);
  127. svg.innerHTML = svgWords;
  128. let wordCloudText = createWordCloud(words, wcParameters);
  129. svg.innerHTML = wordCloudText;
  130. }
  131. // Helper function -- draw special empty word cloud if there are no occurrences
  132. function drawEmptyWordCloud(){
  133. let wordIcon = "<div id='users_icon' class='no_info_icon'> \
  134. <svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 640 512'> \
  135. <!--! Font Awesome Pro 6.1.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path d='M319.9 320c57.41 0 103.1-46.56 103.1-104c0-57.44-46.54-104-103.1-104c-57.41 0-103.1 46.56-103.1 104C215.9 273.4 262.5 320 319.9 320zM369.9 352H270.1C191.6 352 128 411.7 128 485.3C128 500.1 140.7 512 156.4 512h327.2C499.3 512 512 500.1 512 485.3C512 411.7 448.4 352 369.9 352zM512 160c44.18 0 80-35.82 80-80S556.2 0 512 0c-44.18 0-80 35.82-80 80S467.8 160 512 160zM183.9 216c0-5.449 .9824-10.63 1.609-15.91C174.6 194.1 162.6 192 149.9 192H88.08C39.44 192 0 233.8 0 285.3C0 295.6 7.887 304 17.62 304h199.5C196.7 280.2 183.9 249.7 183.9 216zM128 160c44.18 0 80-35.82 80-80S172.2 0 128 0C83.82 0 48 35.82 48 80S83.82 160 128 160zM551.9 192h-61.84c-12.8 0-24.88 3.037-35.86 8.24C454.8 205.5 455.8 210.6 455.8 216c0 33.71-12.78 64.21-33.16 88h199.7C632.1 304 640 295.6 640 285.3C640 233.8 600.6 192 551.9 192z'/> \
  136. </svg> \
  137. <p>Nessuna persona trovata</p> \
  138. </div>";
  139. $("#wordcloudNetwork").css("display", "none");
  140. $("#references_network").css("display", "none");
  141. document.getElementById("myWordCloud").innerHTML = wordIcon;
  142. }
  143. /*****************************
  144. * Customized word cloud code *
  145. *****************************/
  146. function hackWords(words, parameters){
  147. let weights = words.map(word => word.count);
  148. let minWeight = Math.min(...weights);
  149. let maxFontSize = parameters.maxFontSize;
  150. let minFontSize = parameters.minFontSize;
  151. console.log('weights', weights);
  152. console.log('maxFontSize', maxFontSize);
  153. console.log('minFontSize',minFontSize);
  154. let stringOfWords = "";
  155. for(let ind=0; ind<words.length; ind++){
  156. let word = words[ind];
  157. word.fontSize = getFontSize(words[ind].count, minWeight, maxFontSize, minFontSize);
  158. stringOfWords = stringOfWords+
  159. '<text x="100" y ="' + (maxFontSize + maxFontSize*ind).toString() + '" + font-family="Verdana" font-size="'+(word.fontSize)+'" id="word' + ind + '">'+words[ind].text+'</text>\n';
  160. }
  161. return stringOfWords;
  162. }
  163. // Takes a list of words, returns the innerHTML for a Word Cloud as a string containing multiple <g> svg elements
  164. function createWordCloud(words, parameters){
  165. let actualUsefulWidth = parameters.svgWidth;
  166. let actualUsefulHeight = parameters.svgHeight;
  167. console.log('Actual SVG dimensions', actualUsefulWidth, actualUsefulHeight);
  168. maxFontSize = parameters.maxFontSize;
  169. minFontSize = parameters.minFontSize;
  170. const rectObjs = []
  171. let allBooms = 0
  172. let attempts = 0;
  173. let outOfBorder = 0;
  174. for(let ind=0;ind<words.length;ind++){
  175. //for(let ind=0;ind<3;ind++){
  176. // Control vars
  177. if(allBooms>=50*(ind+1)) break;
  178. attempts++;
  179. let boom = false;
  180. // word + bounding box parameters definition
  181. let word = words[ind];
  182. let id0 = "word"+ind;
  183. let wordBB = document.getElementById(id0).getBoundingClientRect();
  184. let fontSize = word.fontSize;
  185. let width = wordBB.width*1;
  186. let height = wordBB.height*1;
  187. let multiplier = 0.1 + 0.4*(maxFontSize-fontSize)/(maxFontSize-minFontSize);
  188. let xR = Math.floor(actualUsefulWidth/2 + randomRange(-multiplier*actualUsefulWidth, multiplier*actualUsefulWidth));
  189. let yR = Math.floor(actualUsefulHeight/2 + randomRange(-multiplier*actualUsefulHeight, multiplier*actualUsefulHeight));
  190. //console.log('x', 'y', xR, yR)
  191. let angle;
  192. if(word.received/word.sent>3) angle = -3;
  193. else if(word.received/word.sent<0.3) angle = 3;
  194. let text = word.text;
  195. //console.log('Rect:', width, height, xR, yR, angle, text);
  196. //
  197. let newRect = new Rect(width, height, xR, yR, angle, text, fontSize);
  198. boom = checkBorder(newRect, 0, actualUsefulWidth, 0, actualUsefulHeight)
  199. if(boom){
  200. outOfBorder++;
  201. if(outOfBorder<30){
  202. ind--;
  203. continue;
  204. } else{
  205. boom = false;
  206. }
  207. }
  208. for(let rect of rectObjs){
  209. boom = checkColl(newRect, rect);
  210. if(boom) break;
  211. }
  212. if(boom){
  213. allBooms++;
  214. ind--;
  215. continue;
  216. }
  217. // allBooms = 0;
  218. rectObjs.push(newRect);
  219. }
  220. console.log('Attempted:', attempts);
  221. console.log('Placed:', rectObjs.length);
  222. const rectTexts = [];
  223. rectObjs.forEach(rect => rectTexts.push(rect.printCoords(false, maxFontSize, minFontSize)));
  224. let rects = rectTexts.join('\n');
  225. return rects;
  226. }
  227. function getFontSize(weight, minWeight, maxFontSize, minFontSize){
  228. let r1 = weight/10*minWeight - 1/10; // between 0 and a big num
  229. let res = minFontSize + (maxFontSize-minFontSize)*r1/Math.sqrt(1+r1**2);
  230. return res;
  231. }
  232. function randomRange(a, b=0){
  233. return (a-b)*Math.random()+b;
  234. }
  235. function randomInt(a){
  236. return Math.floor(a*Math.random());
  237. }
  238. function randomRangeInt(a, b=0){
  239. let ap = Math.floor(a);
  240. let bp = Math.floor(b);
  241. return Math.floor((ap-bp)*Math.random())+bp;
  242. }
  243. // Check border collision
  244. function checkBorder(rect, minX, maxX, minY, maxY){
  245. if(rect.minX<minX) return true;
  246. if(rect.maxX>maxX) return true;
  247. if(rect.minY<minY) return true;
  248. if(rect.maxY>maxY) return true;
  249. return false;
  250. }