1. 1 : /**
  2. 2 : * Microsearch visualizes the frequency and distribution of terms in a corpus.
  3. 3 : *
  4. 4 : * @example
  5. 5 : *
  6. 6 : * let config = {
  7. 7 : * "query": null,
  8. 8 : * "stopList": "auto"
  9. 9 : * };
  10. 10 : *
  11. 11 : * loadCorpus("austen").tool("microsearch", config);
  12. 12 : *
  13. 13 : * @class MicroSearch
  14. 14 : * @tutorial microsearch
  15. 15 : * @memberof Tools
  16. 16 : */
  17. 17 : Ext.define('Voyant.panel.MicroSearch', {
  18. 18 : extend: 'Ext.panel.Panel',
  19. 19 : mixins: ['Voyant.panel.Panel'],
  20. 20 : alias: 'widget.microsearch',
  21. 21 : statics: {
  22. 22 : i18n: {
  23. 23 : },
  24. 24 : api: {
  25. 25 : /**
  26. 26 : * @memberof Tools.MicroSearch
  27. 27 : * @instance
  28. 28 : * @property {stopList}
  29. 29 : * @default
  30. 30 : */
  31. 31 : stopList: 'auto',
  32. 32 :
  33. 33 : /**
  34. 34 : * @memberof Tools.MicroSearch
  35. 35 : * @instance
  36. 36 : * @property {query}
  37. 37 : */
  38. 38 : query: undefined
  39. 39 : },
  40. 40 : glyph: 'xf1ea@FontAwesome'
  41. 41 : },
  42. 42 : config: {
  43. 43 : /**
  44. 44 : * @private
  45. 45 : */
  46. 46 : options: [{xtype: 'stoplistoption'},{xtype: 'categoriesoption'}],
  47. 47 :
  48. 48 : /**
  49. 49 : * @private
  50. 50 : */
  51. 51 : maxTokens: 0,
  52. 52 :
  53. 53 : /**
  54. 54 : * @private
  55. 55 : */
  56. 56 : tokensPerSegment: 0,
  57. 57 :
  58. 58 : /**
  59. 59 : * @private
  60. 60 : */
  61. 61 : maxVerticalLines: 0,
  62. 62 :
  63. 63 : /**
  64. 64 : * @private
  65. 65 : */
  66. 66 : maxSegments: 0
  67. 67 : },
  68. 68 : constructor: function(config ) {
  69. 69 :
  70. 70 : Ext.apply(this, {
  71. 71 : title: this.localize('title'),
  72. 72 : dockedItems: [{
  73. 73 : dock: 'bottom',
  74. 74 : xtype: 'toolbar',
  75. 75 : overflowHandler: 'scroller',
  76. 76 : items: [{
  77. 77 : xtype: 'querysearchfield'
  78. 78 : }]
  79. 79 : }]
  80. 80 : });
  81. 81 :
  82. 82 : this.callParent(arguments);
  83. 83 : this.mixins['Voyant.panel.Panel'].constructor.apply(this, arguments);
  84. 84 :
  85. 85 : // create a listener for corpus loading (defined here, in case we need to load it next)
  86. 86 : this.on('loadedCorpus', function(src, corpus) {
  87. 87 : if (this.rendered) {
  88. 88 : this.initialize();
  89. 89 : }
  90. 90 : else {
  91. 91 : this.on("afterrender", function() {
  92. 92 : this.initialize();
  93. 93 : }, this)
  94. 94 : }
  95. 95 :
  96. 96 : });
  97. 97 :
  98. 98 : this.on('query', function(src, query) {
  99. 99 : this.setApiParam('query', query);
  100. 100 : this.updateSearchResults();
  101. 101 : })
  102. 102 :
  103. 103 : },
  104. 104 :
  105. 105 : initialize: function() {
  106. 106 :
  107. 107 : var el = this.getTargetEl(), corpus = this.getCorpus();
  108. 108 :
  109. 109 : var lineSize = 5; // pixels, including margins below and above
  110. 110 : this.setMaxVerticalLines(Math.floor((el.getHeight() - 10 /* margin of 5px */) / lineSize));
  111. 111 :
  112. 112 : // max segments
  113. 113 : var gutterSize = 10;
  114. 114 : var corpusSize = corpus.getDocumentsCount();
  115. 115 : var gutter = corpusSize * gutterSize;
  116. 116 : var columnSize = Math.floor((el.getWidth() - gutter - 10 /* margin of 5px */) / corpusSize);
  117. 117 : if (columnSize>200) {columnSize=200;}
  118. 118 : var segmentWidth = 3; // each segment is 3 pixels
  119. 119 : var maxSegmentsPerLine = Math.floor(columnSize / segmentWidth);
  120. 120 : if (maxSegmentsPerLine<1) {maxSegmentsPerLine=1;}
  121. 121 :
  122. 122 : // and the answer is...
  123. 123 : this.setMaxSegments(maxSegmentsPerLine * this.getMaxVerticalLines());
  124. 124 :
  125. 125 : var documentsStore = corpus.getDocuments();
  126. 126 : this.setMaxTokens(documentsStore.max('tokensCount-lexical'));
  127. 127 :
  128. 128 : this.setTokensPerSegment(this.getMaxTokens() < this.getMaxSegments() ? 1 : Math.ceil(this.getMaxTokens()/this.getMaxSegments()));
  129. 129 :
  130. 130 :
  131. 131 : var canvas = "<table cellpadding='0' cellspacing='0' style='height: 100%'><tr>";
  132. 132 : this.segments = [];
  133. 133 : documentsStore.each(function(document) {
  134. 134 : docIndex = document.getIndex();
  135. 135 : canvas+='<td style="overflow: hidden; vertical-align: top; width: '+columnSize+'px;">'+
  136. 136 : '<div class="docLabel" style="white-space: nowrap; width: '+columnSize+'px;" data-qtip="'+document.getFullLabel()+'">'+document.getFullLabel()+"</div>"+
  137. 137 : '<canvas style="display: block;" width="'+columnSize+'" height="'+el.getHeight()+'" id="'+this.body.id+'-'+docIndex+'">'+
  138. 138 : '</td>';
  139. 139 : if (docIndex+1<corpusSize) {canvas+='<td style="width: '+gutterSize+'px;">&nbsp;</td>';}
  140. 140 : }, this);
  141. 141 : canvas+='</tr></table>';
  142. 142 : el.update(canvas);
  143. 143 :
  144. 144 : this.updateSearchResults();
  145. 145 :
  146. 146 : if (!this.getApiParam('query')) {
  147. 147 : var me = this;
  148. 148 : return this.getCorpus().loadCorpusTerms({limit: 1, stopList: this.getApiParam('stopList'), categories: this.getApiParam("categories")}).then(function(corpusTerms) {
  149. 149 : var term = corpusTerms.getAt(0).getTerm();
  150. 150 : var q = me.down('querysearchfield');
  151. 151 : q.addValue(new Voyant.data.model.CorpusTerm({term: term}));
  152. 152 : me.fireEvent("query", me, [term])
  153. 153 : });
  154. 154 : }
  155. 155 :
  156. 156 : },
  157. 157 :
  158. 158 : updateSearchResults: function() {
  159. 159 : query = this.getApiParam('query');
  160. 160 :
  161. 161 : // draw background
  162. 162 : this.getCorpus().getDocuments().each(function(document) {
  163. 163 : var distributions = this.redistributeDistributions(document, new Array(this.getMaxSegments()));
  164. 164 : this.drawDocumentDistributions(document, null, distributions);
  165. 165 : }, this);
  166. 166 :
  167. 167 : if (Ext.Array.from(query).length > 0) {
  168. 168 : this.mask(this.localize('loading'))
  169. 169 : this.getCorpus().getDocumentTerms().load({
  170. 170 : params: {
  171. 171 : query: Ext.Array.from(query),
  172. 172 : withDistributions: 'relative',
  173. 173 : bins: this.getMaxSegments(),
  174. 174 : categories: this.getApiParam('categories')
  175. 175 : },
  176. 176 : callback: function(records, operation, success) {
  177. 177 : this.unmask();
  178. 178 : var max = 0;
  179. 179 : var min = Number.MAX_VALUE;
  180. 180 : var docs = [];
  181. 181 : records.forEach(function(record) {
  182. 182 : var doc = this.getCorpus().getDocument(record.getDocIndex());
  183. 183 : var distributions = this.redistributeDistributions(doc, record.getDistributions());
  184. 184 : var m = Ext.Array.max(distributions);
  185. 185 : if (m>max) {max=m;}
  186. 186 : distributions.forEach(function(d) {
  187. 187 : if (d && d<min) {
  188. 188 : min = d;
  189. 189 : }
  190. 190 : })
  191. 191 : if (docs[record.getDocIndex()] === undefined) {
  192. 192 : docs[record.getDocIndex()] = {};
  193. 193 : }
  194. 194 : docs[record.getDocIndex()][record.getTerm()] = this.redistributeDistributions(doc, record.getDistributions());
  195. 195 : }, this);
  196. 196 : docs.forEach(function(termDistributions, i) {
  197. 197 : for (var term in termDistributions) {
  198. 198 : var distributions = termDistributions[term];
  199. 199 : this.drawDocumentDistributions(this.getCorpus().getDocument(i), term, distributions, min || Ext.Array.min(distributions), max || Ext.Array.max(distributions));
  200. 200 : }
  201. 201 : }, this)
  202. 202 : },
  203. 203 : scope: this
  204. 204 : })
  205. 205 : }
  206. 206 : },
  207. 207 :
  208. 208 : redistributeDistributions: function(doc, distributions) {
  209. 209 : var segments = Math.ceil(doc.getLexicalTokensCount() / this.getTokensPerSegment());
  210. 210 :
  211. 211 : // redistribute if needed, we'll take the mean of the distribution values to maintain comparison across segments
  212. 212 : if (distributions.length>segments) {
  213. 213 : var newdistributions = [];
  214. 214 : for (var i=0; i<distributions.length; i++) {
  215. 215 : var a = parseInt(i*segments/distributions.length);
  216. 216 : if (newdistributions[a]) {newdistributions[a].push(distributions[i])}
  217. 217 : else {newdistributions[a]=[distributions[i]];}
  218. 218 : }
  219. 219 : distributions = newdistributions
  220. 220 : for (var i=0; i<distributions.length; i++) {
  221. 221 : distributions[i] = Ext.Array.mean(distributions[i]);
  222. 222 : }
  223. 223 : }
  224. 224 : return distributions;
  225. 225 : },
  226. 226 :
  227. 227 : drawDocumentDistributions: function(doc, term, distributions, min, max) {
  228. 228 : var canvas = this.getTargetEl().dom.querySelector("#"+this.body.id+"-"+doc.getIndex());
  229. 229 : var c = canvas.getContext('2d');
  230. 230 : var x = 0, w = canvas.clientWidth, y = 0;
  231. 231 :
  232. 232 : var isBlank = term === null;
  233. 233 : var color = [230, 230, 230];
  234. 234 : if (!isBlank) {
  235. 235 : color = this.getApplication().getColorForTerm(term);
  236. 236 : }
  237. 237 :
  238. 238 : for (var j=0; j<distributions.length;j++) {
  239. 239 : if (isBlank) {
  240. 240 : c.fillStyle = "rgb(230,230,230)";
  241. 241 : c.fillRect(x,y,3,3);
  242. 242 : } else if (distributions[j]) {
  243. 243 : var alpha = ((distributions[j]-min)*.7/(max-min))+.3;
  244. 244 : c.fillStyle = "rgba("+color[0]+","+color[1]+","+color[2]+","+alpha+")";
  245. 245 : c.fillRect(x,y,3,3);
  246. 246 : }
  247. 247 : x+=3;
  248. 248 : if (x>=w) {x=0; y+=5}
  249. 249 : }
  250. 250 :
  251. 251 : }
  252. 252 : });