1. 1 : // assuming Bubblelines library is loaded by containing page (via voyant.jsp)
  2. 2 : /**
  3. 3 : * Bubbles is a playful visualization of term frequencies by document.
  4. 4 : *
  5. 5 : * @example
  6. 6 : *
  7. 7 : * let config = {
  8. 8 : * audio: false, // whether or not to include audio
  9. 9 : * docIndex: 1, // document index to restrict to (can be comma-separated list)
  10. 10 : * speed: 10, // speed of the animation (0 to 60 lower is slower)
  11. 11 : * stopList: null, // a named stopword list or comma-separated list of words
  12. 12 : * };
  13. 13 : *
  14. 14 : * loadCorpus("austen").tool("bubbles", config);
  15. 15 : *
  16. 16 : * @class Bubbles
  17. 17 : * @tutorial bubbles
  18. 18 : * @memberof Tools
  19. 19 : */
  20. 20 : Ext.define('Voyant.panel.Bubbles', {
  21. 21 : extend: 'Ext.panel.Panel',
  22. 22 : mixins: ['Voyant.panel.Panel'],
  23. 23 : alias: 'widget.bubbles',
  24. 24 : statics: {
  25. 25 : i18n: {
  26. 26 : },
  27. 27 : api: {
  28. 28 : /**
  29. 29 : * @memberof Tools.Bubbles
  30. 30 : * @instance
  31. 31 : * @property {stopList}
  32. 32 : * @default
  33. 33 : */
  34. 34 : stopList: 'auto',
  35. 35 :
  36. 36 : /**
  37. 37 : * @memberof Tools.Bubbles
  38. 38 : * @instance
  39. 39 : * @property {docIndex}
  40. 40 : */
  41. 41 : docIndex: 0,
  42. 42 :
  43. 43 : /**
  44. 44 : * @memberof Tools.Bubbles
  45. 45 : * @instance
  46. 46 : * @property {limit}
  47. 47 : * @default
  48. 48 : */
  49. 49 : limit: 100,
  50. 50 :
  51. 51 : /**
  52. 52 : * @memberof Tools.Bubbles
  53. 53 : * @instance
  54. 54 : * @property {Boolean} audio Whether or not to play audio
  55. 55 : * @default
  56. 56 : */
  57. 57 : audio: false,
  58. 58 :
  59. 59 : /**
  60. 60 : * @memberof Tools.Bubbles
  61. 61 : * @instance
  62. 62 : * @property {Number} speed How fast to play the visualization
  63. 63 : * @default
  64. 64 : */
  65. 65 : speed: 30
  66. 66 :
  67. 67 :
  68. 68 : },
  69. 69 : glyph: 'xf06e@FontAwesome'
  70. 70 : },
  71. 71 : config: {
  72. 72 : options: {xtype: 'stoplistoption'},
  73. 73 : audio: false
  74. 74 : },
  75. 75 :
  76. 76 : corpusLoaded: false,
  77. 77 : processingLoaded: false,
  78. 78 : bubblesAppCode: undefined,
  79. 79 :
  80. 80 : bubbles: undefined,
  81. 81 : oscillator: undefined,
  82. 82 : gainNode: undefined,
  83. 83 :
  84. 84 :
  85. 85 : constructor: function() {
  86. 86 :
  87. 87 : this.mixins['Voyant.util.Localization'].constructor.apply(this, arguments);
  88. 88 : Ext.apply(this, {
  89. 89 : title: this.localize('title'),
  90. 90 : html: '<canvas style="width: 100%; height: 100%"></canvas>',
  91. 91 : dockedItems: [{
  92. 92 : dock: 'bottom',
  93. 93 : xtype: 'toolbar',
  94. 94 : overflowHandler: 'scroller',
  95. 95 : items: [{
  96. 96 : xtype: 'documentselectorbutton',
  97. 97 : singleSelect: true
  98. 98 : },{
  99. 99 : xtype: 'slider',
  100. 100 : fieldLabel: this.localize('speed'),
  101. 101 : labelAlign: 'right',
  102. 102 : labelWidth: 40,
  103. 103 : width: 100,
  104. 104 : increment: 1,
  105. 105 : minValue: 1,
  106. 106 : maxValue: 60,
  107. 107 : value: 30,
  108. 108 : listeners: {
  109. 109 : render: function(cmp) {
  110. 110 : cmp.setValue(parseInt(this.getApiParam("speed")));
  111. 111 : if (this.bubbles) {this.bubbles.frameRate(cmp.getValue())}
  112. 112 : this.setAudio(cmp.getValue());
  113. 113 : Ext.tip.QuickTipManager.register({
  114. 114 : target: cmp.getEl(),
  115. 115 : text: this.localize('speedTip')
  116. 116 : });
  117. 117 :
  118. 118 : },
  119. 119 : beforedestroy: function(cmp) {
  120. 120 : Ext.tip.QuickTipManager.unregister(cmp.getEl());
  121. 121 : },
  122. 122 : changecomplete: function(cmp, val) {
  123. 123 : this.setApiParam('speed', val);
  124. 124 : if (this.bubbles) {this.bubbles.frameRate(val)}
  125. 125 : },
  126. 126 : scope: this
  127. 127 : }
  128. 128 : },{
  129. 129 : xtype: 'checkbox',
  130. 130 : boxLabel: this.localize('sound'),
  131. 131 : listeners: {
  132. 132 : render: function(cmp) {
  133. 133 : cmp.setValue(this.getApiParam("audio")===true || this.getApiParam("audio")=="true");
  134. 134 : this.setAudio(cmp.getValue());
  135. 135 : Ext.tip.QuickTipManager.register({
  136. 136 : target: cmp.getEl(),
  137. 137 : text: this.localize('soundTip')
  138. 138 : });
  139. 139 :
  140. 140 : },
  141. 141 : beforedestroy: function(cmp) {
  142. 142 : Ext.tip.QuickTipManager.unregister(cmp.getEl());
  143. 143 : },
  144. 144 : change: function(cmp, val) {
  145. 145 : this.setApiParam('audio', val);
  146. 146 : this.setAudio(val);
  147. 147 : },
  148. 148 : scope: this
  149. 149 : }
  150. 150 : },{xtype: 'tbfill'}, {
  151. 151 : xtype: 'tbtext',
  152. 152 : html: this.localize('adaptation') //https://www.m-i-b.com.ar/letters/en/
  153. 153 : }]
  154. 154 : }]
  155. 155 : });
  156. 156 : this.callParent(arguments);
  157. 157 : this.mixins['Voyant.panel.Panel'].constructor.apply(this, arguments);
  158. 158 :
  159. 159 : this.on('boxready', function() {
  160. 160 : this.loadBubbles();
  161. 161 : })
  162. 162 :
  163. 163 : this.on('loadedCorpus', function(src, corpus) {
  164. 164 : this.corpusLoaded = true;
  165. 165 : if (this.bubbles) {
  166. 166 : this.loadDocument();
  167. 167 : } else {
  168. 168 : this.loadBubbles();
  169. 169 : }
  170. 170 : }, this);
  171. 171 :
  172. 172 : this.on("resize", function() {
  173. 173 : if (this.bubbles) {
  174. 174 : this.bubbles.size(this.body.getWidth(),this.body.getHeight());
  175. 175 : }
  176. 176 : });
  177. 177 :
  178. 178 : this.on("documentselected", function(src, doc) {
  179. 179 : this.setApiParam('docIndex', this.getCorpus().getDocument(doc).getIndex());
  180. 180 : this.loadDocument();
  181. 181 : })
  182. 182 : },
  183. 183 :
  184. 184 : setAudio: function(val) {
  185. 185 : if (this.gainNode) {this.gainNode.gain.value=val ? 1 : 0;}
  186. 186 : this.callParent(arguments)
  187. 187 : },
  188. 188 :
  189. 189 : handleCurrentTerm: function(term) {
  190. 190 : if (this.oscillator) {this.oscillator.frequency.value = this.terms[term] ? parseInt((this.terms[term]-this.minFreq) * 2000 / (this.maxFreq-this.minFreq)) : 0;}
  191. 191 : },
  192. 192 :
  193. 193 : handleDocFinished: function() {
  194. 194 : if (this.gainNode) {this.gainNode.gain.value = 0;}
  195. 195 : var index = parseInt(this.getApiParam('docIndex'));
  196. 196 : if (index+1<this.getCorpus().getDocumentsCount()) {
  197. 197 : this.setApiParam('docIndex', index+1);
  198. 198 : this.loadDocument();
  199. 199 : }
  200. 200 : },
  201. 201 :
  202. 202 : loadDocument: function() {
  203. 203 : var me = this, doc = this.getCorpus().getDocument(parseInt(this.getApiParam('docIndex')));
  204. 204 : // if we're not in a tab panel, set the document title as part of the header
  205. 205 : if (!this.up("tabpanel")) {
  206. 206 : this.setTitle(this.localize('title') + " <span class='subtitle'>"+doc.getFullLabel()+"</span>");
  207. 207 : }
  208. 208 :
  209. 209 : doc.loadDocumentTerms(Ext.apply(this.getApiParams(["stopList"]), {
  210. 210 : limit: 100
  211. 211 : })).then(function(documentTerms) {
  212. 212 : me.terms = {};
  213. 213 : documentTerms.each(function(documentTerm) {
  214. 214 : me.terms[documentTerm.getTerm()] = documentTerm.getRawFreq();
  215. 215 : })
  216. 216 : var values = Object.keys(me.terms).map(function(k){return me.terms[k]});
  217. 217 : me.minFreq = Ext.Array.min(values);
  218. 218 : me.maxFreq = Ext.Array.max(values);
  219. 219 : me.getCorpus().loadTokens({whitelist: Object.keys(me.terms), noOthers: true, limit: 0, docIndex: me.getApiParam('docIndex')}).then(function(tokens) {
  220. 220 : var words = [];
  221. 221 : tokens.each(function(token) {
  222. 222 : words.push(token.getTerm().toLowerCase());
  223. 223 : })
  224. 224 : me.bubbles.setLines([doc.getTitle(),words.join(" ")]);
  225. 225 : me.bubbles.loop();
  226. 226 : me.oscillator.frequency.value = 150;
  227. 227 : me.gainNode.gain.value = me.getAudio() ? 1 : 0;
  228. 228 : })
  229. 229 : })
  230. 230 : },
  231. 231 :
  232. 232 : loadBubbles: function() {
  233. 233 : if (this.bubbles === undefined && this.processingLoaded && this.bubblesAppCode !== undefined && this.getTargetEl() !== undefined) {
  234. 234 : var canvas = this.getTargetEl().dom.querySelector('canvas');
  235. 235 : this.bubbles = new Processing(canvas, this.bubblesAppCode);
  236. 236 : this.bubbles.size(this.getTargetEl().getWidth(),this.getTargetEl().getHeight());
  237. 237 : this.bubbles.frameRate(this.getApiParam('speed'));
  238. 238 : this.bubbles.bindJavascript(this);
  239. 239 : this.bubbles.noLoop();
  240. 240 :
  241. 241 : this.bubblesAppCode = undefined;
  242. 242 :
  243. 243 : if (this.corpusLoaded) {
  244. 244 : this.loadDocument();
  245. 245 : }
  246. 246 : }
  247. 247 : },
  248. 248 :
  249. 249 : initComponent: function() {
  250. 250 : // make sure to load script
  251. 251 : Ext.Loader.loadScript({
  252. 252 : url: this.getBaseUrl()+"resources/processingjs/processing.min.js",
  253. 253 : onLoad: function() {
  254. 254 : this.processingLoaded = true;
  255. 255 : this.loadBubbles();
  256. 256 : },
  257. 257 : scope: this
  258. 258 : });
  259. 259 :
  260. 260 : Ext.Ajax.request({
  261. 261 : url: this.getBaseUrl()+'resources/bubbles/bubbles.pjs',
  262. 262 : success: function(data) {
  263. 263 : this.bubblesAppCode = data.responseText;
  264. 264 : this.loadBubbles();
  265. 265 : },
  266. 266 : scope: this
  267. 267 : })
  268. 268 :
  269. 269 : var audioCtx = new (window.AudioContext || window.webkitAudioContext)();
  270. 270 :
  271. 271 : this.oscillator = audioCtx.createOscillator();
  272. 272 : this.gainNode = audioCtx.createGain();
  273. 273 : this.oscillator.connect(this.gainNode);
  274. 274 : this.gainNode.connect(audioCtx.destination);
  275. 275 : this.oscillator.frequency.value = 0;
  276. 276 : this.oscillator.start();
  277. 277 : this.gainNode.gain.value = 0;
  278. 278 :
  279. 279 : this.callParent(arguments);
  280. 280 : }
  281. 281 : });