├── .eslintrc.js ├── .eslintrc.yaml ├── .gitignore ├── .npmignore ├── .prettierignore ├── .prettierrc ├── README.md ├── dist ├── echarts-wordcloud.js ├── echarts-wordcloud.js.map ├── echarts-wordcloud.min.js ├── echarts-wordcloud.min.js.LICENSE.txt └── echarts-wordcloud.min.js.map ├── example ├── logo.png ├── optionKeywords.html ├── test.html ├── typeTest.ts ├── word-cloud.png └── wordCloud.html ├── index.d.ts ├── index.js ├── package-lock.json ├── package.json ├── src ├── WordCloudSeries.js ├── WordCloudView.js ├── layout.js └── wordCloud.js └── webpack.config.js /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | browser: true, 5 | node: true 6 | }, 7 | extends: ['prettier'], 8 | plugins: ['prettier'], 9 | // add your custom rules here 10 | rules: { 11 | 'space-before-function-paren': 0 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /.eslintrc.yaml: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | browser: true, 5 | node: true 6 | }, 7 | extends: ['prettier'], 8 | plugins: ['prettier'], 9 | // add your custom rules here 10 | rules: { 11 | 'space-before-function-paren': 0 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | 4 | npm-debug.log 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /build 2 | /example 3 | npm-debug.log -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | example 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "semi": true, 4 | "singleQuote": true, 5 | "trailingComma": "none", 6 | "printWidth": 80 7 | } 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # echarts-wordcloud 2 | 3 | Third-party Wordcloud extension based on [wordcloud2.js](https://github.com/timdream/wordcloud2.js) for [Apache ECharts](https://github.com/apache/echarts). 4 | 5 | ![](./example/word-cloud.png) 6 | 7 | ## Examples 8 | 9 | [Google Trends](https://ecomfe.github.io/echarts-wordcloud/example/wordCloud.html) 10 | 11 | [ECharts Option Keywords](https://ecomfe.github.io/echarts-wordcloud/example/optionKeywords.html) 12 | 13 | ## Install 14 | 15 | ```html 16 | 17 | 18 | ``` 19 | 20 | Or 21 | 22 | ```shell 23 | npm install echarts 24 | npm install echarts-wordcloud 25 | ``` 26 | 27 | ```js 28 | import * as echarts from 'echarts'; 29 | import 'echarts-wordcloud'; 30 | ``` 31 | 32 | ⚠️ NOTE: 33 | 34 | echarts-wordcloud@2 is for echarts@5 35 | 36 | echarts-wordcloud@1 is for echarts@4 37 | 38 | ## Usage 39 | 40 | ```js 41 | var chart = echarts.init(document.getElementById('main')); 42 | 43 | chart.setOption({ 44 | ... 45 | series: [{ 46 | type: 'wordCloud', 47 | 48 | // The shape of the "cloud" to draw. Can be any polar equation represented as a 49 | // callback function, or a keyword present. Available presents are circle (default), 50 | // cardioid (apple or heart shape curve, the most known polar equation), diamond ( 51 | // alias of square), triangle-forward, triangle, (alias of triangle-upright, pentagon, and star. 52 | 53 | shape: 'circle', 54 | 55 | // Keep aspect ratio of maskImage or 1:1 for shapes 56 | // This option is supported since echarts-wordcloud@2.1.0 57 | keepAspect: false, 58 | 59 | // A silhouette image which the white area will be excluded from drawing texts. 60 | // The shape option will continue to apply as the shape of the cloud to grow. 61 | 62 | maskImage: maskImage, 63 | 64 | // Folllowing left/top/width/height/right/bottom are used for positioning the word cloud 65 | // Default to be put in the center and has 75% x 80% size. 66 | 67 | left: 'center', 68 | top: 'center', 69 | width: '70%', 70 | height: '80%', 71 | right: null, 72 | bottom: null, 73 | 74 | // Text size range which the value in data will be mapped to. 75 | // Default to have minimum 12px and maximum 60px size. 76 | 77 | sizeRange: [12, 60], 78 | 79 | // Text rotation range and step in degree. Text will be rotated randomly in range [-90, 90] by rotationStep 45 80 | 81 | rotationRange: [-90, 90], 82 | rotationStep: 45, 83 | 84 | // size of the grid in pixels for marking the availability of the canvas 85 | // the larger the grid size, the bigger the gap between words. 86 | 87 | gridSize: 8, 88 | 89 | // set to true to allow word to be drawn partly outside of the canvas. 90 | // Allow word bigger than the size of the canvas to be drawn 91 | // This option is supported since echarts-wordcloud@2.1.0 92 | drawOutOfBound: false, 93 | 94 | // if the font size is too large for the text to be displayed, 95 | // whether to shrink the text. If it is set to false, the text will 96 | // not be rendered. If it is set to true, the text will be shrinked. 97 | // This option is supported since echarts-wordcloud@2.1.0 98 | shrinkToFit: false, 99 | 100 | // If perform layout animation. 101 | // NOTE disable it will lead to UI blocking when there is lots of words. 102 | layoutAnimation: true, 103 | 104 | // Global text style 105 | textStyle: { 106 | fontFamily: 'sans-serif', 107 | fontWeight: 'bold', 108 | // Color can be a callback function or a color string 109 | color: function () { 110 | // Random color 111 | return 'rgb(' + [ 112 | Math.round(Math.random() * 160), 113 | Math.round(Math.random() * 160), 114 | Math.round(Math.random() * 160) 115 | ].join(',') + ')'; 116 | } 117 | }, 118 | emphasis: { 119 | focus: 'self', 120 | 121 | textStyle: { 122 | textShadowBlur: 10, 123 | textShadowColor: '#333' 124 | } 125 | }, 126 | 127 | // Data is an array. Each array item must have name and value property. 128 | data: [{ 129 | name: 'Farrah Abraham', 130 | value: 366, 131 | // Style of single text 132 | textStyle: { 133 | } 134 | }] 135 | }] 136 | }); 137 | ``` 138 | 139 | ## Changelog 140 | 141 | ### 2.1.0 142 | 143 | - [chore] Update Apache ECharts version 144 | - [chore] Sync with the latest wordcloud2.js and use prettier to format the code 145 | - [feature] Add `keepAspect` option to keep aspect ratio of maskImage or 1:1 for shapes 146 | - [feature] Add `drawOutOfBound` option to allow words to be drawn partly outside of the canvas 147 | - [feature] Add `shrinkToFit` option to shrink the text if the font size is too large for the text to be displayed 148 | 149 | ## Notice 150 | 151 | The Apache Software Foundation [Apache ECharts, ECharts](https://echarts.apache.org/), Apache, the Apache feather, and the Apache ECharts project logo are either registered trademarks or trademarks of the [Apache Software Foundation](https://www.apache.org/). 152 | -------------------------------------------------------------------------------- /dist/echarts-wordcloud.js: -------------------------------------------------------------------------------- 1 | (function webpackUniversalModuleDefinition(root, factory) { 2 | if(typeof exports === 'object' && typeof module === 'object') 3 | module.exports = factory(require("echarts")); 4 | else if(typeof define === 'function' && define.amd) 5 | define(["echarts"], factory); 6 | else if(typeof exports === 'object') 7 | exports["echarts-wordcloud"] = factory(require("echarts")); 8 | else 9 | root["echarts-wordcloud"] = factory(root["echarts"]); 10 | })(self, function(__WEBPACK_EXTERNAL_MODULE_echarts_lib_echarts__) { 11 | return /******/ (() => { // webpackBootstrap 12 | /******/ "use strict"; 13 | /******/ var __webpack_modules__ = ({ 14 | 15 | /***/ "./index.js": 16 | /*!******************************!*\ 17 | !*** ./index.js + 4 modules ***! 18 | \******************************/ 19 | /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { 20 | 21 | // ESM COMPAT FLAG 22 | __webpack_require__.r(__webpack_exports__); 23 | 24 | // EXTERNAL MODULE: external "echarts" 25 | var external_echarts_ = __webpack_require__("echarts/lib/echarts"); 26 | ;// CONCATENATED MODULE: ./src/WordCloudSeries.js 27 | 28 | 29 | external_echarts_.extendSeriesModel({ 30 | type: 'series.wordCloud', 31 | 32 | visualStyleAccessPath: 'textStyle', 33 | visualStyleMapper: function (model) { 34 | return { 35 | fill: model.get('color') 36 | }; 37 | }, 38 | visualDrawType: 'fill', 39 | 40 | optionUpdated: function () { 41 | var option = this.option; 42 | option.gridSize = Math.max(Math.floor(option.gridSize), 4); 43 | }, 44 | 45 | getInitialData: function (option, ecModel) { 46 | var dimensions = external_echarts_.helper.createDimensions(option.data, { 47 | coordDimensions: ['value'] 48 | }); 49 | var list = new external_echarts_.List(dimensions, this); 50 | list.initData(option.data); 51 | return list; 52 | }, 53 | 54 | // Most of options are from https://github.com/timdream/wordcloud2.js/blob/gh-pages/API.md 55 | defaultOption: { 56 | maskImage: null, 57 | 58 | // Shape can be 'circle', 'cardioid', 'diamond', 'triangle-forward', 'triangle', 'pentagon', 'star' 59 | shape: 'circle', 60 | keepAspect: false, 61 | 62 | left: 'center', 63 | 64 | top: 'center', 65 | 66 | width: '70%', 67 | 68 | height: '80%', 69 | 70 | sizeRange: [12, 60], 71 | 72 | rotationRange: [-90, 90], 73 | 74 | rotationStep: 45, 75 | 76 | gridSize: 8, 77 | 78 | drawOutOfBound: false, 79 | shrinkToFit: false, 80 | 81 | textStyle: { 82 | fontWeight: 'normal' 83 | } 84 | } 85 | }); 86 | 87 | ;// CONCATENATED MODULE: ./src/WordCloudView.js 88 | 89 | 90 | external_echarts_.extendChartView({ 91 | type: 'wordCloud', 92 | 93 | render: function (seriesModel, ecModel, api) { 94 | var group = this.group; 95 | group.removeAll(); 96 | 97 | var data = seriesModel.getData(); 98 | 99 | var gridSize = seriesModel.get('gridSize'); 100 | 101 | seriesModel.layoutInstance.ondraw = function (text, size, dataIdx, drawn) { 102 | var itemModel = data.getItemModel(dataIdx); 103 | var textStyleModel = itemModel.getModel('textStyle'); 104 | 105 | var textEl = new external_echarts_.graphic.Text({ 106 | style: external_echarts_.helper.createTextStyle(textStyleModel), 107 | scaleX: 1 / drawn.info.mu, 108 | scaleY: 1 / drawn.info.mu, 109 | x: (drawn.gx + drawn.info.gw / 2) * gridSize, 110 | y: (drawn.gy + drawn.info.gh / 2) * gridSize, 111 | rotation: drawn.rot 112 | }); 113 | textEl.setStyle({ 114 | x: drawn.info.fillTextOffsetX, 115 | y: drawn.info.fillTextOffsetY + size * 0.5, 116 | text: text, 117 | verticalAlign: 'middle', 118 | fill: data.getItemVisual(dataIdx, 'style').fill, 119 | fontSize: size 120 | }); 121 | 122 | group.add(textEl); 123 | 124 | data.setItemGraphicEl(dataIdx, textEl); 125 | 126 | textEl.ensureState('emphasis').style = external_echarts_.helper.createTextStyle( 127 | itemModel.getModel(['emphasis', 'textStyle']), 128 | { 129 | state: 'emphasis' 130 | } 131 | ); 132 | textEl.ensureState('blur').style = external_echarts_.helper.createTextStyle( 133 | itemModel.getModel(['blur', 'textStyle']), 134 | { 135 | state: 'blur' 136 | } 137 | ); 138 | 139 | external_echarts_.helper.enableHoverEmphasis( 140 | textEl, 141 | itemModel.get(['emphasis', 'focus']), 142 | itemModel.get(['emphasis', 'blurScope']) 143 | ); 144 | 145 | textEl.stateTransition = { 146 | duration: seriesModel.get('animation') 147 | ? seriesModel.get(['stateAnimation', 'duration']) 148 | : 0, 149 | easing: seriesModel.get(['stateAnimation', 'easing']) 150 | }; 151 | // TODO 152 | textEl.__highDownDispatcher = true; 153 | }; 154 | 155 | this._model = seriesModel; 156 | }, 157 | 158 | remove: function () { 159 | this.group.removeAll(); 160 | 161 | this._model.layoutInstance.dispose(); 162 | }, 163 | 164 | dispose: function () { 165 | this._model.layoutInstance.dispose(); 166 | } 167 | }); 168 | 169 | ;// CONCATENATED MODULE: ./src/layout.js 170 | /*! 171 | * wordcloud2.js 172 | * http://timdream.org/wordcloud2.js/ 173 | * 174 | * Copyright 2011 - 2019 Tim Guan-tin Chien and contributors. 175 | * Released under the MIT license 176 | */ 177 | 178 | 179 | 180 | // setImmediate 181 | if (!window.setImmediate) { 182 | window.setImmediate = (function setupSetImmediate() { 183 | return ( 184 | window.msSetImmediate || 185 | window.webkitSetImmediate || 186 | window.mozSetImmediate || 187 | window.oSetImmediate || 188 | (function setupSetZeroTimeout() { 189 | if (!window.postMessage || !window.addEventListener) { 190 | return null; 191 | } 192 | 193 | var callbacks = [undefined]; 194 | var message = 'zero-timeout-message'; 195 | 196 | // Like setTimeout, but only takes a function argument. There's 197 | // no time argument (always zero) and no arguments (you have to 198 | // use a closure). 199 | var setZeroTimeout = function setZeroTimeout(callback) { 200 | var id = callbacks.length; 201 | callbacks.push(callback); 202 | window.postMessage(message + id.toString(36), '*'); 203 | 204 | return id; 205 | }; 206 | 207 | window.addEventListener( 208 | 'message', 209 | function setZeroTimeoutMessage(evt) { 210 | // Skipping checking event source, retarded IE confused this window 211 | // object with another in the presence of iframe 212 | if ( 213 | typeof evt.data !== 'string' || 214 | evt.data.substr(0, message.length) !== message /* || 215 | evt.source !== window */ 216 | ) { 217 | return; 218 | } 219 | 220 | evt.stopImmediatePropagation(); 221 | 222 | var id = parseInt(evt.data.substr(message.length), 36); 223 | if (!callbacks[id]) { 224 | return; 225 | } 226 | 227 | callbacks[id](); 228 | callbacks[id] = undefined; 229 | }, 230 | true 231 | ); 232 | 233 | /* specify clearImmediate() here since we need the scope */ 234 | window.clearImmediate = function clearZeroTimeout(id) { 235 | if (!callbacks[id]) { 236 | return; 237 | } 238 | 239 | callbacks[id] = undefined; 240 | }; 241 | 242 | return setZeroTimeout; 243 | })() || 244 | // fallback 245 | function setImmediateFallback(fn) { 246 | window.setTimeout(fn, 0); 247 | } 248 | ); 249 | })(); 250 | } 251 | 252 | if (!window.clearImmediate) { 253 | window.clearImmediate = (function setupClearImmediate() { 254 | return ( 255 | window.msClearImmediate || 256 | window.webkitClearImmediate || 257 | window.mozClearImmediate || 258 | window.oClearImmediate || 259 | // "clearZeroTimeout" is implement on the previous block || 260 | // fallback 261 | function clearImmediateFallback(timer) { 262 | window.clearTimeout(timer); 263 | } 264 | ); 265 | })(); 266 | } 267 | 268 | // Check if WordCloud can run on this browser 269 | var isSupported = (function isSupported() { 270 | var canvas = document.createElement('canvas'); 271 | if (!canvas || !canvas.getContext) { 272 | return false; 273 | } 274 | 275 | var ctx = canvas.getContext('2d'); 276 | if (!ctx) { 277 | return false; 278 | } 279 | if (!ctx.getImageData) { 280 | return false; 281 | } 282 | if (!ctx.fillText) { 283 | return false; 284 | } 285 | 286 | if (!Array.prototype.some) { 287 | return false; 288 | } 289 | if (!Array.prototype.push) { 290 | return false; 291 | } 292 | 293 | return true; 294 | })(); 295 | 296 | // Find out if the browser impose minium font size by 297 | // drawing small texts on a canvas and measure it's width. 298 | var minFontSize = (function getMinFontSize() { 299 | if (!isSupported) { 300 | return; 301 | } 302 | 303 | var ctx = document.createElement('canvas').getContext('2d'); 304 | 305 | // start from 20 306 | var size = 20; 307 | 308 | // two sizes to measure 309 | var hanWidth, mWidth; 310 | 311 | while (size) { 312 | ctx.font = size.toString(10) + 'px sans-serif'; 313 | if ( 314 | ctx.measureText('\uFF37').width === hanWidth && 315 | ctx.measureText('m').width === mWidth 316 | ) { 317 | return size + 1; 318 | } 319 | 320 | hanWidth = ctx.measureText('\uFF37').width; 321 | mWidth = ctx.measureText('m').width; 322 | 323 | size--; 324 | } 325 | 326 | return 0; 327 | })(); 328 | 329 | var getItemExtraData = function (item) { 330 | if (Array.isArray(item)) { 331 | var itemCopy = item.slice(); 332 | // remove data we already have (word and weight) 333 | itemCopy.splice(0, 2); 334 | return itemCopy; 335 | } else { 336 | return []; 337 | } 338 | }; 339 | 340 | // Based on http://jsfromhell.com/array/shuffle 341 | var shuffleArray = function shuffleArray(arr) { 342 | for (var j, x, i = arr.length; i; ) { 343 | j = Math.floor(Math.random() * i); 344 | x = arr[--i]; 345 | arr[i] = arr[j]; 346 | arr[j] = x; 347 | } 348 | return arr; 349 | }; 350 | 351 | var timer = {}; 352 | var WordCloud = function WordCloud(elements, options) { 353 | if (!isSupported) { 354 | return; 355 | } 356 | 357 | var timerId = Math.floor(Math.random() * Date.now()); 358 | 359 | if (!Array.isArray(elements)) { 360 | elements = [elements]; 361 | } 362 | 363 | elements.forEach(function (el, i) { 364 | if (typeof el === 'string') { 365 | elements[i] = document.getElementById(el); 366 | if (!elements[i]) { 367 | throw new Error('The element id specified is not found.'); 368 | } 369 | } else if (!el.tagName && !el.appendChild) { 370 | throw new Error( 371 | 'You must pass valid HTML elements, or ID of the element.' 372 | ); 373 | } 374 | }); 375 | 376 | /* Default values to be overwritten by options object */ 377 | var settings = { 378 | list: [], 379 | fontFamily: 380 | '"Trebuchet MS", "Heiti TC", "微軟正黑體", ' + 381 | '"Arial Unicode MS", "Droid Fallback Sans", sans-serif', 382 | fontWeight: 'normal', 383 | color: 'random-dark', 384 | minSize: 0, // 0 to disable 385 | weightFactor: 1, 386 | clearCanvas: true, 387 | backgroundColor: '#fff', // opaque white = rgba(255, 255, 255, 1) 388 | 389 | gridSize: 8, 390 | drawOutOfBound: false, 391 | shrinkToFit: false, 392 | origin: null, 393 | 394 | drawMask: false, 395 | maskColor: 'rgba(255,0,0,0.3)', 396 | maskGapWidth: 0.3, 397 | 398 | layoutAnimation: true, 399 | 400 | wait: 0, 401 | abortThreshold: 0, // disabled 402 | abort: function noop() {}, 403 | 404 | minRotation: -Math.PI / 2, 405 | maxRotation: Math.PI / 2, 406 | rotationStep: 0.1, 407 | 408 | shuffle: true, 409 | rotateRatio: 0.1, 410 | 411 | shape: 'circle', 412 | ellipticity: 0.65, 413 | 414 | classes: null, 415 | 416 | hover: null, 417 | click: null 418 | }; 419 | 420 | if (options) { 421 | for (var key in options) { 422 | if (key in settings) { 423 | settings[key] = options[key]; 424 | } 425 | } 426 | } 427 | 428 | /* Convert weightFactor into a function */ 429 | if (typeof settings.weightFactor !== 'function') { 430 | var factor = settings.weightFactor; 431 | settings.weightFactor = function weightFactor(pt) { 432 | return pt * factor; // in px 433 | }; 434 | } 435 | 436 | /* Convert shape into a function */ 437 | if (typeof settings.shape !== 'function') { 438 | switch (settings.shape) { 439 | case 'circle': 440 | /* falls through */ 441 | default: 442 | // 'circle' is the default and a shortcut in the code loop. 443 | settings.shape = 'circle'; 444 | break; 445 | 446 | case 'cardioid': 447 | settings.shape = function shapeCardioid(theta) { 448 | return 1 - Math.sin(theta); 449 | }; 450 | break; 451 | 452 | /* 453 | To work out an X-gon, one has to calculate "m", 454 | where 1/(cos(2*PI/X)+m*sin(2*PI/X)) = 1/(cos(0)+m*sin(0)) 455 | http://www.wolframalpha.com/input/?i=1%2F%28cos%282*PI%2FX%29%2Bm*sin%28 456 | 2*PI%2FX%29%29+%3D+1%2F%28cos%280%29%2Bm*sin%280%29%29 457 | Copy the solution into polar equation r = 1/(cos(t') + m*sin(t')) 458 | where t' equals to mod(t, 2PI/X); 459 | */ 460 | 461 | case 'diamond': 462 | // http://www.wolframalpha.com/input/?i=plot+r+%3D+1%2F%28cos%28mod+ 463 | // %28t%2C+PI%2F2%29%29%2Bsin%28mod+%28t%2C+PI%2F2%29%29%29%2C+t+%3D 464 | // +0+..+2*PI 465 | settings.shape = function shapeSquare(theta) { 466 | var thetaPrime = theta % ((2 * Math.PI) / 4); 467 | return 1 / (Math.cos(thetaPrime) + Math.sin(thetaPrime)); 468 | }; 469 | break; 470 | 471 | case 'square': 472 | // http://www.wolframalpha.com/input/?i=plot+r+%3D+min(1%2Fabs(cos(t 473 | // )),1%2Fabs(sin(t)))),+t+%3D+0+..+2*PI 474 | settings.shape = function shapeSquare(theta) { 475 | return Math.min( 476 | 1 / Math.abs(Math.cos(theta)), 477 | 1 / Math.abs(Math.sin(theta)) 478 | ); 479 | }; 480 | break; 481 | 482 | case 'triangle-forward': 483 | // http://www.wolframalpha.com/input/?i=plot+r+%3D+1%2F%28cos%28mod+ 484 | // %28t%2C+2*PI%2F3%29%29%2Bsqrt%283%29sin%28mod+%28t%2C+2*PI%2F3%29 485 | // %29%29%2C+t+%3D+0+..+2*PI 486 | settings.shape = function shapeTriangle(theta) { 487 | var thetaPrime = theta % ((2 * Math.PI) / 3); 488 | return ( 489 | 1 / (Math.cos(thetaPrime) + Math.sqrt(3) * Math.sin(thetaPrime)) 490 | ); 491 | }; 492 | break; 493 | 494 | case 'triangle': 495 | case 'triangle-upright': 496 | settings.shape = function shapeTriangle(theta) { 497 | var thetaPrime = (theta + (Math.PI * 3) / 2) % ((2 * Math.PI) / 3); 498 | return ( 499 | 1 / (Math.cos(thetaPrime) + Math.sqrt(3) * Math.sin(thetaPrime)) 500 | ); 501 | }; 502 | break; 503 | 504 | case 'pentagon': 505 | settings.shape = function shapePentagon(theta) { 506 | var thetaPrime = (theta + 0.955) % ((2 * Math.PI) / 5); 507 | return 1 / (Math.cos(thetaPrime) + 0.726543 * Math.sin(thetaPrime)); 508 | }; 509 | break; 510 | 511 | case 'star': 512 | settings.shape = function shapeStar(theta) { 513 | var thetaPrime = (theta + 0.955) % ((2 * Math.PI) / 10); 514 | if ( 515 | ((theta + 0.955) % ((2 * Math.PI) / 5)) - (2 * Math.PI) / 10 >= 516 | 0 517 | ) { 518 | return ( 519 | 1 / 520 | (Math.cos((2 * Math.PI) / 10 - thetaPrime) + 521 | 3.07768 * Math.sin((2 * Math.PI) / 10 - thetaPrime)) 522 | ); 523 | } else { 524 | return 1 / (Math.cos(thetaPrime) + 3.07768 * Math.sin(thetaPrime)); 525 | } 526 | }; 527 | break; 528 | } 529 | } 530 | 531 | /* Make sure gridSize is a whole number and is not smaller than 4px */ 532 | settings.gridSize = Math.max(Math.floor(settings.gridSize), 4); 533 | 534 | /* shorthand */ 535 | var g = settings.gridSize; 536 | var maskRectWidth = g - settings.maskGapWidth; 537 | 538 | /* normalize rotation settings */ 539 | var rotationRange = Math.abs(settings.maxRotation - settings.minRotation); 540 | var minRotation = Math.min(settings.maxRotation, settings.minRotation); 541 | var rotationStep = settings.rotationStep; 542 | 543 | /* information/object available to all functions, set when start() */ 544 | var grid, // 2d array containing filling information 545 | ngx, 546 | ngy, // width and height of the grid 547 | center, // position of the center of the cloud 548 | maxRadius; 549 | 550 | /* timestamp for measuring each putWord() action */ 551 | var escapeTime; 552 | 553 | /* function for getting the color of the text */ 554 | var getTextColor; 555 | function randomHslColor(min, max) { 556 | return ( 557 | 'hsl(' + 558 | (Math.random() * 360).toFixed() + 559 | ',' + 560 | (Math.random() * 30 + 70).toFixed() + 561 | '%,' + 562 | (Math.random() * (max - min) + min).toFixed() + 563 | '%)' 564 | ); 565 | } 566 | switch (settings.color) { 567 | case 'random-dark': 568 | getTextColor = function getRandomDarkColor() { 569 | return randomHslColor(10, 50); 570 | }; 571 | break; 572 | 573 | case 'random-light': 574 | getTextColor = function getRandomLightColor() { 575 | return randomHslColor(50, 90); 576 | }; 577 | break; 578 | 579 | default: 580 | if (typeof settings.color === 'function') { 581 | getTextColor = settings.color; 582 | } 583 | break; 584 | } 585 | 586 | /* function for getting the font-weight of the text */ 587 | var getTextFontWeight; 588 | if (typeof settings.fontWeight === 'function') { 589 | getTextFontWeight = settings.fontWeight; 590 | } 591 | 592 | /* function for getting the classes of the text */ 593 | var getTextClasses = null; 594 | if (typeof settings.classes === 'function') { 595 | getTextClasses = settings.classes; 596 | } 597 | 598 | /* Interactive */ 599 | var interactive = false; 600 | var infoGrid = []; 601 | var hovered; 602 | 603 | var getInfoGridFromMouseTouchEvent = function getInfoGridFromMouseTouchEvent( 604 | evt 605 | ) { 606 | var canvas = evt.currentTarget; 607 | var rect = canvas.getBoundingClientRect(); 608 | var clientX; 609 | var clientY; 610 | /** Detect if touches are available */ 611 | if (evt.touches) { 612 | clientX = evt.touches[0].clientX; 613 | clientY = evt.touches[0].clientY; 614 | } else { 615 | clientX = evt.clientX; 616 | clientY = evt.clientY; 617 | } 618 | var eventX = clientX - rect.left; 619 | var eventY = clientY - rect.top; 620 | 621 | var x = Math.floor((eventX * (canvas.width / rect.width || 1)) / g); 622 | var y = Math.floor((eventY * (canvas.height / rect.height || 1)) / g); 623 | 624 | if (!infoGrid[x]) { 625 | return null 626 | } 627 | 628 | return infoGrid[x][y]; 629 | }; 630 | 631 | var wordcloudhover = function wordcloudhover(evt) { 632 | var info = getInfoGridFromMouseTouchEvent(evt); 633 | 634 | if (hovered === info) { 635 | return; 636 | } 637 | 638 | hovered = info; 639 | if (!info) { 640 | settings.hover(undefined, undefined, evt); 641 | 642 | return; 643 | } 644 | 645 | settings.hover(info.item, info.dimension, evt); 646 | }; 647 | 648 | var wordcloudclick = function wordcloudclick(evt) { 649 | var info = getInfoGridFromMouseTouchEvent(evt); 650 | if (!info) { 651 | return; 652 | } 653 | 654 | settings.click(info.item, info.dimension, evt); 655 | evt.preventDefault(); 656 | }; 657 | 658 | /* Get points on the grid for a given radius away from the center */ 659 | var pointsAtRadius = []; 660 | var getPointsAtRadius = function getPointsAtRadius(radius) { 661 | if (pointsAtRadius[radius]) { 662 | return pointsAtRadius[radius]; 663 | } 664 | 665 | // Look for these number of points on each radius 666 | var T = radius * 8; 667 | 668 | // Getting all the points at this radius 669 | var t = T; 670 | var points = []; 671 | 672 | if (radius === 0) { 673 | points.push([center[0], center[1], 0]); 674 | } 675 | 676 | while (t--) { 677 | // distort the radius to put the cloud in shape 678 | var rx = 1; 679 | if (settings.shape !== 'circle') { 680 | rx = settings.shape((t / T) * 2 * Math.PI); // 0 to 1 681 | } 682 | 683 | // Push [x, y, t]; t is used solely for getTextColor() 684 | points.push([ 685 | center[0] + radius * rx * Math.cos((-t / T) * 2 * Math.PI), 686 | center[1] + 687 | radius * rx * Math.sin((-t / T) * 2 * Math.PI) * settings.ellipticity, 688 | (t / T) * 2 * Math.PI 689 | ]); 690 | } 691 | 692 | pointsAtRadius[radius] = points; 693 | return points; 694 | }; 695 | 696 | /* Return true if we had spent too much time */ 697 | var exceedTime = function exceedTime() { 698 | return ( 699 | settings.abortThreshold > 0 && 700 | new Date().getTime() - escapeTime > settings.abortThreshold 701 | ); 702 | }; 703 | 704 | /* Get the deg of rotation according to settings, and luck. */ 705 | var getRotateDeg = function getRotateDeg() { 706 | if (settings.rotateRatio === 0) { 707 | return 0; 708 | } 709 | 710 | if (Math.random() > settings.rotateRatio) { 711 | return 0; 712 | } 713 | 714 | if (rotationRange === 0) { 715 | return minRotation; 716 | } 717 | 718 | return minRotation + Math.round(Math.random() * rotationRange / rotationStep) * rotationStep; 719 | }; 720 | 721 | var getTextInfo = function getTextInfo( 722 | word, 723 | weight, 724 | rotateDeg, 725 | extraDataArray 726 | ) { 727 | // calculate the acutal font size 728 | // fontSize === 0 means weightFactor function wants the text skipped, 729 | // and size < minSize means we cannot draw the text. 730 | var debug = false; 731 | var fontSize = settings.weightFactor(weight); 732 | if (fontSize <= settings.minSize) { 733 | return false; 734 | } 735 | 736 | // Scale factor here is to make sure fillText is not limited by 737 | // the minium font size set by browser. 738 | // It will always be 1 or 2n. 739 | var mu = 1; 740 | if (fontSize < minFontSize) { 741 | mu = (function calculateScaleFactor() { 742 | var mu = 2; 743 | while (mu * fontSize < minFontSize) { 744 | mu += 2; 745 | } 746 | return mu; 747 | })(); 748 | } 749 | 750 | // Get fontWeight that will be used to set fctx.font 751 | var fontWeight; 752 | if (getTextFontWeight) { 753 | fontWeight = getTextFontWeight(word, weight, fontSize, extraDataArray); 754 | } else { 755 | fontWeight = settings.fontWeight; 756 | } 757 | 758 | var fcanvas = document.createElement('canvas'); 759 | var fctx = fcanvas.getContext('2d', { willReadFrequently: true }); 760 | 761 | fctx.font = 762 | fontWeight + 763 | ' ' + 764 | (fontSize * mu).toString(10) + 765 | 'px ' + 766 | settings.fontFamily; 767 | 768 | // Estimate the dimension of the text with measureText(). 769 | var fw = fctx.measureText(word).width / mu; 770 | var fh = 771 | Math.max( 772 | fontSize * mu, 773 | fctx.measureText('m').width, 774 | fctx.measureText('\uFF37').width 775 | ) / mu; 776 | 777 | // Create a boundary box that is larger than our estimates, 778 | // so text don't get cut of (it sill might) 779 | var boxWidth = fw + fh * 2; 780 | var boxHeight = fh * 3; 781 | var fgw = Math.ceil(boxWidth / g); 782 | var fgh = Math.ceil(boxHeight / g); 783 | boxWidth = fgw * g; 784 | boxHeight = fgh * g; 785 | 786 | // Calculate the proper offsets to make the text centered at 787 | // the preferred position. 788 | 789 | // This is simply half of the width. 790 | var fillTextOffsetX = -fw / 2; 791 | // Instead of moving the box to the exact middle of the preferred 792 | // position, for Y-offset we move 0.4 instead, so Latin alphabets look 793 | // vertical centered. 794 | var fillTextOffsetY = -fh * 0.4; 795 | 796 | // Calculate the actual dimension of the canvas, considering the rotation. 797 | var cgh = Math.ceil( 798 | (boxWidth * Math.abs(Math.sin(rotateDeg)) + 799 | boxHeight * Math.abs(Math.cos(rotateDeg))) / 800 | g 801 | ); 802 | var cgw = Math.ceil( 803 | (boxWidth * Math.abs(Math.cos(rotateDeg)) + 804 | boxHeight * Math.abs(Math.sin(rotateDeg))) / 805 | g 806 | ); 807 | var width = cgw * g; 808 | var height = cgh * g; 809 | 810 | fcanvas.setAttribute('width', width); 811 | fcanvas.setAttribute('height', height); 812 | 813 | if (debug) { 814 | // Attach fcanvas to the DOM 815 | document.body.appendChild(fcanvas); 816 | // Save it's state so that we could restore and draw the grid correctly. 817 | fctx.save(); 818 | } 819 | 820 | // Scale the canvas with |mu|. 821 | fctx.scale(1 / mu, 1 / mu); 822 | fctx.translate((width * mu) / 2, (height * mu) / 2); 823 | fctx.rotate(-rotateDeg); 824 | 825 | // Once the width/height is set, ctx info will be reset. 826 | // Set it again here. 827 | fctx.font = 828 | fontWeight + 829 | ' ' + 830 | (fontSize * mu).toString(10) + 831 | 'px ' + 832 | settings.fontFamily; 833 | 834 | // Fill the text into the fcanvas. 835 | // XXX: We cannot because textBaseline = 'top' here because 836 | // Firefox and Chrome uses different default line-height for canvas. 837 | // Please read https://bugzil.la/737852#c6. 838 | // Here, we use textBaseline = 'middle' and draw the text at exactly 839 | // 0.5 * fontSize lower. 840 | fctx.fillStyle = '#000'; 841 | fctx.textBaseline = 'middle'; 842 | fctx.fillText( 843 | word, 844 | fillTextOffsetX * mu, 845 | (fillTextOffsetY + fontSize * 0.5) * mu 846 | ); 847 | 848 | // Get the pixels of the text 849 | var imageData = fctx.getImageData(0, 0, width, height).data; 850 | 851 | if (exceedTime()) { 852 | return false; 853 | } 854 | 855 | if (debug) { 856 | // Draw the box of the original estimation 857 | fctx.strokeRect(fillTextOffsetX * mu, fillTextOffsetY, fw * mu, fh * mu); 858 | fctx.restore(); 859 | } 860 | 861 | // Read the pixels and save the information to the occupied array 862 | var occupied = []; 863 | var gx = cgw; 864 | var gy, x, y; 865 | var bounds = [cgh / 2, cgw / 2, cgh / 2, cgw / 2]; 866 | while (gx--) { 867 | gy = cgh; 868 | while (gy--) { 869 | y = g; 870 | /* eslint no-labels: ['error', { 'allowLoop': true }] */ 871 | singleGridLoop: while (y--) { 872 | x = g; 873 | while (x--) { 874 | if (imageData[((gy * g + y) * width + (gx * g + x)) * 4 + 3]) { 875 | occupied.push([gx, gy]); 876 | 877 | if (gx < bounds[3]) { 878 | bounds[3] = gx; 879 | } 880 | if (gx > bounds[1]) { 881 | bounds[1] = gx; 882 | } 883 | if (gy < bounds[0]) { 884 | bounds[0] = gy; 885 | } 886 | if (gy > bounds[2]) { 887 | bounds[2] = gy; 888 | } 889 | 890 | if (debug) { 891 | fctx.fillStyle = 'rgba(255, 0, 0, 0.5)'; 892 | fctx.fillRect(gx * g, gy * g, g - 0.5, g - 0.5); 893 | } 894 | break singleGridLoop; 895 | } 896 | } 897 | } 898 | if (debug) { 899 | fctx.fillStyle = 'rgba(0, 0, 255, 0.5)'; 900 | fctx.fillRect(gx * g, gy * g, g - 0.5, g - 0.5); 901 | } 902 | } 903 | } 904 | 905 | if (debug) { 906 | fctx.fillStyle = 'rgba(0, 255, 0, 0.5)'; 907 | fctx.fillRect( 908 | bounds[3] * g, 909 | bounds[0] * g, 910 | (bounds[1] - bounds[3] + 1) * g, 911 | (bounds[2] - bounds[0] + 1) * g 912 | ); 913 | } 914 | 915 | // Return information needed to create the text on the real canvas 916 | return { 917 | mu: mu, 918 | occupied: occupied, 919 | bounds: bounds, 920 | gw: cgw, 921 | gh: cgh, 922 | fillTextOffsetX: fillTextOffsetX, 923 | fillTextOffsetY: fillTextOffsetY, 924 | fillTextWidth: fw, 925 | fillTextHeight: fh, 926 | fontSize: fontSize 927 | }; 928 | }; 929 | 930 | /* Determine if there is room available in the given dimension */ 931 | var canFitText = function canFitText(gx, gy, gw, gh, occupied) { 932 | // Go through the occupied points, 933 | // return false if the space is not available. 934 | var i = occupied.length; 935 | while (i--) { 936 | var px = gx + occupied[i][0]; 937 | var py = gy + occupied[i][1]; 938 | 939 | if (px >= ngx || py >= ngy || px < 0 || py < 0) { 940 | if (!settings.drawOutOfBound) { 941 | return false; 942 | } 943 | continue; 944 | } 945 | 946 | if (!grid[px][py]) { 947 | return false; 948 | } 949 | } 950 | return true; 951 | }; 952 | 953 | /* Actually draw the text on the grid */ 954 | var drawText = function drawText( 955 | gx, 956 | gy, 957 | info, 958 | word, 959 | weight, 960 | distance, 961 | theta, 962 | rotateDeg, 963 | attributes, 964 | extraDataArray 965 | ) { 966 | var fontSize = info.fontSize; 967 | var color; 968 | if (getTextColor) { 969 | color = getTextColor( 970 | word, 971 | weight, 972 | fontSize, 973 | distance, 974 | theta, 975 | extraDataArray 976 | ); 977 | } else { 978 | color = settings.color; 979 | } 980 | 981 | // get fontWeight that will be used to set ctx.font and font style rule 982 | var fontWeight; 983 | if (getTextFontWeight) { 984 | fontWeight = getTextFontWeight(word, weight, fontSize, extraDataArray); 985 | } else { 986 | fontWeight = settings.fontWeight; 987 | } 988 | 989 | var classes; 990 | if (getTextClasses) { 991 | classes = getTextClasses(word, weight, fontSize, extraDataArray); 992 | } else { 993 | classes = settings.classes; 994 | } 995 | 996 | elements.forEach(function (el) { 997 | if (el.getContext) { 998 | var ctx = el.getContext('2d'); 999 | var mu = info.mu; 1000 | 1001 | // Save the current state before messing it 1002 | ctx.save(); 1003 | ctx.scale(1 / mu, 1 / mu); 1004 | 1005 | ctx.font = 1006 | fontWeight + 1007 | ' ' + 1008 | (fontSize * mu).toString(10) + 1009 | 'px ' + 1010 | settings.fontFamily; 1011 | ctx.fillStyle = color; 1012 | 1013 | // Translate the canvas position to the origin coordinate of where 1014 | // the text should be put. 1015 | ctx.translate((gx + info.gw / 2) * g * mu, (gy + info.gh / 2) * g * mu); 1016 | 1017 | if (rotateDeg !== 0) { 1018 | ctx.rotate(-rotateDeg); 1019 | } 1020 | 1021 | // Finally, fill the text. 1022 | 1023 | // XXX: We cannot because textBaseline = 'top' here because 1024 | // Firefox and Chrome uses different default line-height for canvas. 1025 | // Please read https://bugzil.la/737852#c6. 1026 | // Here, we use textBaseline = 'middle' and draw the text at exactly 1027 | // 0.5 * fontSize lower. 1028 | ctx.textBaseline = 'middle'; 1029 | ctx.fillText( 1030 | word, 1031 | info.fillTextOffsetX * mu, 1032 | (info.fillTextOffsetY + fontSize * 0.5) * mu 1033 | ); 1034 | 1035 | // The below box is always matches how s are positioned 1036 | /* ctx.strokeRect(info.fillTextOffsetX, info.fillTextOffsetY, 1037 | info.fillTextWidth, info.fillTextHeight); */ 1038 | 1039 | // Restore the state. 1040 | ctx.restore(); 1041 | } else { 1042 | // drawText on DIV element 1043 | var span = document.createElement('span'); 1044 | var transformRule = ''; 1045 | transformRule = 'rotate(' + (-rotateDeg / Math.PI) * 180 + 'deg) '; 1046 | if (info.mu !== 1) { 1047 | transformRule += 1048 | 'translateX(-' + 1049 | info.fillTextWidth / 4 + 1050 | 'px) ' + 1051 | 'scale(' + 1052 | 1 / info.mu + 1053 | ')'; 1054 | } 1055 | var styleRules = { 1056 | position: 'absolute', 1057 | display: 'block', 1058 | font: 1059 | fontWeight + ' ' + fontSize * info.mu + 'px ' + settings.fontFamily, 1060 | left: (gx + info.gw / 2) * g + info.fillTextOffsetX + 'px', 1061 | top: (gy + info.gh / 2) * g + info.fillTextOffsetY + 'px', 1062 | width: info.fillTextWidth + 'px', 1063 | height: info.fillTextHeight + 'px', 1064 | lineHeight: fontSize + 'px', 1065 | whiteSpace: 'nowrap', 1066 | transform: transformRule, 1067 | webkitTransform: transformRule, 1068 | msTransform: transformRule, 1069 | transformOrigin: '50% 40%', 1070 | webkitTransformOrigin: '50% 40%', 1071 | msTransformOrigin: '50% 40%' 1072 | }; 1073 | if (color) { 1074 | styleRules.color = color; 1075 | } 1076 | span.textContent = word; 1077 | for (var cssProp in styleRules) { 1078 | span.style[cssProp] = styleRules[cssProp]; 1079 | } 1080 | if (attributes) { 1081 | for (var attribute in attributes) { 1082 | span.setAttribute(attribute, attributes[attribute]); 1083 | } 1084 | } 1085 | if (classes) { 1086 | span.className += classes; 1087 | } 1088 | el.appendChild(span); 1089 | } 1090 | }); 1091 | }; 1092 | 1093 | /* Help function to updateGrid */ 1094 | var fillGridAt = function fillGridAt(x, y, drawMask, dimension, item) { 1095 | if (x >= ngx || y >= ngy || x < 0 || y < 0) { 1096 | return; 1097 | } 1098 | 1099 | grid[x][y] = false; 1100 | 1101 | if (drawMask) { 1102 | var ctx = elements[0].getContext('2d'); 1103 | ctx.fillRect(x * g, y * g, maskRectWidth, maskRectWidth); 1104 | } 1105 | 1106 | if (interactive) { 1107 | infoGrid[x][y] = { item: item, dimension: dimension }; 1108 | } 1109 | }; 1110 | 1111 | /* Update the filling information of the given space with occupied points. 1112 | Draw the mask on the canvas if necessary. */ 1113 | var updateGrid = function updateGrid(gx, gy, gw, gh, info, item) { 1114 | var occupied = info.occupied; 1115 | var drawMask = settings.drawMask; 1116 | var ctx; 1117 | if (drawMask) { 1118 | ctx = elements[0].getContext('2d'); 1119 | ctx.save(); 1120 | ctx.fillStyle = settings.maskColor; 1121 | } 1122 | 1123 | var dimension; 1124 | if (interactive) { 1125 | var bounds = info.bounds; 1126 | dimension = { 1127 | x: (gx + bounds[3]) * g, 1128 | y: (gy + bounds[0]) * g, 1129 | w: (bounds[1] - bounds[3] + 1) * g, 1130 | h: (bounds[2] - bounds[0] + 1) * g 1131 | }; 1132 | } 1133 | 1134 | var i = occupied.length; 1135 | while (i--) { 1136 | var px = gx + occupied[i][0]; 1137 | var py = gy + occupied[i][1]; 1138 | 1139 | if (px >= ngx || py >= ngy || px < 0 || py < 0) { 1140 | continue; 1141 | } 1142 | 1143 | fillGridAt(px, py, drawMask, dimension, item); 1144 | } 1145 | 1146 | if (drawMask) { 1147 | ctx.restore(); 1148 | } 1149 | }; 1150 | 1151 | /* putWord() processes each item on the list, 1152 | calculate it's size and determine it's position, and actually 1153 | put it on the canvas. */ 1154 | var putWord = function putWord(item, loopIndex) { 1155 | if (loopIndex > 20) { 1156 | return null; 1157 | } 1158 | 1159 | var word, weight, attributes; 1160 | if (Array.isArray(item)) { 1161 | word = item[0]; 1162 | weight = item[1]; 1163 | } else { 1164 | word = item.word; 1165 | weight = item.weight; 1166 | attributes = item.attributes; 1167 | } 1168 | var rotateDeg = getRotateDeg(); 1169 | 1170 | var extraDataArray = getItemExtraData(item); 1171 | 1172 | // get info needed to put the text onto the canvas 1173 | var info = getTextInfo(word, weight, rotateDeg, extraDataArray); 1174 | 1175 | // not getting the info means we shouldn't be drawing this one. 1176 | if (!info) { 1177 | return false; 1178 | } 1179 | 1180 | if (exceedTime()) { 1181 | return false; 1182 | } 1183 | 1184 | // If drawOutOfBound is set to false, 1185 | // skip the loop if we have already know the bounding box of 1186 | // word is larger than the canvas. 1187 | if (!settings.drawOutOfBound && !settings.shrinkToFit) { 1188 | var bounds = info.bounds; 1189 | if (bounds[1] - bounds[3] + 1 > ngx || bounds[2] - bounds[0] + 1 > ngy) { 1190 | return false; 1191 | } 1192 | } 1193 | 1194 | // Determine the position to put the text by 1195 | // start looking for the nearest points 1196 | var r = maxRadius + 1; 1197 | 1198 | var tryToPutWordAtPoint = function (gxy) { 1199 | var gx = Math.floor(gxy[0] - info.gw / 2); 1200 | var gy = Math.floor(gxy[1] - info.gh / 2); 1201 | var gw = info.gw; 1202 | var gh = info.gh; 1203 | 1204 | // If we cannot fit the text at this position, return false 1205 | // and go to the next position. 1206 | if (!canFitText(gx, gy, gw, gh, info.occupied)) { 1207 | return false; 1208 | } 1209 | 1210 | // Actually put the text on the canvas 1211 | drawText( 1212 | gx, 1213 | gy, 1214 | info, 1215 | word, 1216 | weight, 1217 | maxRadius - r, 1218 | gxy[2], 1219 | rotateDeg, 1220 | attributes, 1221 | extraDataArray 1222 | ); 1223 | 1224 | // Mark the spaces on the grid as filled 1225 | updateGrid(gx, gy, gw, gh, info, item); 1226 | 1227 | return { 1228 | gx: gx, 1229 | gy: gy, 1230 | rot: rotateDeg, 1231 | info: info 1232 | }; 1233 | }; 1234 | 1235 | while (r--) { 1236 | var points = getPointsAtRadius(maxRadius - r); 1237 | 1238 | if (settings.shuffle) { 1239 | points = [].concat(points); 1240 | shuffleArray(points); 1241 | } 1242 | 1243 | // Try to fit the words by looking at each point. 1244 | // array.some() will stop and return true 1245 | // when putWordAtPoint() returns true. 1246 | for (var i = 0; i < points.length; i++) { 1247 | var res = tryToPutWordAtPoint(points[i]); 1248 | if (res) { 1249 | return res; 1250 | } 1251 | } 1252 | 1253 | // var drawn = points.some(tryToPutWordAtPoint); 1254 | // if (drawn) { 1255 | // // leave putWord() and return true 1256 | // return true; 1257 | // } 1258 | } 1259 | 1260 | if (settings.shrinkToFit) { 1261 | if (Array.isArray(item)) { 1262 | item[1] = (item[1] * 3) / 4; 1263 | } else { 1264 | item.weight = (item.weight * 3) / 4; 1265 | } 1266 | return putWord(item, loopIndex + 1); 1267 | } 1268 | 1269 | // we tried all distances but text won't fit, return null 1270 | return null; 1271 | }; 1272 | 1273 | /* Send DOM event to all elements. Will stop sending event and return 1274 | if the previous one is canceled (for cancelable events). */ 1275 | var sendEvent = function sendEvent(type, cancelable, details) { 1276 | if (cancelable) { 1277 | return !elements.some(function (el) { 1278 | var event = new CustomEvent(type, { 1279 | detail: details || {} 1280 | }); 1281 | return !el.dispatchEvent(event); 1282 | }, this); 1283 | } else { 1284 | elements.forEach(function (el) { 1285 | var event = new CustomEvent(type, { 1286 | detail: details || {} 1287 | }); 1288 | el.dispatchEvent(event); 1289 | }, this); 1290 | } 1291 | }; 1292 | 1293 | /* Start drawing on a canvas */ 1294 | var start = function start() { 1295 | // For dimensions, clearCanvas etc., 1296 | // we only care about the first element. 1297 | var canvas = elements[0]; 1298 | 1299 | if (canvas.getContext) { 1300 | ngx = Math.ceil(canvas.width / g); 1301 | ngy = Math.ceil(canvas.height / g); 1302 | } else { 1303 | var rect = canvas.getBoundingClientRect(); 1304 | ngx = Math.ceil(rect.width / g); 1305 | ngy = Math.ceil(rect.height / g); 1306 | } 1307 | 1308 | // Sending a wordcloudstart event which cause the previous loop to stop. 1309 | // Do nothing if the event is canceled. 1310 | if (!sendEvent('wordcloudstart', true)) { 1311 | return; 1312 | } 1313 | 1314 | // Determine the center of the word cloud 1315 | center = settings.origin 1316 | ? [settings.origin[0] / g, settings.origin[1] / g] 1317 | : [ngx / 2, ngy / 2]; 1318 | 1319 | // Maxium radius to look for space 1320 | maxRadius = Math.floor(Math.sqrt(ngx * ngx + ngy * ngy)); 1321 | 1322 | /* Clear the canvas only if the clearCanvas is set, 1323 | if not, update the grid to the current canvas state */ 1324 | grid = []; 1325 | 1326 | var gx, gy, i; 1327 | if (!canvas.getContext || settings.clearCanvas) { 1328 | elements.forEach(function (el) { 1329 | if (el.getContext) { 1330 | var ctx = el.getContext('2d'); 1331 | ctx.fillStyle = settings.backgroundColor; 1332 | ctx.clearRect(0, 0, ngx * (g + 1), ngy * (g + 1)); 1333 | ctx.fillRect(0, 0, ngx * (g + 1), ngy * (g + 1)); 1334 | } else { 1335 | el.textContent = ''; 1336 | el.style.backgroundColor = settings.backgroundColor; 1337 | el.style.position = 'relative'; 1338 | } 1339 | }); 1340 | 1341 | /* fill the grid with empty state */ 1342 | gx = ngx; 1343 | while (gx--) { 1344 | grid[gx] = []; 1345 | gy = ngy; 1346 | while (gy--) { 1347 | grid[gx][gy] = true; 1348 | } 1349 | } 1350 | } else { 1351 | /* Determine bgPixel by creating 1352 | another canvas and fill the specified background color. */ 1353 | var bctx = document.createElement('canvas').getContext('2d'); 1354 | 1355 | bctx.fillStyle = settings.backgroundColor; 1356 | bctx.fillRect(0, 0, 1, 1); 1357 | var bgPixel = bctx.getImageData(0, 0, 1, 1).data; 1358 | 1359 | /* Read back the pixels of the canvas we got to tell which part of the 1360 | canvas is empty. 1361 | (no clearCanvas only works with a canvas, not divs) */ 1362 | var imageData = canvas 1363 | .getContext('2d') 1364 | .getImageData(0, 0, ngx * g, ngy * g).data; 1365 | 1366 | gx = ngx; 1367 | var x, y; 1368 | while (gx--) { 1369 | grid[gx] = []; 1370 | gy = ngy; 1371 | while (gy--) { 1372 | y = g; 1373 | /* eslint no-labels: ['error', { 'allowLoop': true }] */ 1374 | singleGridLoop: while (y--) { 1375 | x = g; 1376 | while (x--) { 1377 | i = 4; 1378 | while (i--) { 1379 | if ( 1380 | imageData[((gy * g + y) * ngx * g + (gx * g + x)) * 4 + i] !== 1381 | bgPixel[i] 1382 | ) { 1383 | grid[gx][gy] = false; 1384 | break singleGridLoop; 1385 | } 1386 | } 1387 | } 1388 | } 1389 | if (grid[gx][gy] !== false) { 1390 | grid[gx][gy] = true; 1391 | } 1392 | } 1393 | } 1394 | 1395 | imageData = bctx = bgPixel = undefined; 1396 | } 1397 | 1398 | // fill the infoGrid with empty state if we need it 1399 | if (settings.hover || settings.click) { 1400 | interactive = true; 1401 | 1402 | /* fill the grid with empty state */ 1403 | gx = ngx + 1; 1404 | while (gx--) { 1405 | infoGrid[gx] = []; 1406 | } 1407 | 1408 | if (settings.hover) { 1409 | canvas.addEventListener('mousemove', wordcloudhover); 1410 | } 1411 | 1412 | if (settings.click) { 1413 | canvas.addEventListener('click', wordcloudclick); 1414 | canvas.addEventListener('touchstart', wordcloudclick); 1415 | canvas.addEventListener('touchend', function (e) { 1416 | e.preventDefault(); 1417 | }); 1418 | canvas.style.webkitTapHighlightColor = 'rgba(0, 0, 0, 0)'; 1419 | } 1420 | 1421 | canvas.addEventListener('wordcloudstart', function stopInteraction() { 1422 | canvas.removeEventListener('wordcloudstart', stopInteraction); 1423 | 1424 | canvas.removeEventListener('mousemove', wordcloudhover); 1425 | canvas.removeEventListener('click', wordcloudclick); 1426 | hovered = undefined; 1427 | }); 1428 | } 1429 | 1430 | i = 0; 1431 | var loopingFunction, stoppingFunction; 1432 | var layouting = true; 1433 | if (!settings.layoutAnimation) { 1434 | loopingFunction = function (cb) { 1435 | cb(); 1436 | }; 1437 | stoppingFunction = function () { 1438 | layouting = false; 1439 | }; 1440 | } else if (settings.wait !== 0) { 1441 | loopingFunction = window.setTimeout; 1442 | stoppingFunction = window.clearTimeout; 1443 | } else { 1444 | loopingFunction = window.setImmediate; 1445 | stoppingFunction = window.clearImmediate; 1446 | } 1447 | 1448 | var addEventListener = function addEventListener(type, listener) { 1449 | elements.forEach(function (el) { 1450 | el.addEventListener(type, listener); 1451 | }, this); 1452 | }; 1453 | 1454 | var removeEventListener = function removeEventListener(type, listener) { 1455 | elements.forEach(function (el) { 1456 | el.removeEventListener(type, listener); 1457 | }, this); 1458 | }; 1459 | 1460 | var anotherWordCloudStart = function anotherWordCloudStart() { 1461 | removeEventListener('wordcloudstart', anotherWordCloudStart); 1462 | stoppingFunction(timer[timerId]); 1463 | }; 1464 | 1465 | addEventListener('wordcloudstart', anotherWordCloudStart); 1466 | 1467 | // At least wait the following code before call the first iteration. 1468 | timer[timerId] = (settings.layoutAnimation ? loopingFunction : setTimeout)( 1469 | function loop() { 1470 | if (!layouting) { 1471 | return; 1472 | } 1473 | if (i >= settings.list.length) { 1474 | stoppingFunction(timer[timerId]); 1475 | sendEvent('wordcloudstop', false); 1476 | removeEventListener('wordcloudstart', anotherWordCloudStart); 1477 | delete timer[timerId]; 1478 | return; 1479 | } 1480 | escapeTime = new Date().getTime(); 1481 | var drawn = putWord(settings.list[i], 0); 1482 | var canceled = !sendEvent('wordclouddrawn', true, { 1483 | item: settings.list[i], 1484 | drawn: drawn 1485 | }); 1486 | if (exceedTime() || canceled) { 1487 | stoppingFunction(timer[timerId]); 1488 | settings.abort(); 1489 | sendEvent('wordcloudabort', false); 1490 | sendEvent('wordcloudstop', false); 1491 | removeEventListener('wordcloudstart', anotherWordCloudStart); 1492 | return; 1493 | } 1494 | i++; 1495 | timer[timerId] = loopingFunction(loop, settings.wait); 1496 | }, 1497 | settings.wait 1498 | ); 1499 | }; 1500 | 1501 | // All set, start the drawing 1502 | start(); 1503 | }; 1504 | 1505 | WordCloud.isSupported = isSupported; 1506 | WordCloud.minFontSize = minFontSize; 1507 | 1508 | /* harmony default export */ const layout = (WordCloud); 1509 | 1510 | ;// CONCATENATED MODULE: ./src/wordCloud.js 1511 | 1512 | 1513 | 1514 | 1515 | 1516 | 1517 | 1518 | if (!layout.isSupported) { 1519 | throw new Error('Sorry your browser not support wordCloud'); 1520 | } 1521 | 1522 | // https://github.com/timdream/wordcloud2.js/blob/c236bee60436e048949f9becc4f0f67bd832dc5c/index.js#L233 1523 | function updateCanvasMask(maskCanvas) { 1524 | var ctx = maskCanvas.getContext('2d'); 1525 | var imageData = ctx.getImageData(0, 0, maskCanvas.width, maskCanvas.height); 1526 | var newImageData = ctx.createImageData(imageData); 1527 | 1528 | var toneSum = 0; 1529 | var toneCnt = 0; 1530 | for (var i = 0; i < imageData.data.length; i += 4) { 1531 | var alpha = imageData.data[i + 3]; 1532 | if (alpha > 128) { 1533 | var tone = 1534 | imageData.data[i] + imageData.data[i + 1] + imageData.data[i + 2]; 1535 | toneSum += tone; 1536 | ++toneCnt; 1537 | } 1538 | } 1539 | var threshold = toneSum / toneCnt; 1540 | 1541 | for (var i = 0; i < imageData.data.length; i += 4) { 1542 | var tone = 1543 | imageData.data[i] + imageData.data[i + 1] + imageData.data[i + 2]; 1544 | var alpha = imageData.data[i + 3]; 1545 | 1546 | if (alpha < 128 || tone > threshold) { 1547 | // Area not to draw 1548 | newImageData.data[i] = 0; 1549 | newImageData.data[i + 1] = 0; 1550 | newImageData.data[i + 2] = 0; 1551 | newImageData.data[i + 3] = 0; 1552 | } else { 1553 | // Area to draw 1554 | // The color must be same with backgroundColor 1555 | newImageData.data[i] = 255; 1556 | newImageData.data[i + 1] = 255; 1557 | newImageData.data[i + 2] = 255; 1558 | newImageData.data[i + 3] = 255; 1559 | } 1560 | } 1561 | 1562 | ctx.putImageData(newImageData, 0, 0); 1563 | } 1564 | 1565 | external_echarts_.registerLayout(function (ecModel, api) { 1566 | ecModel.eachSeriesByType('wordCloud', function (seriesModel) { 1567 | var gridRect = external_echarts_.helper.getLayoutRect( 1568 | seriesModel.getBoxLayoutParams(), 1569 | { 1570 | width: api.getWidth(), 1571 | height: api.getHeight() 1572 | } 1573 | ); 1574 | 1575 | var keepAspect = seriesModel.get('keepAspect'); 1576 | var maskImage = seriesModel.get('maskImage'); 1577 | var ratio = maskImage ? maskImage.width / maskImage.height : 1; 1578 | keepAspect && adjustRectAspect(gridRect, ratio); 1579 | 1580 | var data = seriesModel.getData(); 1581 | 1582 | var canvas = document.createElement('canvas'); 1583 | canvas.width = gridRect.width; 1584 | canvas.height = gridRect.height; 1585 | 1586 | var ctx = canvas.getContext('2d'); 1587 | if (maskImage) { 1588 | try { 1589 | ctx.drawImage(maskImage, 0, 0, canvas.width, canvas.height); 1590 | updateCanvasMask(canvas); 1591 | } catch (e) { 1592 | console.error('Invalid mask image'); 1593 | console.error(e.toString()); 1594 | } 1595 | } 1596 | 1597 | var sizeRange = seriesModel.get('sizeRange'); 1598 | var rotationRange = seriesModel.get('rotationRange'); 1599 | var valueExtent = data.getDataExtent('value'); 1600 | 1601 | var DEGREE_TO_RAD = Math.PI / 180; 1602 | var gridSize = seriesModel.get('gridSize'); 1603 | layout(canvas, { 1604 | list: data 1605 | .mapArray('value', function (value, idx) { 1606 | var itemModel = data.getItemModel(idx); 1607 | return [ 1608 | data.getName(idx), 1609 | itemModel.get('textStyle.fontSize', true) || 1610 | external_echarts_.number.linearMap(value, valueExtent, sizeRange), 1611 | idx 1612 | ]; 1613 | }) 1614 | .sort(function (a, b) { 1615 | // Sort from large to small in case there is no more room for more words 1616 | return b[1] - a[1]; 1617 | }), 1618 | fontFamily: 1619 | seriesModel.get('textStyle.fontFamily') || 1620 | seriesModel.get('emphasis.textStyle.fontFamily') || 1621 | ecModel.get('textStyle.fontFamily'), 1622 | fontWeight: 1623 | seriesModel.get('textStyle.fontWeight') || 1624 | seriesModel.get('emphasis.textStyle.fontWeight') || 1625 | ecModel.get('textStyle.fontWeight'), 1626 | 1627 | gridSize: gridSize, 1628 | 1629 | ellipticity: gridRect.height / gridRect.width, 1630 | 1631 | minRotation: rotationRange[0] * DEGREE_TO_RAD, 1632 | maxRotation: rotationRange[1] * DEGREE_TO_RAD, 1633 | 1634 | clearCanvas: !maskImage, 1635 | 1636 | rotateRatio: 1, 1637 | 1638 | rotationStep: seriesModel.get('rotationStep') * DEGREE_TO_RAD, 1639 | 1640 | drawOutOfBound: seriesModel.get('drawOutOfBound'), 1641 | shrinkToFit: seriesModel.get('shrinkToFit'), 1642 | 1643 | layoutAnimation: seriesModel.get('layoutAnimation'), 1644 | 1645 | shuffle: false, 1646 | 1647 | shape: seriesModel.get('shape') 1648 | }); 1649 | 1650 | function onWordCloudDrawn(e) { 1651 | var item = e.detail.item; 1652 | if (e.detail.drawn && seriesModel.layoutInstance.ondraw) { 1653 | e.detail.drawn.gx += gridRect.x / gridSize; 1654 | e.detail.drawn.gy += gridRect.y / gridSize; 1655 | seriesModel.layoutInstance.ondraw( 1656 | item[0], 1657 | item[1], 1658 | item[2], 1659 | e.detail.drawn 1660 | ); 1661 | } 1662 | } 1663 | 1664 | canvas.addEventListener('wordclouddrawn', onWordCloudDrawn); 1665 | 1666 | if (seriesModel.layoutInstance) { 1667 | // Dispose previous 1668 | seriesModel.layoutInstance.dispose(); 1669 | } 1670 | 1671 | seriesModel.layoutInstance = { 1672 | ondraw: null, 1673 | 1674 | dispose: function () { 1675 | canvas.removeEventListener('wordclouddrawn', onWordCloudDrawn); 1676 | // Abort 1677 | canvas.addEventListener('wordclouddrawn', function (e) { 1678 | // Prevent default to cancle the event and stop the loop 1679 | e.preventDefault(); 1680 | }); 1681 | } 1682 | }; 1683 | }); 1684 | }); 1685 | 1686 | external_echarts_.registerPreprocessor(function (option) { 1687 | var series = (option || {}).series; 1688 | !external_echarts_.util.isArray(series) && (series = series ? [series] : []); 1689 | 1690 | var compats = ['shadowColor', 'shadowBlur', 'shadowOffsetX', 'shadowOffsetY']; 1691 | 1692 | external_echarts_.util.each(series, function (seriesItem) { 1693 | if (seriesItem && seriesItem.type === 'wordCloud') { 1694 | var textStyle = seriesItem.textStyle || {}; 1695 | 1696 | compatTextStyle(textStyle.normal); 1697 | compatTextStyle(textStyle.emphasis); 1698 | } 1699 | }); 1700 | 1701 | function compatTextStyle(textStyle) { 1702 | textStyle && 1703 | external_echarts_.util.each(compats, function (key) { 1704 | if (textStyle.hasOwnProperty(key)) { 1705 | textStyle['text' + external_echarts_.format.capitalFirst(key)] = textStyle[key]; 1706 | } 1707 | }); 1708 | } 1709 | }); 1710 | 1711 | function adjustRectAspect(gridRect, aspect) { 1712 | // var outerWidth = gridRect.width + gridRect.x * 2; 1713 | // var outerHeight = gridRect.height + gridRect.y * 2; 1714 | var width = gridRect.width; 1715 | var height = gridRect.height; 1716 | if (width > height * aspect) { 1717 | gridRect.x += (width - height * aspect) / 2; 1718 | gridRect.width = height * aspect; 1719 | } else { 1720 | gridRect.y += (height - width / aspect) / 2; 1721 | gridRect.height = width / aspect; 1722 | } 1723 | } 1724 | 1725 | ;// CONCATENATED MODULE: ./index.js 1726 | 1727 | 1728 | 1729 | /***/ }), 1730 | 1731 | /***/ "echarts/lib/echarts": 1732 | /*!**************************!*\ 1733 | !*** external "echarts" ***! 1734 | \**************************/ 1735 | /***/ ((module) => { 1736 | 1737 | module.exports = __WEBPACK_EXTERNAL_MODULE_echarts_lib_echarts__; 1738 | 1739 | /***/ }) 1740 | 1741 | /******/ }); 1742 | /************************************************************************/ 1743 | /******/ // The module cache 1744 | /******/ var __webpack_module_cache__ = {}; 1745 | /******/ 1746 | /******/ // The require function 1747 | /******/ function __webpack_require__(moduleId) { 1748 | /******/ // Check if module is in cache 1749 | /******/ if(__webpack_module_cache__[moduleId]) { 1750 | /******/ return __webpack_module_cache__[moduleId].exports; 1751 | /******/ } 1752 | /******/ // Create a new module (and put it into the cache) 1753 | /******/ var module = __webpack_module_cache__[moduleId] = { 1754 | /******/ // no module.id needed 1755 | /******/ // no module.loaded needed 1756 | /******/ exports: {} 1757 | /******/ }; 1758 | /******/ 1759 | /******/ // Execute the module function 1760 | /******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__); 1761 | /******/ 1762 | /******/ // Return the exports of the module 1763 | /******/ return module.exports; 1764 | /******/ } 1765 | /******/ 1766 | /************************************************************************/ 1767 | /******/ /* webpack/runtime/make namespace object */ 1768 | /******/ (() => { 1769 | /******/ // define __esModule on exports 1770 | /******/ __webpack_require__.r = (exports) => { 1771 | /******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { 1772 | /******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); 1773 | /******/ } 1774 | /******/ Object.defineProperty(exports, '__esModule', { value: true }); 1775 | /******/ }; 1776 | /******/ })(); 1777 | /******/ 1778 | /************************************************************************/ 1779 | /******/ // module exports must be returned from runtime so entry inlining is disabled 1780 | /******/ // startup 1781 | /******/ // Load entry module and return exports 1782 | /******/ return __webpack_require__("./index.js"); 1783 | /******/ })() 1784 | ; 1785 | }); 1786 | //# sourceMappingURL=echarts-wordcloud.js.map -------------------------------------------------------------------------------- /dist/echarts-wordcloud.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["webpack://echarts-wordcloud/webpack/universalModuleDefinition","webpack://echarts-wordcloud/./src/WordCloudSeries.js","webpack://echarts-wordcloud/./src/WordCloudView.js","webpack://echarts-wordcloud/./src/layout.js","webpack://echarts-wordcloud/./src/wordCloud.js","webpack://echarts-wordcloud/./index.js","webpack://echarts-wordcloud/external \"echarts\"","webpack://echarts-wordcloud/webpack/bootstrap","webpack://echarts-wordcloud/webpack/runtime/make namespace object","webpack://echarts-wordcloud/webpack/startup"],"names":[],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,CAAC;AACD,O;;;;;;;;;;;;;;;;ACV+C;;AAE/C,mCAAyB;AACzB;;AAEA;AACA;AACA;AACA;AACA;AACA,GAAG;AACH;;AAEA;AACA;AACA;AACA,GAAG;;AAEH;AACA,qBAAqB,yCAA+B;AACpD;AACA,KAAK;AACL,mBAAmB,sBAAY;AAC/B;AACA;AACA,GAAG;;AAEH;AACA;AACA;;AAEA;AACA;AACA;;AAEA;;AAEA;;AAEA;;AAEA;;AAEA;;AAEA;;AAEA;;AAEA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA,CAAC;;;AC1D8C;;AAE/C,iCAAuB;AACvB;;AAEA;AACA;AACA;;AAEA;;AAEA;;AAEA;AACA;AACA;;AAEA,uBAAuB,8BAAoB;AAC3C,eAAe,wCAA8B;AAC7C;AACA;AACA;AACA;AACA;AACA,OAAO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO;;AAEP;;AAEA;;AAEA,6CAA6C,wCAA8B;AAC3E;AACA;AACA;AACA;AACA;AACA,yCAAyC,wCAA8B;AACvE;AACA;AACA;AACA;AACA;;AAEA,MAAM,4CAAkC;AACxC;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA,GAAG;;AAEH;AACA;;AAEA;AACA,GAAG;;AAEH;AACA;AACA;AACA,CAAC;;;AC/ED;AACA;AACA;AACA;AACA;AACA;AACA;;AAEa;;AAEb;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA,WAAW;AACX;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA,OAAO;AACP;AACA;AACA;AACA;AACA;AACA,GAAG;AACH;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,GAAG;AACH;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA,CAAC;;AAED;AACA;AACA;AACA;AACA;AACA;;AAEA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA,CAAC;;AAED;AACA;AACA;AACA;AACA;AACA;AACA,GAAG;AACH;AACA;AACA;;AAEA;AACA;AACA,gCAAgC,GAAG;AACnC;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,KAAK;AACL;AACA;AACA;AACA;AACA,GAAG;;AAEH;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;;AAEA;AACA;AACA,6BAA6B;;AAE7B;AACA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA,yBAAyB;AACzB;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,WAAW;AACX;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,KAAK;AACL;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA,mDAAmD;AACnD;;AAEA,wBAAwB;AACxB;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO;AACP;;AAEA;AACA;AACA;AACA;AACA,KAAK;AACL;AACA;;AAEA;AACA,yCAAyC,2BAA2B;;AAEpE;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,wCAAwC,oBAAoB;AAC5D;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,KAAK;AACL;AACA;;AAEA;AACA;AACA;AACA;AACA,KAAK;AACL;AACA;;AAEA;AACA;AACA;AACA,KAAK;AACL;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA,qDAAqD;;AAErD;AACA;AACA,OAAO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,KAAK;AACL;;AAEA;AACA;AACA;AACA;AACA;;AAEA;;AAEA;AACA;AACA;AACA;;AAEA;AACA,wBAAwB;AACxB;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA,KAAK;AACL;AACA;AACA;AACA;AACA;;AAEA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA,qBAAqB,mBAAmB;AACxC;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA,OAAO;AACP;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,SAAS;AACT;AACA,OAAO;AACP,KAAK;AACL;AACA;AACA;AACA,SAAS;AACT;AACA,OAAO;AACP;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA,KAAK;AACL;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,SAAS;AACT;AACA;AACA;AACA;AACA,OAAO;;AAEP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,KAAK;AACL;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,0CAA0C,oBAAoB;AAC9D;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,SAAS;AACT;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA,OAAO;AACP;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,KAAK;AACL;AACA;AACA,KAAK;AACL;AACA;AACA;;AAEA;AACA;AACA;AACA,OAAO;AACP;;AAEA;AACA;AACA;AACA,OAAO;AACP;;AAEA;AACA;AACA;AACA;;AAEA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,SAAS;AACT;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO;AACP;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;;AAEA,6CAAe,SAAS,EAAC;;;AC1zCsB;;AAEpB;AACF;;AAEoB;;AAE7C,KAAK,kBAAiC;AACtC;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA,iBAAiB,2BAA2B;AAC5C;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA,iBAAiB,2BAA2B;AAC5C;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,KAAK;AACL;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA,gCAAsB;AACtB;AACA,mBAAmB,sCAA4B;AAC/C;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,OAAO;AACP;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA,IAAI,MAAqB;AACzB;AACA;AACA;AACA;AACA;AACA;AACA,cAAc,kCAAwB;AACtC;AACA;AACA,SAAS;AACT;AACA;AACA;AACA,SAAS;AACT;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;;AAEA;;AAEA;AACA;;AAEA;;AAEA;;AAEA;;AAEA;AACA;;AAEA;;AAEA;;AAEA;AACA,KAAK;;AAEL;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,SAAS;AACT;AACA;AACA,GAAG;AACH,CAAC;;AAED,sCAA4B;AAC5B,4BAA4B;AAC5B,GAAG,8BAAoB;;AAEvB;;AAEA,EAAE,2BAAiB;AACnB;AACA;;AAEA;AACA;AACA;AACA,GAAG;;AAEH;AACA;AACA,MAAM,2BAAiB;AACvB;AACA,6BAA6B,qCAA2B;AACxD;AACA,OAAO;AACP;AACA,CAAC;;AAED;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,GAAG;AACH;AACA;AACA;AACA;;;ACpNyB;;;;;;;;;;;ACAzB,iE;;;;;;UCAA;UACA;;UAEA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;;UAEA;UACA;;UAEA;UACA;UACA;;;;;WCrBA;WACA;WACA;WACA,sDAAsD,kBAAkB;WACxE;WACA,+CAA+C,cAAc;WAC7D,E;;;;UCNA;UACA;UACA;UACA","file":"echarts-wordcloud.js","sourcesContent":["(function webpackUniversalModuleDefinition(root, factory) {\n\tif(typeof exports === 'object' && typeof module === 'object')\n\t\tmodule.exports = factory(require(\"echarts\"));\n\telse if(typeof define === 'function' && define.amd)\n\t\tdefine([\"echarts\"], factory);\n\telse if(typeof exports === 'object')\n\t\texports[\"echarts-wordcloud\"] = factory(require(\"echarts\"));\n\telse\n\t\troot[\"echarts-wordcloud\"] = factory(root[\"echarts\"]);\n})(self, function(__WEBPACK_EXTERNAL_MODULE_echarts_lib_echarts__) {\nreturn ","import * as echarts from 'echarts/lib/echarts';\n\necharts.extendSeriesModel({\n type: 'series.wordCloud',\n\n visualStyleAccessPath: 'textStyle',\n visualStyleMapper: function (model) {\n return {\n fill: model.get('color')\n };\n },\n visualDrawType: 'fill',\n\n optionUpdated: function () {\n var option = this.option;\n option.gridSize = Math.max(Math.floor(option.gridSize), 4);\n },\n\n getInitialData: function (option, ecModel) {\n var dimensions = echarts.helper.createDimensions(option.data, {\n coordDimensions: ['value']\n });\n var list = new echarts.List(dimensions, this);\n list.initData(option.data);\n return list;\n },\n\n // Most of options are from https://github.com/timdream/wordcloud2.js/blob/gh-pages/API.md\n defaultOption: {\n maskImage: null,\n\n // Shape can be 'circle', 'cardioid', 'diamond', 'triangle-forward', 'triangle', 'pentagon', 'star'\n shape: 'circle',\n keepAspect: false,\n\n left: 'center',\n\n top: 'center',\n\n width: '70%',\n\n height: '80%',\n\n sizeRange: [12, 60],\n\n rotationRange: [-90, 90],\n\n rotationStep: 45,\n\n gridSize: 8,\n\n drawOutOfBound: false,\n shrinkToFit: false,\n\n textStyle: {\n fontWeight: 'normal'\n }\n }\n});\n","import * as echarts from 'echarts/lib/echarts';\n\necharts.extendChartView({\n type: 'wordCloud',\n\n render: function (seriesModel, ecModel, api) {\n var group = this.group;\n group.removeAll();\n\n var data = seriesModel.getData();\n\n var gridSize = seriesModel.get('gridSize');\n\n seriesModel.layoutInstance.ondraw = function (text, size, dataIdx, drawn) {\n var itemModel = data.getItemModel(dataIdx);\n var textStyleModel = itemModel.getModel('textStyle');\n\n var textEl = new echarts.graphic.Text({\n style: echarts.helper.createTextStyle(textStyleModel),\n scaleX: 1 / drawn.info.mu,\n scaleY: 1 / drawn.info.mu,\n x: (drawn.gx + drawn.info.gw / 2) * gridSize,\n y: (drawn.gy + drawn.info.gh / 2) * gridSize,\n rotation: drawn.rot\n });\n textEl.setStyle({\n x: drawn.info.fillTextOffsetX,\n y: drawn.info.fillTextOffsetY + size * 0.5,\n text: text,\n verticalAlign: 'middle',\n fill: data.getItemVisual(dataIdx, 'style').fill,\n fontSize: size\n });\n\n group.add(textEl);\n\n data.setItemGraphicEl(dataIdx, textEl);\n\n textEl.ensureState('emphasis').style = echarts.helper.createTextStyle(\n itemModel.getModel(['emphasis', 'textStyle']),\n {\n state: 'emphasis'\n }\n );\n textEl.ensureState('blur').style = echarts.helper.createTextStyle(\n itemModel.getModel(['blur', 'textStyle']),\n {\n state: 'blur'\n }\n );\n\n echarts.helper.enableHoverEmphasis(\n textEl,\n itemModel.get(['emphasis', 'focus']),\n itemModel.get(['emphasis', 'blurScope'])\n );\n\n textEl.stateTransition = {\n duration: seriesModel.get('animation')\n ? seriesModel.get(['stateAnimation', 'duration'])\n : 0,\n easing: seriesModel.get(['stateAnimation', 'easing'])\n };\n // TODO\n textEl.__highDownDispatcher = true;\n };\n\n this._model = seriesModel;\n },\n\n remove: function () {\n this.group.removeAll();\n\n this._model.layoutInstance.dispose();\n },\n\n dispose: function () {\n this._model.layoutInstance.dispose();\n }\n});\n","/*!\n * wordcloud2.js\n * http://timdream.org/wordcloud2.js/\n *\n * Copyright 2011 - 2019 Tim Guan-tin Chien and contributors.\n * Released under the MIT license\n */\n\n'use strict';\n\n// setImmediate\nif (!window.setImmediate) {\n window.setImmediate = (function setupSetImmediate() {\n return (\n window.msSetImmediate ||\n window.webkitSetImmediate ||\n window.mozSetImmediate ||\n window.oSetImmediate ||\n (function setupSetZeroTimeout() {\n if (!window.postMessage || !window.addEventListener) {\n return null;\n }\n\n var callbacks = [undefined];\n var message = 'zero-timeout-message';\n\n // Like setTimeout, but only takes a function argument. There's\n // no time argument (always zero) and no arguments (you have to\n // use a closure).\n var setZeroTimeout = function setZeroTimeout(callback) {\n var id = callbacks.length;\n callbacks.push(callback);\n window.postMessage(message + id.toString(36), '*');\n\n return id;\n };\n\n window.addEventListener(\n 'message',\n function setZeroTimeoutMessage(evt) {\n // Skipping checking event source, retarded IE confused this window\n // object with another in the presence of iframe\n if (\n typeof evt.data !== 'string' ||\n evt.data.substr(0, message.length) !== message /* ||\n evt.source !== window */\n ) {\n return;\n }\n\n evt.stopImmediatePropagation();\n\n var id = parseInt(evt.data.substr(message.length), 36);\n if (!callbacks[id]) {\n return;\n }\n\n callbacks[id]();\n callbacks[id] = undefined;\n },\n true\n );\n\n /* specify clearImmediate() here since we need the scope */\n window.clearImmediate = function clearZeroTimeout(id) {\n if (!callbacks[id]) {\n return;\n }\n\n callbacks[id] = undefined;\n };\n\n return setZeroTimeout;\n })() ||\n // fallback\n function setImmediateFallback(fn) {\n window.setTimeout(fn, 0);\n }\n );\n })();\n}\n\nif (!window.clearImmediate) {\n window.clearImmediate = (function setupClearImmediate() {\n return (\n window.msClearImmediate ||\n window.webkitClearImmediate ||\n window.mozClearImmediate ||\n window.oClearImmediate ||\n // \"clearZeroTimeout\" is implement on the previous block ||\n // fallback\n function clearImmediateFallback(timer) {\n window.clearTimeout(timer);\n }\n );\n })();\n}\n\n// Check if WordCloud can run on this browser\nvar isSupported = (function isSupported() {\n var canvas = document.createElement('canvas');\n if (!canvas || !canvas.getContext) {\n return false;\n }\n\n var ctx = canvas.getContext('2d');\n if (!ctx) {\n return false;\n }\n if (!ctx.getImageData) {\n return false;\n }\n if (!ctx.fillText) {\n return false;\n }\n\n if (!Array.prototype.some) {\n return false;\n }\n if (!Array.prototype.push) {\n return false;\n }\n\n return true;\n})();\n\n// Find out if the browser impose minium font size by\n// drawing small texts on a canvas and measure it's width.\nvar minFontSize = (function getMinFontSize() {\n if (!isSupported) {\n return;\n }\n\n var ctx = document.createElement('canvas').getContext('2d');\n\n // start from 20\n var size = 20;\n\n // two sizes to measure\n var hanWidth, mWidth;\n\n while (size) {\n ctx.font = size.toString(10) + 'px sans-serif';\n if (\n ctx.measureText('\\uFF37').width === hanWidth &&\n ctx.measureText('m').width === mWidth\n ) {\n return size + 1;\n }\n\n hanWidth = ctx.measureText('\\uFF37').width;\n mWidth = ctx.measureText('m').width;\n\n size--;\n }\n\n return 0;\n})();\n\nvar getItemExtraData = function (item) {\n if (Array.isArray(item)) {\n var itemCopy = item.slice();\n // remove data we already have (word and weight)\n itemCopy.splice(0, 2);\n return itemCopy;\n } else {\n return [];\n }\n};\n\n// Based on http://jsfromhell.com/array/shuffle\nvar shuffleArray = function shuffleArray(arr) {\n for (var j, x, i = arr.length; i; ) {\n j = Math.floor(Math.random() * i);\n x = arr[--i];\n arr[i] = arr[j];\n arr[j] = x;\n }\n return arr;\n};\n\nvar timer = {};\nvar WordCloud = function WordCloud(elements, options) {\n if (!isSupported) {\n return;\n }\n\n var timerId = Math.floor(Math.random() * Date.now());\n\n if (!Array.isArray(elements)) {\n elements = [elements];\n }\n\n elements.forEach(function (el, i) {\n if (typeof el === 'string') {\n elements[i] = document.getElementById(el);\n if (!elements[i]) {\n throw new Error('The element id specified is not found.');\n }\n } else if (!el.tagName && !el.appendChild) {\n throw new Error(\n 'You must pass valid HTML elements, or ID of the element.'\n );\n }\n });\n\n /* Default values to be overwritten by options object */\n var settings = {\n list: [],\n fontFamily:\n '\"Trebuchet MS\", \"Heiti TC\", \"微軟正黑體\", ' +\n '\"Arial Unicode MS\", \"Droid Fallback Sans\", sans-serif',\n fontWeight: 'normal',\n color: 'random-dark',\n minSize: 0, // 0 to disable\n weightFactor: 1,\n clearCanvas: true,\n backgroundColor: '#fff', // opaque white = rgba(255, 255, 255, 1)\n\n gridSize: 8,\n drawOutOfBound: false,\n shrinkToFit: false,\n origin: null,\n\n drawMask: false,\n maskColor: 'rgba(255,0,0,0.3)',\n maskGapWidth: 0.3,\n\n layoutAnimation: true,\n\n wait: 0,\n abortThreshold: 0, // disabled\n abort: function noop() {},\n\n minRotation: -Math.PI / 2,\n maxRotation: Math.PI / 2,\n rotationStep: 0.1,\n\n shuffle: true,\n rotateRatio: 0.1,\n\n shape: 'circle',\n ellipticity: 0.65,\n\n classes: null,\n\n hover: null,\n click: null\n };\n\n if (options) {\n for (var key in options) {\n if (key in settings) {\n settings[key] = options[key];\n }\n }\n }\n\n /* Convert weightFactor into a function */\n if (typeof settings.weightFactor !== 'function') {\n var factor = settings.weightFactor;\n settings.weightFactor = function weightFactor(pt) {\n return pt * factor; // in px\n };\n }\n\n /* Convert shape into a function */\n if (typeof settings.shape !== 'function') {\n switch (settings.shape) {\n case 'circle':\n /* falls through */\n default:\n // 'circle' is the default and a shortcut in the code loop.\n settings.shape = 'circle';\n break;\n\n case 'cardioid':\n settings.shape = function shapeCardioid(theta) {\n return 1 - Math.sin(theta);\n };\n break;\n\n /*\n To work out an X-gon, one has to calculate \"m\",\n where 1/(cos(2*PI/X)+m*sin(2*PI/X)) = 1/(cos(0)+m*sin(0))\n http://www.wolframalpha.com/input/?i=1%2F%28cos%282*PI%2FX%29%2Bm*sin%28\n 2*PI%2FX%29%29+%3D+1%2F%28cos%280%29%2Bm*sin%280%29%29\n Copy the solution into polar equation r = 1/(cos(t') + m*sin(t'))\n where t' equals to mod(t, 2PI/X);\n */\n\n case 'diamond':\n // http://www.wolframalpha.com/input/?i=plot+r+%3D+1%2F%28cos%28mod+\n // %28t%2C+PI%2F2%29%29%2Bsin%28mod+%28t%2C+PI%2F2%29%29%29%2C+t+%3D\n // +0+..+2*PI\n settings.shape = function shapeSquare(theta) {\n var thetaPrime = theta % ((2 * Math.PI) / 4);\n return 1 / (Math.cos(thetaPrime) + Math.sin(thetaPrime));\n };\n break;\n\n case 'square':\n // http://www.wolframalpha.com/input/?i=plot+r+%3D+min(1%2Fabs(cos(t\n // )),1%2Fabs(sin(t)))),+t+%3D+0+..+2*PI\n settings.shape = function shapeSquare(theta) {\n return Math.min(\n 1 / Math.abs(Math.cos(theta)),\n 1 / Math.abs(Math.sin(theta))\n );\n };\n break;\n\n case 'triangle-forward':\n // http://www.wolframalpha.com/input/?i=plot+r+%3D+1%2F%28cos%28mod+\n // %28t%2C+2*PI%2F3%29%29%2Bsqrt%283%29sin%28mod+%28t%2C+2*PI%2F3%29\n // %29%29%2C+t+%3D+0+..+2*PI\n settings.shape = function shapeTriangle(theta) {\n var thetaPrime = theta % ((2 * Math.PI) / 3);\n return (\n 1 / (Math.cos(thetaPrime) + Math.sqrt(3) * Math.sin(thetaPrime))\n );\n };\n break;\n\n case 'triangle':\n case 'triangle-upright':\n settings.shape = function shapeTriangle(theta) {\n var thetaPrime = (theta + (Math.PI * 3) / 2) % ((2 * Math.PI) / 3);\n return (\n 1 / (Math.cos(thetaPrime) + Math.sqrt(3) * Math.sin(thetaPrime))\n );\n };\n break;\n\n case 'pentagon':\n settings.shape = function shapePentagon(theta) {\n var thetaPrime = (theta + 0.955) % ((2 * Math.PI) / 5);\n return 1 / (Math.cos(thetaPrime) + 0.726543 * Math.sin(thetaPrime));\n };\n break;\n\n case 'star':\n settings.shape = function shapeStar(theta) {\n var thetaPrime = (theta + 0.955) % ((2 * Math.PI) / 10);\n if (\n ((theta + 0.955) % ((2 * Math.PI) / 5)) - (2 * Math.PI) / 10 >=\n 0\n ) {\n return (\n 1 /\n (Math.cos((2 * Math.PI) / 10 - thetaPrime) +\n 3.07768 * Math.sin((2 * Math.PI) / 10 - thetaPrime))\n );\n } else {\n return 1 / (Math.cos(thetaPrime) + 3.07768 * Math.sin(thetaPrime));\n }\n };\n break;\n }\n }\n\n /* Make sure gridSize is a whole number and is not smaller than 4px */\n settings.gridSize = Math.max(Math.floor(settings.gridSize), 4);\n\n /* shorthand */\n var g = settings.gridSize;\n var maskRectWidth = g - settings.maskGapWidth;\n\n /* normalize rotation settings */\n var rotationRange = Math.abs(settings.maxRotation - settings.minRotation);\n var minRotation = Math.min(settings.maxRotation, settings.minRotation);\n var rotationStep = settings.rotationStep;\n\n /* information/object available to all functions, set when start() */\n var grid, // 2d array containing filling information\n ngx,\n ngy, // width and height of the grid\n center, // position of the center of the cloud\n maxRadius;\n\n /* timestamp for measuring each putWord() action */\n var escapeTime;\n\n /* function for getting the color of the text */\n var getTextColor;\n function randomHslColor(min, max) {\n return (\n 'hsl(' +\n (Math.random() * 360).toFixed() +\n ',' +\n (Math.random() * 30 + 70).toFixed() +\n '%,' +\n (Math.random() * (max - min) + min).toFixed() +\n '%)'\n );\n }\n switch (settings.color) {\n case 'random-dark':\n getTextColor = function getRandomDarkColor() {\n return randomHslColor(10, 50);\n };\n break;\n\n case 'random-light':\n getTextColor = function getRandomLightColor() {\n return randomHslColor(50, 90);\n };\n break;\n\n default:\n if (typeof settings.color === 'function') {\n getTextColor = settings.color;\n }\n break;\n }\n\n /* function for getting the font-weight of the text */\n var getTextFontWeight;\n if (typeof settings.fontWeight === 'function') {\n getTextFontWeight = settings.fontWeight;\n }\n\n /* function for getting the classes of the text */\n var getTextClasses = null;\n if (typeof settings.classes === 'function') {\n getTextClasses = settings.classes;\n }\n\n /* Interactive */\n var interactive = false;\n var infoGrid = [];\n var hovered;\n\n var getInfoGridFromMouseTouchEvent = function getInfoGridFromMouseTouchEvent(\n evt\n ) {\n var canvas = evt.currentTarget;\n var rect = canvas.getBoundingClientRect();\n var clientX;\n var clientY;\n /** Detect if touches are available */\n if (evt.touches) {\n clientX = evt.touches[0].clientX;\n clientY = evt.touches[0].clientY;\n } else {\n clientX = evt.clientX;\n clientY = evt.clientY;\n }\n var eventX = clientX - rect.left;\n var eventY = clientY - rect.top;\n\n var x = Math.floor((eventX * (canvas.width / rect.width || 1)) / g);\n var y = Math.floor((eventY * (canvas.height / rect.height || 1)) / g);\n\n if (!infoGrid[x]) {\n return null\n }\n\n return infoGrid[x][y];\n };\n\n var wordcloudhover = function wordcloudhover(evt) {\n var info = getInfoGridFromMouseTouchEvent(evt);\n\n if (hovered === info) {\n return;\n }\n\n hovered = info;\n if (!info) {\n settings.hover(undefined, undefined, evt);\n\n return;\n }\n\n settings.hover(info.item, info.dimension, evt);\n };\n\n var wordcloudclick = function wordcloudclick(evt) {\n var info = getInfoGridFromMouseTouchEvent(evt);\n if (!info) {\n return;\n }\n\n settings.click(info.item, info.dimension, evt);\n evt.preventDefault();\n };\n\n /* Get points on the grid for a given radius away from the center */\n var pointsAtRadius = [];\n var getPointsAtRadius = function getPointsAtRadius(radius) {\n if (pointsAtRadius[radius]) {\n return pointsAtRadius[radius];\n }\n\n // Look for these number of points on each radius\n var T = radius * 8;\n\n // Getting all the points at this radius\n var t = T;\n var points = [];\n\n if (radius === 0) {\n points.push([center[0], center[1], 0]);\n }\n\n while (t--) {\n // distort the radius to put the cloud in shape\n var rx = 1;\n if (settings.shape !== 'circle') {\n rx = settings.shape((t / T) * 2 * Math.PI); // 0 to 1\n }\n\n // Push [x, y, t]; t is used solely for getTextColor()\n points.push([\n center[0] + radius * rx * Math.cos((-t / T) * 2 * Math.PI),\n center[1] +\n radius * rx * Math.sin((-t / T) * 2 * Math.PI) * settings.ellipticity,\n (t / T) * 2 * Math.PI\n ]);\n }\n\n pointsAtRadius[radius] = points;\n return points;\n };\n\n /* Return true if we had spent too much time */\n var exceedTime = function exceedTime() {\n return (\n settings.abortThreshold > 0 &&\n new Date().getTime() - escapeTime > settings.abortThreshold\n );\n };\n\n /* Get the deg of rotation according to settings, and luck. */\n var getRotateDeg = function getRotateDeg() {\n if (settings.rotateRatio === 0) {\n return 0;\n }\n\n if (Math.random() > settings.rotateRatio) {\n return 0;\n }\n\n if (rotationRange === 0) {\n return minRotation;\n }\n\n return minRotation + Math.round(Math.random() * rotationRange / rotationStep) * rotationStep;\n };\n\n var getTextInfo = function getTextInfo(\n word,\n weight,\n rotateDeg,\n extraDataArray\n ) {\n // calculate the acutal font size\n // fontSize === 0 means weightFactor function wants the text skipped,\n // and size < minSize means we cannot draw the text.\n var debug = false;\n var fontSize = settings.weightFactor(weight);\n if (fontSize <= settings.minSize) {\n return false;\n }\n\n // Scale factor here is to make sure fillText is not limited by\n // the minium font size set by browser.\n // It will always be 1 or 2n.\n var mu = 1;\n if (fontSize < minFontSize) {\n mu = (function calculateScaleFactor() {\n var mu = 2;\n while (mu * fontSize < minFontSize) {\n mu += 2;\n }\n return mu;\n })();\n }\n\n // Get fontWeight that will be used to set fctx.font\n var fontWeight;\n if (getTextFontWeight) {\n fontWeight = getTextFontWeight(word, weight, fontSize, extraDataArray);\n } else {\n fontWeight = settings.fontWeight;\n }\n\n var fcanvas = document.createElement('canvas');\n var fctx = fcanvas.getContext('2d', { willReadFrequently: true });\n\n fctx.font =\n fontWeight +\n ' ' +\n (fontSize * mu).toString(10) +\n 'px ' +\n settings.fontFamily;\n\n // Estimate the dimension of the text with measureText().\n var fw = fctx.measureText(word).width / mu;\n var fh =\n Math.max(\n fontSize * mu,\n fctx.measureText('m').width,\n fctx.measureText('\\uFF37').width\n ) / mu;\n\n // Create a boundary box that is larger than our estimates,\n // so text don't get cut of (it sill might)\n var boxWidth = fw + fh * 2;\n var boxHeight = fh * 3;\n var fgw = Math.ceil(boxWidth / g);\n var fgh = Math.ceil(boxHeight / g);\n boxWidth = fgw * g;\n boxHeight = fgh * g;\n\n // Calculate the proper offsets to make the text centered at\n // the preferred position.\n\n // This is simply half of the width.\n var fillTextOffsetX = -fw / 2;\n // Instead of moving the box to the exact middle of the preferred\n // position, for Y-offset we move 0.4 instead, so Latin alphabets look\n // vertical centered.\n var fillTextOffsetY = -fh * 0.4;\n\n // Calculate the actual dimension of the canvas, considering the rotation.\n var cgh = Math.ceil(\n (boxWidth * Math.abs(Math.sin(rotateDeg)) +\n boxHeight * Math.abs(Math.cos(rotateDeg))) /\n g\n );\n var cgw = Math.ceil(\n (boxWidth * Math.abs(Math.cos(rotateDeg)) +\n boxHeight * Math.abs(Math.sin(rotateDeg))) /\n g\n );\n var width = cgw * g;\n var height = cgh * g;\n\n fcanvas.setAttribute('width', width);\n fcanvas.setAttribute('height', height);\n\n if (debug) {\n // Attach fcanvas to the DOM\n document.body.appendChild(fcanvas);\n // Save it's state so that we could restore and draw the grid correctly.\n fctx.save();\n }\n\n // Scale the canvas with |mu|.\n fctx.scale(1 / mu, 1 / mu);\n fctx.translate((width * mu) / 2, (height * mu) / 2);\n fctx.rotate(-rotateDeg);\n\n // Once the width/height is set, ctx info will be reset.\n // Set it again here.\n fctx.font =\n fontWeight +\n ' ' +\n (fontSize * mu).toString(10) +\n 'px ' +\n settings.fontFamily;\n\n // Fill the text into the fcanvas.\n // XXX: We cannot because textBaseline = 'top' here because\n // Firefox and Chrome uses different default line-height for canvas.\n // Please read https://bugzil.la/737852#c6.\n // Here, we use textBaseline = 'middle' and draw the text at exactly\n // 0.5 * fontSize lower.\n fctx.fillStyle = '#000';\n fctx.textBaseline = 'middle';\n fctx.fillText(\n word,\n fillTextOffsetX * mu,\n (fillTextOffsetY + fontSize * 0.5) * mu\n );\n\n // Get the pixels of the text\n var imageData = fctx.getImageData(0, 0, width, height).data;\n\n if (exceedTime()) {\n return false;\n }\n\n if (debug) {\n // Draw the box of the original estimation\n fctx.strokeRect(fillTextOffsetX * mu, fillTextOffsetY, fw * mu, fh * mu);\n fctx.restore();\n }\n\n // Read the pixels and save the information to the occupied array\n var occupied = [];\n var gx = cgw;\n var gy, x, y;\n var bounds = [cgh / 2, cgw / 2, cgh / 2, cgw / 2];\n while (gx--) {\n gy = cgh;\n while (gy--) {\n y = g;\n /* eslint no-labels: ['error', { 'allowLoop': true }] */\n singleGridLoop: while (y--) {\n x = g;\n while (x--) {\n if (imageData[((gy * g + y) * width + (gx * g + x)) * 4 + 3]) {\n occupied.push([gx, gy]);\n\n if (gx < bounds[3]) {\n bounds[3] = gx;\n }\n if (gx > bounds[1]) {\n bounds[1] = gx;\n }\n if (gy < bounds[0]) {\n bounds[0] = gy;\n }\n if (gy > bounds[2]) {\n bounds[2] = gy;\n }\n\n if (debug) {\n fctx.fillStyle = 'rgba(255, 0, 0, 0.5)';\n fctx.fillRect(gx * g, gy * g, g - 0.5, g - 0.5);\n }\n break singleGridLoop;\n }\n }\n }\n if (debug) {\n fctx.fillStyle = 'rgba(0, 0, 255, 0.5)';\n fctx.fillRect(gx * g, gy * g, g - 0.5, g - 0.5);\n }\n }\n }\n\n if (debug) {\n fctx.fillStyle = 'rgba(0, 255, 0, 0.5)';\n fctx.fillRect(\n bounds[3] * g,\n bounds[0] * g,\n (bounds[1] - bounds[3] + 1) * g,\n (bounds[2] - bounds[0] + 1) * g\n );\n }\n\n // Return information needed to create the text on the real canvas\n return {\n mu: mu,\n occupied: occupied,\n bounds: bounds,\n gw: cgw,\n gh: cgh,\n fillTextOffsetX: fillTextOffsetX,\n fillTextOffsetY: fillTextOffsetY,\n fillTextWidth: fw,\n fillTextHeight: fh,\n fontSize: fontSize\n };\n };\n\n /* Determine if there is room available in the given dimension */\n var canFitText = function canFitText(gx, gy, gw, gh, occupied) {\n // Go through the occupied points,\n // return false if the space is not available.\n var i = occupied.length;\n while (i--) {\n var px = gx + occupied[i][0];\n var py = gy + occupied[i][1];\n\n if (px >= ngx || py >= ngy || px < 0 || py < 0) {\n if (!settings.drawOutOfBound) {\n return false;\n }\n continue;\n }\n\n if (!grid[px][py]) {\n return false;\n }\n }\n return true;\n };\n\n /* Actually draw the text on the grid */\n var drawText = function drawText(\n gx,\n gy,\n info,\n word,\n weight,\n distance,\n theta,\n rotateDeg,\n attributes,\n extraDataArray\n ) {\n var fontSize = info.fontSize;\n var color;\n if (getTextColor) {\n color = getTextColor(\n word,\n weight,\n fontSize,\n distance,\n theta,\n extraDataArray\n );\n } else {\n color = settings.color;\n }\n\n // get fontWeight that will be used to set ctx.font and font style rule\n var fontWeight;\n if (getTextFontWeight) {\n fontWeight = getTextFontWeight(word, weight, fontSize, extraDataArray);\n } else {\n fontWeight = settings.fontWeight;\n }\n\n var classes;\n if (getTextClasses) {\n classes = getTextClasses(word, weight, fontSize, extraDataArray);\n } else {\n classes = settings.classes;\n }\n\n elements.forEach(function (el) {\n if (el.getContext) {\n var ctx = el.getContext('2d');\n var mu = info.mu;\n\n // Save the current state before messing it\n ctx.save();\n ctx.scale(1 / mu, 1 / mu);\n\n ctx.font =\n fontWeight +\n ' ' +\n (fontSize * mu).toString(10) +\n 'px ' +\n settings.fontFamily;\n ctx.fillStyle = color;\n\n // Translate the canvas position to the origin coordinate of where\n // the text should be put.\n ctx.translate((gx + info.gw / 2) * g * mu, (gy + info.gh / 2) * g * mu);\n\n if (rotateDeg !== 0) {\n ctx.rotate(-rotateDeg);\n }\n\n // Finally, fill the text.\n\n // XXX: We cannot because textBaseline = 'top' here because\n // Firefox and Chrome uses different default line-height for canvas.\n // Please read https://bugzil.la/737852#c6.\n // Here, we use textBaseline = 'middle' and draw the text at exactly\n // 0.5 * fontSize lower.\n ctx.textBaseline = 'middle';\n ctx.fillText(\n word,\n info.fillTextOffsetX * mu,\n (info.fillTextOffsetY + fontSize * 0.5) * mu\n );\n\n // The below box is always matches how s are positioned\n /* ctx.strokeRect(info.fillTextOffsetX, info.fillTextOffsetY,\n info.fillTextWidth, info.fillTextHeight); */\n\n // Restore the state.\n ctx.restore();\n } else {\n // drawText on DIV element\n var span = document.createElement('span');\n var transformRule = '';\n transformRule = 'rotate(' + (-rotateDeg / Math.PI) * 180 + 'deg) ';\n if (info.mu !== 1) {\n transformRule +=\n 'translateX(-' +\n info.fillTextWidth / 4 +\n 'px) ' +\n 'scale(' +\n 1 / info.mu +\n ')';\n }\n var styleRules = {\n position: 'absolute',\n display: 'block',\n font:\n fontWeight + ' ' + fontSize * info.mu + 'px ' + settings.fontFamily,\n left: (gx + info.gw / 2) * g + info.fillTextOffsetX + 'px',\n top: (gy + info.gh / 2) * g + info.fillTextOffsetY + 'px',\n width: info.fillTextWidth + 'px',\n height: info.fillTextHeight + 'px',\n lineHeight: fontSize + 'px',\n whiteSpace: 'nowrap',\n transform: transformRule,\n webkitTransform: transformRule,\n msTransform: transformRule,\n transformOrigin: '50% 40%',\n webkitTransformOrigin: '50% 40%',\n msTransformOrigin: '50% 40%'\n };\n if (color) {\n styleRules.color = color;\n }\n span.textContent = word;\n for (var cssProp in styleRules) {\n span.style[cssProp] = styleRules[cssProp];\n }\n if (attributes) {\n for (var attribute in attributes) {\n span.setAttribute(attribute, attributes[attribute]);\n }\n }\n if (classes) {\n span.className += classes;\n }\n el.appendChild(span);\n }\n });\n };\n\n /* Help function to updateGrid */\n var fillGridAt = function fillGridAt(x, y, drawMask, dimension, item) {\n if (x >= ngx || y >= ngy || x < 0 || y < 0) {\n return;\n }\n\n grid[x][y] = false;\n\n if (drawMask) {\n var ctx = elements[0].getContext('2d');\n ctx.fillRect(x * g, y * g, maskRectWidth, maskRectWidth);\n }\n\n if (interactive) {\n infoGrid[x][y] = { item: item, dimension: dimension };\n }\n };\n\n /* Update the filling information of the given space with occupied points.\n Draw the mask on the canvas if necessary. */\n var updateGrid = function updateGrid(gx, gy, gw, gh, info, item) {\n var occupied = info.occupied;\n var drawMask = settings.drawMask;\n var ctx;\n if (drawMask) {\n ctx = elements[0].getContext('2d');\n ctx.save();\n ctx.fillStyle = settings.maskColor;\n }\n\n var dimension;\n if (interactive) {\n var bounds = info.bounds;\n dimension = {\n x: (gx + bounds[3]) * g,\n y: (gy + bounds[0]) * g,\n w: (bounds[1] - bounds[3] + 1) * g,\n h: (bounds[2] - bounds[0] + 1) * g\n };\n }\n\n var i = occupied.length;\n while (i--) {\n var px = gx + occupied[i][0];\n var py = gy + occupied[i][1];\n\n if (px >= ngx || py >= ngy || px < 0 || py < 0) {\n continue;\n }\n\n fillGridAt(px, py, drawMask, dimension, item);\n }\n\n if (drawMask) {\n ctx.restore();\n }\n };\n\n /* putWord() processes each item on the list,\n calculate it's size and determine it's position, and actually\n put it on the canvas. */\n var putWord = function putWord(item, loopIndex) {\n if (loopIndex > 20) {\n return null;\n }\n\n var word, weight, attributes;\n if (Array.isArray(item)) {\n word = item[0];\n weight = item[1];\n } else {\n word = item.word;\n weight = item.weight;\n attributes = item.attributes;\n }\n var rotateDeg = getRotateDeg();\n\n var extraDataArray = getItemExtraData(item);\n\n // get info needed to put the text onto the canvas\n var info = getTextInfo(word, weight, rotateDeg, extraDataArray);\n\n // not getting the info means we shouldn't be drawing this one.\n if (!info) {\n return false;\n }\n\n if (exceedTime()) {\n return false;\n }\n\n // If drawOutOfBound is set to false,\n // skip the loop if we have already know the bounding box of\n // word is larger than the canvas.\n if (!settings.drawOutOfBound && !settings.shrinkToFit) {\n var bounds = info.bounds;\n if (bounds[1] - bounds[3] + 1 > ngx || bounds[2] - bounds[0] + 1 > ngy) {\n return false;\n }\n }\n\n // Determine the position to put the text by\n // start looking for the nearest points\n var r = maxRadius + 1;\n\n var tryToPutWordAtPoint = function (gxy) {\n var gx = Math.floor(gxy[0] - info.gw / 2);\n var gy = Math.floor(gxy[1] - info.gh / 2);\n var gw = info.gw;\n var gh = info.gh;\n\n // If we cannot fit the text at this position, return false\n // and go to the next position.\n if (!canFitText(gx, gy, gw, gh, info.occupied)) {\n return false;\n }\n\n // Actually put the text on the canvas\n drawText(\n gx,\n gy,\n info,\n word,\n weight,\n maxRadius - r,\n gxy[2],\n rotateDeg,\n attributes,\n extraDataArray\n );\n\n // Mark the spaces on the grid as filled\n updateGrid(gx, gy, gw, gh, info, item);\n\n return {\n gx: gx,\n gy: gy,\n rot: rotateDeg,\n info: info\n };\n };\n\n while (r--) {\n var points = getPointsAtRadius(maxRadius - r);\n\n if (settings.shuffle) {\n points = [].concat(points);\n shuffleArray(points);\n }\n\n // Try to fit the words by looking at each point.\n // array.some() will stop and return true\n // when putWordAtPoint() returns true.\n for (var i = 0; i < points.length; i++) {\n var res = tryToPutWordAtPoint(points[i]);\n if (res) {\n return res;\n }\n }\n\n // var drawn = points.some(tryToPutWordAtPoint);\n // if (drawn) {\n // // leave putWord() and return true\n // return true;\n // }\n }\n\n if (settings.shrinkToFit) {\n if (Array.isArray(item)) {\n item[1] = (item[1] * 3) / 4;\n } else {\n item.weight = (item.weight * 3) / 4;\n }\n return putWord(item, loopIndex + 1);\n }\n\n // we tried all distances but text won't fit, return null\n return null;\n };\n\n /* Send DOM event to all elements. Will stop sending event and return\n if the previous one is canceled (for cancelable events). */\n var sendEvent = function sendEvent(type, cancelable, details) {\n if (cancelable) {\n return !elements.some(function (el) {\n var event = new CustomEvent(type, {\n detail: details || {}\n });\n return !el.dispatchEvent(event);\n }, this);\n } else {\n elements.forEach(function (el) {\n var event = new CustomEvent(type, {\n detail: details || {}\n });\n el.dispatchEvent(event);\n }, this);\n }\n };\n\n /* Start drawing on a canvas */\n var start = function start() {\n // For dimensions, clearCanvas etc.,\n // we only care about the first element.\n var canvas = elements[0];\n\n if (canvas.getContext) {\n ngx = Math.ceil(canvas.width / g);\n ngy = Math.ceil(canvas.height / g);\n } else {\n var rect = canvas.getBoundingClientRect();\n ngx = Math.ceil(rect.width / g);\n ngy = Math.ceil(rect.height / g);\n }\n\n // Sending a wordcloudstart event which cause the previous loop to stop.\n // Do nothing if the event is canceled.\n if (!sendEvent('wordcloudstart', true)) {\n return;\n }\n\n // Determine the center of the word cloud\n center = settings.origin\n ? [settings.origin[0] / g, settings.origin[1] / g]\n : [ngx / 2, ngy / 2];\n\n // Maxium radius to look for space\n maxRadius = Math.floor(Math.sqrt(ngx * ngx + ngy * ngy));\n\n /* Clear the canvas only if the clearCanvas is set,\n if not, update the grid to the current canvas state */\n grid = [];\n\n var gx, gy, i;\n if (!canvas.getContext || settings.clearCanvas) {\n elements.forEach(function (el) {\n if (el.getContext) {\n var ctx = el.getContext('2d');\n ctx.fillStyle = settings.backgroundColor;\n ctx.clearRect(0, 0, ngx * (g + 1), ngy * (g + 1));\n ctx.fillRect(0, 0, ngx * (g + 1), ngy * (g + 1));\n } else {\n el.textContent = '';\n el.style.backgroundColor = settings.backgroundColor;\n el.style.position = 'relative';\n }\n });\n\n /* fill the grid with empty state */\n gx = ngx;\n while (gx--) {\n grid[gx] = [];\n gy = ngy;\n while (gy--) {\n grid[gx][gy] = true;\n }\n }\n } else {\n /* Determine bgPixel by creating\n another canvas and fill the specified background color. */\n var bctx = document.createElement('canvas').getContext('2d');\n\n bctx.fillStyle = settings.backgroundColor;\n bctx.fillRect(0, 0, 1, 1);\n var bgPixel = bctx.getImageData(0, 0, 1, 1).data;\n\n /* Read back the pixels of the canvas we got to tell which part of the\n canvas is empty.\n (no clearCanvas only works with a canvas, not divs) */\n var imageData = canvas\n .getContext('2d')\n .getImageData(0, 0, ngx * g, ngy * g).data;\n\n gx = ngx;\n var x, y;\n while (gx--) {\n grid[gx] = [];\n gy = ngy;\n while (gy--) {\n y = g;\n /* eslint no-labels: ['error', { 'allowLoop': true }] */\n singleGridLoop: while (y--) {\n x = g;\n while (x--) {\n i = 4;\n while (i--) {\n if (\n imageData[((gy * g + y) * ngx * g + (gx * g + x)) * 4 + i] !==\n bgPixel[i]\n ) {\n grid[gx][gy] = false;\n break singleGridLoop;\n }\n }\n }\n }\n if (grid[gx][gy] !== false) {\n grid[gx][gy] = true;\n }\n }\n }\n\n imageData = bctx = bgPixel = undefined;\n }\n\n // fill the infoGrid with empty state if we need it\n if (settings.hover || settings.click) {\n interactive = true;\n\n /* fill the grid with empty state */\n gx = ngx + 1;\n while (gx--) {\n infoGrid[gx] = [];\n }\n\n if (settings.hover) {\n canvas.addEventListener('mousemove', wordcloudhover);\n }\n\n if (settings.click) {\n canvas.addEventListener('click', wordcloudclick);\n canvas.addEventListener('touchstart', wordcloudclick);\n canvas.addEventListener('touchend', function (e) {\n e.preventDefault();\n });\n canvas.style.webkitTapHighlightColor = 'rgba(0, 0, 0, 0)';\n }\n\n canvas.addEventListener('wordcloudstart', function stopInteraction() {\n canvas.removeEventListener('wordcloudstart', stopInteraction);\n\n canvas.removeEventListener('mousemove', wordcloudhover);\n canvas.removeEventListener('click', wordcloudclick);\n hovered = undefined;\n });\n }\n\n i = 0;\n var loopingFunction, stoppingFunction;\n var layouting = true;\n if (!settings.layoutAnimation) {\n loopingFunction = function (cb) {\n cb();\n };\n stoppingFunction = function () {\n layouting = false;\n };\n } else if (settings.wait !== 0) {\n loopingFunction = window.setTimeout;\n stoppingFunction = window.clearTimeout;\n } else {\n loopingFunction = window.setImmediate;\n stoppingFunction = window.clearImmediate;\n }\n\n var addEventListener = function addEventListener(type, listener) {\n elements.forEach(function (el) {\n el.addEventListener(type, listener);\n }, this);\n };\n\n var removeEventListener = function removeEventListener(type, listener) {\n elements.forEach(function (el) {\n el.removeEventListener(type, listener);\n }, this);\n };\n\n var anotherWordCloudStart = function anotherWordCloudStart() {\n removeEventListener('wordcloudstart', anotherWordCloudStart);\n stoppingFunction(timer[timerId]);\n };\n\n addEventListener('wordcloudstart', anotherWordCloudStart);\n\n // At least wait the following code before call the first iteration.\n timer[timerId] = (settings.layoutAnimation ? loopingFunction : setTimeout)(\n function loop() {\n if (!layouting) {\n return;\n }\n if (i >= settings.list.length) {\n stoppingFunction(timer[timerId]);\n sendEvent('wordcloudstop', false);\n removeEventListener('wordcloudstart', anotherWordCloudStart);\n delete timer[timerId];\n return;\n }\n escapeTime = new Date().getTime();\n var drawn = putWord(settings.list[i], 0);\n var canceled = !sendEvent('wordclouddrawn', true, {\n item: settings.list[i],\n drawn: drawn\n });\n if (exceedTime() || canceled) {\n stoppingFunction(timer[timerId]);\n settings.abort();\n sendEvent('wordcloudabort', false);\n sendEvent('wordcloudstop', false);\n removeEventListener('wordcloudstart', anotherWordCloudStart);\n return;\n }\n i++;\n timer[timerId] = loopingFunction(loop, settings.wait);\n },\n settings.wait\n );\n };\n\n // All set, start the drawing\n start();\n};\n\nWordCloud.isSupported = isSupported;\nWordCloud.minFontSize = minFontSize;\n\nexport default WordCloud;\n","import * as echarts from 'echarts/lib/echarts';\n\nimport './WordCloudSeries';\nimport './WordCloudView';\n\nimport wordCloudLayoutHelper from './layout';\n\nif (!wordCloudLayoutHelper.isSupported) {\n throw new Error('Sorry your browser not support wordCloud');\n}\n\n// https://github.com/timdream/wordcloud2.js/blob/c236bee60436e048949f9becc4f0f67bd832dc5c/index.js#L233\nfunction updateCanvasMask(maskCanvas) {\n var ctx = maskCanvas.getContext('2d');\n var imageData = ctx.getImageData(0, 0, maskCanvas.width, maskCanvas.height);\n var newImageData = ctx.createImageData(imageData);\n\n var toneSum = 0;\n var toneCnt = 0;\n for (var i = 0; i < imageData.data.length; i += 4) {\n var alpha = imageData.data[i + 3];\n if (alpha > 128) {\n var tone =\n imageData.data[i] + imageData.data[i + 1] + imageData.data[i + 2];\n toneSum += tone;\n ++toneCnt;\n }\n }\n var threshold = toneSum / toneCnt;\n\n for (var i = 0; i < imageData.data.length; i += 4) {\n var tone =\n imageData.data[i] + imageData.data[i + 1] + imageData.data[i + 2];\n var alpha = imageData.data[i + 3];\n\n if (alpha < 128 || tone > threshold) {\n // Area not to draw\n newImageData.data[i] = 0;\n newImageData.data[i + 1] = 0;\n newImageData.data[i + 2] = 0;\n newImageData.data[i + 3] = 0;\n } else {\n // Area to draw\n // The color must be same with backgroundColor\n newImageData.data[i] = 255;\n newImageData.data[i + 1] = 255;\n newImageData.data[i + 2] = 255;\n newImageData.data[i + 3] = 255;\n }\n }\n\n ctx.putImageData(newImageData, 0, 0);\n}\n\necharts.registerLayout(function (ecModel, api) {\n ecModel.eachSeriesByType('wordCloud', function (seriesModel) {\n var gridRect = echarts.helper.getLayoutRect(\n seriesModel.getBoxLayoutParams(),\n {\n width: api.getWidth(),\n height: api.getHeight()\n }\n );\n\n var keepAspect = seriesModel.get('keepAspect');\n var maskImage = seriesModel.get('maskImage');\n var ratio = maskImage ? maskImage.width / maskImage.height : 1;\n keepAspect && adjustRectAspect(gridRect, ratio);\n\n var data = seriesModel.getData();\n\n var canvas = document.createElement('canvas');\n canvas.width = gridRect.width;\n canvas.height = gridRect.height;\n\n var ctx = canvas.getContext('2d');\n if (maskImage) {\n try {\n ctx.drawImage(maskImage, 0, 0, canvas.width, canvas.height);\n updateCanvasMask(canvas);\n } catch (e) {\n console.error('Invalid mask image');\n console.error(e.toString());\n }\n }\n\n var sizeRange = seriesModel.get('sizeRange');\n var rotationRange = seriesModel.get('rotationRange');\n var valueExtent = data.getDataExtent('value');\n\n var DEGREE_TO_RAD = Math.PI / 180;\n var gridSize = seriesModel.get('gridSize');\n wordCloudLayoutHelper(canvas, {\n list: data\n .mapArray('value', function (value, idx) {\n var itemModel = data.getItemModel(idx);\n return [\n data.getName(idx),\n itemModel.get('textStyle.fontSize', true) ||\n echarts.number.linearMap(value, valueExtent, sizeRange),\n idx\n ];\n })\n .sort(function (a, b) {\n // Sort from large to small in case there is no more room for more words\n return b[1] - a[1];\n }),\n fontFamily:\n seriesModel.get('textStyle.fontFamily') ||\n seriesModel.get('emphasis.textStyle.fontFamily') ||\n ecModel.get('textStyle.fontFamily'),\n fontWeight:\n seriesModel.get('textStyle.fontWeight') ||\n seriesModel.get('emphasis.textStyle.fontWeight') ||\n ecModel.get('textStyle.fontWeight'),\n\n gridSize: gridSize,\n\n ellipticity: gridRect.height / gridRect.width,\n\n minRotation: rotationRange[0] * DEGREE_TO_RAD,\n maxRotation: rotationRange[1] * DEGREE_TO_RAD,\n\n clearCanvas: !maskImage,\n\n rotateRatio: 1,\n\n rotationStep: seriesModel.get('rotationStep') * DEGREE_TO_RAD,\n\n drawOutOfBound: seriesModel.get('drawOutOfBound'),\n shrinkToFit: seriesModel.get('shrinkToFit'),\n\n layoutAnimation: seriesModel.get('layoutAnimation'),\n\n shuffle: false,\n\n shape: seriesModel.get('shape')\n });\n\n function onWordCloudDrawn(e) {\n var item = e.detail.item;\n if (e.detail.drawn && seriesModel.layoutInstance.ondraw) {\n e.detail.drawn.gx += gridRect.x / gridSize;\n e.detail.drawn.gy += gridRect.y / gridSize;\n seriesModel.layoutInstance.ondraw(\n item[0],\n item[1],\n item[2],\n e.detail.drawn\n );\n }\n }\n\n canvas.addEventListener('wordclouddrawn', onWordCloudDrawn);\n\n if (seriesModel.layoutInstance) {\n // Dispose previous\n seriesModel.layoutInstance.dispose();\n }\n\n seriesModel.layoutInstance = {\n ondraw: null,\n\n dispose: function () {\n canvas.removeEventListener('wordclouddrawn', onWordCloudDrawn);\n // Abort\n canvas.addEventListener('wordclouddrawn', function (e) {\n // Prevent default to cancle the event and stop the loop\n e.preventDefault();\n });\n }\n };\n });\n});\n\necharts.registerPreprocessor(function (option) {\n var series = (option || {}).series;\n !echarts.util.isArray(series) && (series = series ? [series] : []);\n\n var compats = ['shadowColor', 'shadowBlur', 'shadowOffsetX', 'shadowOffsetY'];\n\n echarts.util.each(series, function (seriesItem) {\n if (seriesItem && seriesItem.type === 'wordCloud') {\n var textStyle = seriesItem.textStyle || {};\n\n compatTextStyle(textStyle.normal);\n compatTextStyle(textStyle.emphasis);\n }\n });\n\n function compatTextStyle(textStyle) {\n textStyle &&\n echarts.util.each(compats, function (key) {\n if (textStyle.hasOwnProperty(key)) {\n textStyle['text' + echarts.format.capitalFirst(key)] = textStyle[key];\n }\n });\n }\n});\n\nfunction adjustRectAspect(gridRect, aspect) {\n // var outerWidth = gridRect.width + gridRect.x * 2;\n // var outerHeight = gridRect.height + gridRect.y * 2;\n var width = gridRect.width;\n var height = gridRect.height;\n if (width > height * aspect) {\n gridRect.x += (width - height * aspect) / 2;\n gridRect.width = height * aspect;\n } else {\n gridRect.y += (height - width / aspect) / 2;\n gridRect.height = width / aspect;\n }\n}\n","import './src/wordCloud';\n","module.exports = __WEBPACK_EXTERNAL_MODULE_echarts_lib_echarts__;","// The module cache\nvar __webpack_module_cache__ = {};\n\n// The require function\nfunction __webpack_require__(moduleId) {\n\t// Check if module is in cache\n\tif(__webpack_module_cache__[moduleId]) {\n\t\treturn __webpack_module_cache__[moduleId].exports;\n\t}\n\t// Create a new module (and put it into the cache)\n\tvar module = __webpack_module_cache__[moduleId] = {\n\t\t// no module.id needed\n\t\t// no module.loaded needed\n\t\texports: {}\n\t};\n\n\t// Execute the module function\n\t__webpack_modules__[moduleId](module, module.exports, __webpack_require__);\n\n\t// Return the exports of the module\n\treturn module.exports;\n}\n\n","// define __esModule on exports\n__webpack_require__.r = (exports) => {\n\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n\t}\n\tObject.defineProperty(exports, '__esModule', { value: true });\n};","// module exports must be returned from runtime so entry inlining is disabled\n// startup\n// Load entry module and return exports\nreturn __webpack_require__(\"./index.js\");\n"],"sourceRoot":""} -------------------------------------------------------------------------------- /dist/echarts-wordcloud.min.js: -------------------------------------------------------------------------------- 1 | /*! For license information please see echarts-wordcloud.min.js.LICENSE.txt */ 2 | !function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e(require("echarts")):"function"==typeof define&&define.amd?define(["echarts"],e):"object"==typeof exports?exports["echarts-wordcloud"]=e(require("echarts")):t["echarts-wordcloud"]=e(t.echarts)}(self,(function(t){return(()=>{"use strict";var e={638:(t,e,a)=>{a.r(e);var r=a(83);r.extendSeriesModel({type:"series.wordCloud",visualStyleAccessPath:"textStyle",visualStyleMapper:function(t){return{fill:t.get("color")}},visualDrawType:"fill",optionUpdated:function(){var t=this.option;t.gridSize=Math.max(Math.floor(t.gridSize),4)},getInitialData:function(t,e){var a=r.helper.createDimensions(t.data,{coordDimensions:["value"]}),i=new r.List(a,this);return i.initData(t.data),i},defaultOption:{maskImage:null,shape:"circle",keepAspect:!1,left:"center",top:"center",width:"70%",height:"80%",sizeRange:[12,60],rotationRange:[-90,90],rotationStep:45,gridSize:8,drawOutOfBound:!1,shrinkToFit:!1,textStyle:{fontWeight:"normal"}}}),r.extendChartView({type:"wordCloud",render:function(t,e,a){var i=this.group;i.removeAll();var o=t.getData(),n=t.get("gridSize");t.layoutInstance.ondraw=function(e,a,s,l){var d=o.getItemModel(s),u=d.getModel("textStyle"),f=new r.graphic.Text({style:r.helper.createTextStyle(u),scaleX:1/l.info.mu,scaleY:1/l.info.mu,x:(l.gx+l.info.gw/2)*n,y:(l.gy+l.info.gh/2)*n,rotation:l.rot});f.setStyle({x:l.info.fillTextOffsetX,y:l.info.fillTextOffsetY+.5*a,text:e,verticalAlign:"middle",fill:o.getItemVisual(s,"style").fill,fontSize:a}),i.add(f),o.setItemGraphicEl(s,f),f.ensureState("emphasis").style=r.helper.createTextStyle(d.getModel(["emphasis","textStyle"]),{state:"emphasis"}),f.ensureState("blur").style=r.helper.createTextStyle(d.getModel(["blur","textStyle"]),{state:"blur"}),r.helper.enableHoverEmphasis(f,d.get(["emphasis","focus"]),d.get(["emphasis","blurScope"])),f.stateTransition={duration:t.get("animation")?t.get(["stateAnimation","duration"]):0,easing:t.get(["stateAnimation","easing"])},f.__highDownDispatcher=!0},this._model=t},remove:function(){this.group.removeAll(),this._model.layoutInstance.dispose()},dispose:function(){this._model.layoutInstance.dispose()}}),window.setImmediate||(window.setImmediate=window.msSetImmediate||window.webkitSetImmediate||window.mozSetImmediate||window.oSetImmediate||function(){if(!window.postMessage||!window.addEventListener)return null;var t=[void 0],e="zero-timeout-message";return window.addEventListener("message",(function(a){if("string"==typeof a.data&&a.data.substr(0,e.length)===e){a.stopImmediatePropagation();var r=parseInt(a.data.substr(e.length),36);t[r]&&(t[r](),t[r]=void 0)}}),!0),window.clearImmediate=function(e){t[e]&&(t[e]=void 0)},function(a){var r=t.length;return t.push(a),window.postMessage(e+r.toString(36),"*"),r}}()||function(t){window.setTimeout(t,0)}),window.clearImmediate||(window.clearImmediate=window.msClearImmediate||window.webkitClearImmediate||window.mozClearImmediate||window.oClearImmediate||function(t){window.clearTimeout(t)});var i=function(){var t=document.createElement("canvas");if(!t||!t.getContext)return!1;var e=t.getContext("2d");return!!(e&&e.getImageData&&e.fillText&&Array.prototype.some&&Array.prototype.push)}(),o=function(){if(i){for(var t,e,a=document.createElement("canvas").getContext("2d"),r=20;r;){if(a.font=r.toString(10)+"px sans-serif",a.measureText("W").width===t&&a.measureText("m").width===e)return r+1;t=a.measureText("W").width,e=a.measureText("m").width,r--}return 0}}(),n=function(t){for(var e,a,r=t.length;r;)e=Math.floor(Math.random()*r),a=t[--r],t[r]=t[e],t[e]=a;return t},s={},l=function(t,e){if(i){var a=Math.floor(Math.random()*Date.now());Array.isArray(t)||(t=[t]),t.forEach((function(e,a){if("string"==typeof e){if(t[a]=document.getElementById(e),!t[a])throw new Error("The element id specified is not found.")}else if(!e.tagName&&!e.appendChild)throw new Error("You must pass valid HTML elements, or ID of the element.")}));var r={list:[],fontFamily:'"Trebuchet MS", "Heiti TC", "微軟正黑體", "Arial Unicode MS", "Droid Fallback Sans", sans-serif',fontWeight:"normal",color:"random-dark",minSize:0,weightFactor:1,clearCanvas:!0,backgroundColor:"#fff",gridSize:8,drawOutOfBound:!1,shrinkToFit:!1,origin:null,drawMask:!1,maskColor:"rgba(255,0,0,0.3)",maskGapWidth:.3,layoutAnimation:!0,wait:0,abortThreshold:0,abort:function(){},minRotation:-Math.PI/2,maxRotation:Math.PI/2,rotationStep:.1,shuffle:!0,rotateRatio:.1,shape:"circle",ellipticity:.65,classes:null,hover:null,click:null};if(e)for(var l in e)l in r&&(r[l]=e[l]);if("function"!=typeof r.weightFactor){var d=r.weightFactor;r.weightFactor=function(t){return t*d}}if("function"!=typeof r.shape)switch(r.shape){case"circle":default:r.shape="circle";break;case"cardioid":r.shape=function(t){return 1-Math.sin(t)};break;case"diamond":r.shape=function(t){var e=t%(2*Math.PI/4);return 1/(Math.cos(e)+Math.sin(e))};break;case"square":r.shape=function(t){return Math.min(1/Math.abs(Math.cos(t)),1/Math.abs(Math.sin(t)))};break;case"triangle-forward":r.shape=function(t){var e=t%(2*Math.PI/3);return 1/(Math.cos(e)+Math.sqrt(3)*Math.sin(e))};break;case"triangle":case"triangle-upright":r.shape=function(t){var e=(t+3*Math.PI/2)%(2*Math.PI/3);return 1/(Math.cos(e)+Math.sqrt(3)*Math.sin(e))};break;case"pentagon":r.shape=function(t){var e=(t+.955)%(2*Math.PI/5);return 1/(Math.cos(e)+.726543*Math.sin(e))};break;case"star":r.shape=function(t){var e=(t+.955)%(2*Math.PI/10);return(t+.955)%(2*Math.PI/5)-2*Math.PI/10>=0?1/(Math.cos(2*Math.PI/10-e)+3.07768*Math.sin(2*Math.PI/10-e)):1/(Math.cos(e)+3.07768*Math.sin(e))}}r.gridSize=Math.max(Math.floor(r.gridSize),4);var u,f,c,h,m,g,w,v,p=r.gridSize,y=p-r.maskGapWidth,x=Math.abs(r.maxRotation-r.minRotation),M=Math.min(r.maxRotation,r.minRotation),S=r.rotationStep;switch(r.color){case"random-dark":w=function(){return L(10,50)};break;case"random-light":w=function(){return L(50,90)};break;default:"function"==typeof r.color&&(w=r.color)}"function"==typeof r.fontWeight&&(v=r.fontWeight);var b=null;"function"==typeof r.classes&&(b=r.classes);var I,T=!1,k=[],C=function(t){var e,a,r=t.currentTarget,i=r.getBoundingClientRect();t.touches?(e=t.touches[0].clientX,a=t.touches[0].clientY):(e=t.clientX,a=t.clientY);var o=e-i.left,n=a-i.top,s=Math.floor(o*(r.width/i.width||1)/p),l=Math.floor(n*(r.height/i.height||1)/p);return k[s]?k[s][l]:null},E=function(t){var e=C(t);I!==e&&(I=e,e?r.hover(e.item,e.dimension,t):r.hover(void 0,void 0,t))},A=function(t){var e=C(t);e&&(r.click(e.item,e.dimension,t),t.preventDefault())},O=[],F=function(t){if(O[t])return O[t];var e=8*t,a=e,i=[];for(0===t&&i.push([h[0],h[1],0]);a--;){var o=1;"circle"!==r.shape&&(o=r.shape(a/e*2*Math.PI)),i.push([h[0]+t*o*Math.cos(-a/e*2*Math.PI),h[1]+t*o*Math.sin(-a/e*2*Math.PI)*r.ellipticity,a/e*2*Math.PI])}return O[t]=i,i},D=function(){return r.abortThreshold>0&&(new Date).getTime()-g>r.abortThreshold},P=function(e,a,r,i,o){e>=f||a>=c||e<0||a<0||(u[e][a]=!1,r&&t[0].getContext("2d").fillRect(e*p,a*p,y,y),T&&(k[e][a]={item:o,dimension:i}))},R=function e(a,i){if(i>20)return null;var s,l,d;Array.isArray(a)?(s=a[0],l=a[1]):(s=a.word,l=a.weight,d=a.attributes);var h,g,y,I=0===r.rotateRatio||Math.random()>r.rotateRatio?0:0===x?M:M+Math.round(Math.random()*x/S)*S,k=function(t){if(Array.isArray(t)){var e=t.slice();return e.splice(0,2),e}return[]}(a),C=function(t,e,a,i){var n=r.weightFactor(e);if(n<=r.minSize)return!1;var s,l=1;nF[1]&&(F[1]=O),kF[2]&&(F[2]=k);break t}}return{mu:l,occupied:A,bounds:F,gw:S,gh:M,fillTextOffsetX:y,fillTextOffsetY:x,fillTextWidth:f,fillTextHeight:c,fontSize:n}}(s,l,I,k);if(!C)return!1;if(D())return!1;if(!r.drawOutOfBound&&!r.shrinkToFit){var E=C.bounds;if(E[1]-E[3]+1>f||E[2]-E[0]+1>c)return!1}for(var A=m+1;A--;){var O=F(m-A);r.shuffle&&(O=[].concat(O),n(O));for(var R=0;R=f||l>=c||s<0||l<0){if(!r.drawOutOfBound)return!1}else if(!u[s][l])return!1}return!0}(g,y,0,0,C.occupied)&&(function(e,a,i,o,n,s,l,d,u,f){var c,h,m,g=i.fontSize;c=w?w(o,n,g,s,l,f):r.color,h=v?v(o,n,g,f):r.fontWeight,m=b?b(o,n,g,f):r.classes,t.forEach((function(t){if(t.getContext){var n=t.getContext("2d"),s=i.mu;n.save(),n.scale(1/s,1/s),n.font=h+" "+(g*s).toString(10)+"px "+r.fontFamily,n.fillStyle=c,n.translate((e+i.gw/2)*p*s,(a+i.gh/2)*p*s),0!==d&&n.rotate(-d),n.textBaseline="middle",n.fillText(o,i.fillTextOffsetX*s,(i.fillTextOffsetY+.5*g)*s),n.restore()}else{var l=document.createElement("span"),f="";f="rotate("+-d/Math.PI*180+"deg) ",1!==i.mu&&(f+="translateX(-"+i.fillTextWidth/4+"px) scale("+1/i.mu+")");var w={position:"absolute",display:"block",font:h+" "+g*i.mu+"px "+r.fontFamily,left:(e+i.gw/2)*p+i.fillTextOffsetX+"px",top:(a+i.gh/2)*p+i.fillTextOffsetY+"px",width:i.fillTextWidth+"px",height:i.fillTextHeight+"px",lineHeight:g+"px",whiteSpace:"nowrap",transform:f,webkitTransform:f,msTransform:f,transformOrigin:"50% 40%",webkitTransformOrigin:"50% 40%",msTransformOrigin:"50% 40%"};for(var v in c&&(w.color=c),l.textContent=o,w)l.style[v]=w[v];if(u)for(var y in u)l.setAttribute(y,u[y]);m&&(l.className+=m),t.appendChild(l)}}))}(g,y,C,s,l,m-A,h[2],I,d,k),function(e,a,i,o,n,s){var l,d,u=n.occupied,h=r.drawMask;if(h&&((l=t[0].getContext("2d")).save(),l.fillStyle=r.maskColor),T){var m=n.bounds;d={x:(e+m[3])*p,y:(a+m[0])*p,w:(m[1]-m[3]+1)*p,h:(m[2]-m[0]+1)*p}}for(var g=u.length;g--;){var w=e+u[g][0],v=a+u[g][1];w>=f||v>=c||w<0||v<0||P(w,v,h,d,s)}h&&l.restore()}(g,y,0,0,C,a),{gx:g,gy:y,rot:I,info:C}));if(z)return z}}return r.shrinkToFit?(Array.isArray(a)?a[1]=3*a[1]/4:a.weight=3*a.weight/4,e(a,i+1)):null},z=function(e,a,r){if(a)return!t.some((function(t){var a=new CustomEvent(e,{detail:r||{}});return!t.dispatchEvent(a)}),this);t.forEach((function(t){var a=new CustomEvent(e,{detail:r||{}});t.dispatchEvent(a)}),this)};!function(){var e=t[0];if(e.getContext)f=Math.ceil(e.width/p),c=Math.ceil(e.height/p);else{var i=e.getBoundingClientRect();f=Math.ceil(i.width/p),c=Math.ceil(i.height/p)}if(z("wordcloudstart",!0)){var o,n,l,d,w;if(h=r.origin?[r.origin[0]/p,r.origin[1]/p]:[f/2,c/2],m=Math.floor(Math.sqrt(f*f+c*c)),u=[],!e.getContext||r.clearCanvas)for(t.forEach((function(t){if(t.getContext){var e=t.getContext("2d");e.fillStyle=r.backgroundColor,e.clearRect(0,0,f*(p+1),c*(p+1)),e.fillRect(0,0,f*(p+1),c*(p+1))}else t.textContent="",t.style.backgroundColor=r.backgroundColor,t.style.position="relative"})),o=f;o--;)for(u[o]=[],n=c;n--;)u[o][n]=!0;else{var v=document.createElement("canvas").getContext("2d");v.fillStyle=r.backgroundColor,v.fillRect(0,0,1,1);var y,x,M=v.getImageData(0,0,1,1).data,S=e.getContext("2d").getImageData(0,0,f*p,c*p).data;for(o=f;o--;)for(u[o]=[],n=c;n--;){x=p;t:for(;x--;)for(y=p;y--;)for(l=4;l--;)if(S[4*((n*p+x)*f*p+(o*p+y))+l]!==M[l]){u[o][n]=!1;break t}!1!==u[o][n]&&(u[o][n]=!0)}S=v=M=void 0}if(r.hover||r.click){for(T=!0,o=f+1;o--;)k[o]=[];r.hover&&e.addEventListener("mousemove",E),r.click&&(e.addEventListener("click",A),e.addEventListener("touchstart",A),e.addEventListener("touchend",(function(t){t.preventDefault()})),e.style.webkitTapHighlightColor="rgba(0, 0, 0, 0)"),e.addEventListener("wordcloudstart",(function t(){e.removeEventListener("wordcloudstart",t),e.removeEventListener("mousemove",E),e.removeEventListener("click",A),I=void 0}))}l=0;var b=!0;r.layoutAnimation?0!==r.wait?(d=window.setTimeout,w=window.clearTimeout):(d=window.setImmediate,w=window.clearImmediate):(d=function(t){t()},w=function(){b=!1});var C=function(e,a){t.forEach((function(t){t.removeEventListener(e,a)}),this)},O=function t(){C("wordcloudstart",t),w(s[a])};!function(e,a){t.forEach((function(t){t.addEventListener("wordcloudstart",a)}),this)}(0,O),s[a]=(r.layoutAnimation?d:setTimeout)((function t(){if(b){if(l>=r.list.length)return w(s[a]),z("wordcloudstop",!1),C("wordcloudstart",O),void delete s[a];g=(new Date).getTime();var e=R(r.list[l],0),i=!z("wordclouddrawn",!0,{item:r.list[l],drawn:e});if(D()||i)return w(s[a]),r.abort(),z("wordcloudabort",!1),z("wordcloudstop",!1),void C("wordcloudstart",O);l++,s[a]=d(t,r.wait)}}),r.wait)}}()}function L(t,e){return"hsl("+(360*Math.random()).toFixed()+","+(30*Math.random()+70).toFixed()+"%,"+(Math.random()*(e-t)+t).toFixed()+"%)"}};l.isSupported=i,l.minFontSize=o;const d=l;if(!d.isSupported)throw new Error("Sorry your browser not support wordCloud");r.registerLayout((function(t,e){t.eachSeriesByType("wordCloud",(function(a){var i=r.helper.getLayoutRect(a.getBoxLayoutParams(),{width:e.getWidth(),height:e.getHeight()}),o=a.get("keepAspect"),n=a.get("maskImage"),s=n?n.width/n.height:1;o&&function(t,e){var a=t.width,r=t.height;a>r*e?(t.x+=(a-r*e)/2,t.width=r*e):(t.y+=(r-a/e)/2,t.height=a/e)}(i,s);var l=a.getData(),u=document.createElement("canvas");u.width=i.width,u.height=i.height;var f=u.getContext("2d");if(n)try{f.drawImage(n,0,0,u.width,u.height),function(t){for(var e=t.getContext("2d"),a=e.getImageData(0,0,t.width,t.height),r=e.createImageData(a),i=0,o=0,n=0;n128&&(i+=l=a.data[n]+a.data[n+1]+a.data[n+2],++o);var s=i/o;for(n=0;ns?(r.data[n]=0,r.data[n+1]=0,r.data[n+2]=0,r.data[n+3]=0):(r.data[n]=255,r.data[n+1]=255,r.data[n+2]=255,r.data[n+3]=255)}e.putImageData(r,0,0)}(u)}catch(t){console.error("Invalid mask image"),console.error(t.toString())}var c=a.get("sizeRange"),h=a.get("rotationRange"),m=l.getDataExtent("value"),g=Math.PI/180,w=a.get("gridSize");function v(t){var e=t.detail.item;t.detail.drawn&&a.layoutInstance.ondraw&&(t.detail.drawn.gx+=i.x/w,t.detail.drawn.gy+=i.y/w,a.layoutInstance.ondraw(e[0],e[1],e[2],t.detail.drawn))}d(u,{list:l.mapArray("value",(function(t,e){var a=l.getItemModel(e);return[l.getName(e),a.get("textStyle.fontSize",!0)||r.number.linearMap(t,m,c),e]})).sort((function(t,e){return e[1]-t[1]})),fontFamily:a.get("textStyle.fontFamily")||a.get("emphasis.textStyle.fontFamily")||t.get("textStyle.fontFamily"),fontWeight:a.get("textStyle.fontWeight")||a.get("emphasis.textStyle.fontWeight")||t.get("textStyle.fontWeight"),gridSize:w,ellipticity:i.height/i.width,minRotation:h[0]*g,maxRotation:h[1]*g,clearCanvas:!n,rotateRatio:1,rotationStep:a.get("rotationStep")*g,drawOutOfBound:a.get("drawOutOfBound"),shrinkToFit:a.get("shrinkToFit"),layoutAnimation:a.get("layoutAnimation"),shuffle:!1,shape:a.get("shape")}),u.addEventListener("wordclouddrawn",v),a.layoutInstance&&a.layoutInstance.dispose(),a.layoutInstance={ondraw:null,dispose:function(){u.removeEventListener("wordclouddrawn",v),u.addEventListener("wordclouddrawn",(function(t){t.preventDefault()}))}}}))})),r.registerPreprocessor((function(t){var e=(t||{}).series;!r.util.isArray(e)&&(e=e?[e]:[]);var a=["shadowColor","shadowBlur","shadowOffsetX","shadowOffsetY"];function i(t){t&&r.util.each(a,(function(e){t.hasOwnProperty(e)&&(t["text"+r.format.capitalFirst(e)]=t[e])}))}r.util.each(e,(function(t){if(t&&"wordCloud"===t.type){var e=t.textStyle||{};i(e.normal),i(e.emphasis)}}))}))},83:e=>{e.exports=t}},a={};function r(t){if(a[t])return a[t].exports;var i=a[t]={exports:{}};return e[t](i,i.exports,r),i.exports}return r.r=t=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},r(638)})()})); 3 | //# sourceMappingURL=echarts-wordcloud.min.js.map -------------------------------------------------------------------------------- /dist/echarts-wordcloud.min.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /*! 2 | * wordcloud2.js 3 | * http://timdream.org/wordcloud2.js/ 4 | * 5 | * Copyright 2011 - 2019 Tim Guan-tin Chien and contributors. 6 | * Released under the MIT license 7 | */ 8 | -------------------------------------------------------------------------------- /example/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ecomfe/echarts-wordcloud/3aa04edd2b842977109efe5e9a20b3aab46caa18/example/logo.png -------------------------------------------------------------------------------- /example/optionKeywords.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 17 |
18 | 415 | 416 | -------------------------------------------------------------------------------- /example/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 29 |
30 |
31 |

Expect text with 0 to 9 to be displayed all

32 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /example/typeTest.ts: -------------------------------------------------------------------------------- 1 | import echarts from 'echarts'; 2 | import '../'; 3 | 4 | 5 | const option: echarts.EChartsOption = { 6 | series: { 7 | type: 'wordCloud', 8 | shape: 'circle', 9 | 10 | // Folllowing left/top/width/height/right/bottom are used for positioning the word cloud 11 | // Default to be put in the center and has 75% x 80% size. 12 | 13 | left: 'center', 14 | top: 'center', 15 | width: '70%', 16 | height: '80%', 17 | right: null, 18 | bottom: null, 19 | 20 | // Text size range which the value in data will be mapped to. 21 | // Default to have minimum 12px and maximum 60px size. 22 | 23 | sizeRange: [12, 60], 24 | 25 | // Text rotation range and step in degree. Text will be rotated randomly in range [-90, 90] by rotationStep 45 26 | 27 | rotationRange: [-90, 90], 28 | rotationStep: 45, 29 | 30 | // size of the grid in pixels for marking the availability of the canvas 31 | // the larger the grid size, the bigger the gap between words. 32 | 33 | gridSize: 8, 34 | 35 | // set to true to allow word being draw partly outside of the canvas. 36 | // Allow word bigger than the size of the canvas to be drawn 37 | drawOutOfBound: false, 38 | 39 | // If perform layout animation. 40 | // NOTE disable it will lead to UI blocking when there is lots of words. 41 | layoutAnimation: true, 42 | 43 | // Global text style 44 | textStyle: { 45 | fontFamily: 'sans-serif', 46 | fontWeight: 'bold', 47 | // Color can be a callback function or a color string 48 | color: function () { 49 | // Random color 50 | return 'rgb(' + [ 51 | Math.round(Math.random() * 160), 52 | Math.round(Math.random() * 160), 53 | Math.round(Math.random() * 160) 54 | ].join(',') + ')'; 55 | } 56 | }, 57 | emphasis: { 58 | focus: 'self', 59 | 60 | textStyle: { 61 | textShadowBlur: 10, 62 | textShadowColor: '#333' 63 | } 64 | }, 65 | 66 | // Data is an array. Each array item must have name and value property. 67 | data: [{ 68 | name: 'Farrah Abraham', 69 | value: 366, 70 | // Style of single text 71 | textStyle: { 72 | } 73 | }] 74 | } 75 | } -------------------------------------------------------------------------------- /example/word-cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ecomfe/echarts-wordcloud/3aa04edd2b842977109efe5e9a20b3aab46caa18/example/word-cloud.png -------------------------------------------------------------------------------- /example/wordCloud.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 16 |
17 | 143 | 144 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import echarts from 'echarts'; 2 | 3 | interface WordCloudTextStyle { 4 | color?: string; 5 | fontStyle?: string; 6 | fontWeight?: string | number; 7 | fontFamily?: string; 8 | fontSize?: number | string; 9 | align?: string; 10 | verticalAlign?: string; 11 | // @deprecated 12 | baseline?: string; 13 | 14 | opacity?: number; 15 | 16 | lineHeight?: number; 17 | backgroundColor?: 18 | | string 19 | | { 20 | image: HTMLImageElement | HTMLCanvasElement | string; 21 | }; 22 | borderColor?: string; 23 | borderWidth?: number; 24 | borderType?: string; 25 | borderDashOffset?: number; 26 | borderRadius?: number | number[]; 27 | padding?: number | number[]; 28 | 29 | width?: number | string; // Percent 30 | height?: number; 31 | textBorderColor?: string; 32 | textBorderWidth?: number; 33 | textBorderType?: string; 34 | textBorderDashOffset?: number; 35 | 36 | textShadowBlur?: number; 37 | textShadowColor?: string; 38 | textShadowOffsetX?: number; 39 | textShadowOffsetY?: number; 40 | } 41 | 42 | interface WorldCloudDataItem { 43 | name?: string; 44 | value?: number | number[]; 45 | textStyle?: WordCloudTextStyle; 46 | emphasis?: { 47 | textStyle?: WordCloudTextStyle; 48 | }; 49 | } 50 | 51 | declare module 'echarts/types/dist/echarts' { 52 | export interface WordCloudSeriesOption { 53 | mainType?: 'series'; 54 | type?: 'wordCloud'; 55 | silent?: boolean; 56 | blendMode?: string; 57 | /** 58 | * Cursor when mouse on the elements 59 | */ 60 | cursor?: string; 61 | 62 | width?: number | string; 63 | height?: number | string; 64 | top?: number | string; 65 | right?: number | string; 66 | bottom?: number | string; 67 | left?: number | string; 68 | 69 | textStyle?: 70 | | WordCloudTextStyle 71 | | { 72 | color?: (params?: any) => string; 73 | }; 74 | emphasis?: { 75 | focus?: 'self' | 'series' | 'none'; 76 | blurScope?: 'coordinateSystem' | 'global' | 'series'; 77 | textStyle?: WordCloudTextStyle; 78 | }; 79 | 80 | shape?: string; 81 | maskImage?: HTMLImageElement | HTMLCanvasElement; 82 | 83 | sizeRange?: number[]; 84 | rotationRange?: number[]; 85 | rotationStep?: number; 86 | 87 | gridSize?: number; 88 | 89 | drawOutOfBound?: boolean; 90 | layoutAnimation?: boolean; 91 | 92 | data?: WorldCloudDataItem[]; 93 | } 94 | 95 | interface RegisteredSeriesOption { 96 | wordCloud: WordCloudSeriesOption; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import './src/wordCloud'; 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "echarts-wordcloud", 3 | "version": "2.1.0", 4 | "description": "ECharts wordcloud extension based on wordcloud2.js", 5 | "main": "index.js", 6 | "author": "", 7 | "license": "ISC", 8 | "scripts": { 9 | "dev": "npx webpack --mode development --watch", 10 | "build": "npx webpack --mode development", 11 | "release": "npx webpack --mode production && npx webpack --mode development" 12 | }, 13 | "peerDependencies": { 14 | "echarts": "^5.0.1" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/ecomfe/echarts-wordcloud.git" 19 | }, 20 | "devDependencies": { 21 | "echarts": "^5.4.0", 22 | "prettier": "^2.5.1", 23 | "webpack": "^5.11.1", 24 | "webpack-cli": "^4.3.1" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/WordCloudSeries.js: -------------------------------------------------------------------------------- 1 | import * as echarts from 'echarts/lib/echarts'; 2 | 3 | echarts.extendSeriesModel({ 4 | type: 'series.wordCloud', 5 | 6 | visualStyleAccessPath: 'textStyle', 7 | visualStyleMapper: function (model) { 8 | return { 9 | fill: model.get('color') 10 | }; 11 | }, 12 | visualDrawType: 'fill', 13 | 14 | optionUpdated: function () { 15 | var option = this.option; 16 | option.gridSize = Math.max(Math.floor(option.gridSize), 4); 17 | }, 18 | 19 | getInitialData: function (option, ecModel) { 20 | var dimensions = echarts.helper.createDimensions(option.data, { 21 | coordDimensions: ['value'] 22 | }); 23 | var list = new echarts.List(dimensions, this); 24 | list.initData(option.data); 25 | return list; 26 | }, 27 | 28 | // Most of options are from https://github.com/timdream/wordcloud2.js/blob/gh-pages/API.md 29 | defaultOption: { 30 | maskImage: null, 31 | 32 | // Shape can be 'circle', 'cardioid', 'diamond', 'triangle-forward', 'triangle', 'pentagon', 'star' 33 | shape: 'circle', 34 | keepAspect: false, 35 | 36 | left: 'center', 37 | 38 | top: 'center', 39 | 40 | width: '70%', 41 | 42 | height: '80%', 43 | 44 | sizeRange: [12, 60], 45 | 46 | rotationRange: [-90, 90], 47 | 48 | rotationStep: 45, 49 | 50 | gridSize: 8, 51 | 52 | drawOutOfBound: false, 53 | shrinkToFit: false, 54 | 55 | textStyle: { 56 | fontWeight: 'normal' 57 | } 58 | } 59 | }); 60 | -------------------------------------------------------------------------------- /src/WordCloudView.js: -------------------------------------------------------------------------------- 1 | import * as echarts from 'echarts/lib/echarts'; 2 | 3 | echarts.extendChartView({ 4 | type: 'wordCloud', 5 | 6 | render: function (seriesModel, ecModel, api) { 7 | var group = this.group; 8 | group.removeAll(); 9 | 10 | var data = seriesModel.getData(); 11 | 12 | var gridSize = seriesModel.get('gridSize'); 13 | 14 | seriesModel.layoutInstance.ondraw = function (text, size, dataIdx, drawn) { 15 | var itemModel = data.getItemModel(dataIdx); 16 | var textStyleModel = itemModel.getModel('textStyle'); 17 | 18 | var textEl = new echarts.graphic.Text({ 19 | style: echarts.helper.createTextStyle(textStyleModel), 20 | scaleX: 1 / drawn.info.mu, 21 | scaleY: 1 / drawn.info.mu, 22 | x: (drawn.gx + drawn.info.gw / 2) * gridSize, 23 | y: (drawn.gy + drawn.info.gh / 2) * gridSize, 24 | rotation: drawn.rot 25 | }); 26 | textEl.setStyle({ 27 | x: drawn.info.fillTextOffsetX, 28 | y: drawn.info.fillTextOffsetY + size * 0.5, 29 | text: text, 30 | verticalAlign: 'middle', 31 | fill: data.getItemVisual(dataIdx, 'style').fill, 32 | fontSize: size 33 | }); 34 | 35 | group.add(textEl); 36 | 37 | data.setItemGraphicEl(dataIdx, textEl); 38 | 39 | textEl.ensureState('emphasis').style = echarts.helper.createTextStyle( 40 | itemModel.getModel(['emphasis', 'textStyle']), 41 | { 42 | state: 'emphasis' 43 | } 44 | ); 45 | textEl.ensureState('blur').style = echarts.helper.createTextStyle( 46 | itemModel.getModel(['blur', 'textStyle']), 47 | { 48 | state: 'blur' 49 | } 50 | ); 51 | 52 | echarts.helper.enableHoverEmphasis( 53 | textEl, 54 | itemModel.get(['emphasis', 'focus']), 55 | itemModel.get(['emphasis', 'blurScope']) 56 | ); 57 | 58 | textEl.stateTransition = { 59 | duration: seriesModel.get('animation') 60 | ? seriesModel.get(['stateAnimation', 'duration']) 61 | : 0, 62 | easing: seriesModel.get(['stateAnimation', 'easing']) 63 | }; 64 | // TODO 65 | textEl.__highDownDispatcher = true; 66 | }; 67 | 68 | this._model = seriesModel; 69 | }, 70 | 71 | remove: function () { 72 | this.group.removeAll(); 73 | 74 | this._model.layoutInstance.dispose(); 75 | }, 76 | 77 | dispose: function () { 78 | this._model.layoutInstance.dispose(); 79 | } 80 | }); 81 | -------------------------------------------------------------------------------- /src/layout.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * wordcloud2.js 3 | * http://timdream.org/wordcloud2.js/ 4 | * 5 | * Copyright 2011 - 2019 Tim Guan-tin Chien and contributors. 6 | * Released under the MIT license 7 | */ 8 | 9 | 'use strict'; 10 | 11 | // setImmediate 12 | if (!window.setImmediate) { 13 | window.setImmediate = (function setupSetImmediate() { 14 | return ( 15 | window.msSetImmediate || 16 | window.webkitSetImmediate || 17 | window.mozSetImmediate || 18 | window.oSetImmediate || 19 | (function setupSetZeroTimeout() { 20 | if (!window.postMessage || !window.addEventListener) { 21 | return null; 22 | } 23 | 24 | var callbacks = [undefined]; 25 | var message = 'zero-timeout-message'; 26 | 27 | // Like setTimeout, but only takes a function argument. There's 28 | // no time argument (always zero) and no arguments (you have to 29 | // use a closure). 30 | var setZeroTimeout = function setZeroTimeout(callback) { 31 | var id = callbacks.length; 32 | callbacks.push(callback); 33 | window.postMessage(message + id.toString(36), '*'); 34 | 35 | return id; 36 | }; 37 | 38 | window.addEventListener( 39 | 'message', 40 | function setZeroTimeoutMessage(evt) { 41 | // Skipping checking event source, retarded IE confused this window 42 | // object with another in the presence of iframe 43 | if ( 44 | typeof evt.data !== 'string' || 45 | evt.data.substr(0, message.length) !== message /* || 46 | evt.source !== window */ 47 | ) { 48 | return; 49 | } 50 | 51 | evt.stopImmediatePropagation(); 52 | 53 | var id = parseInt(evt.data.substr(message.length), 36); 54 | if (!callbacks[id]) { 55 | return; 56 | } 57 | 58 | callbacks[id](); 59 | callbacks[id] = undefined; 60 | }, 61 | true 62 | ); 63 | 64 | /* specify clearImmediate() here since we need the scope */ 65 | window.clearImmediate = function clearZeroTimeout(id) { 66 | if (!callbacks[id]) { 67 | return; 68 | } 69 | 70 | callbacks[id] = undefined; 71 | }; 72 | 73 | return setZeroTimeout; 74 | })() || 75 | // fallback 76 | function setImmediateFallback(fn) { 77 | window.setTimeout(fn, 0); 78 | } 79 | ); 80 | })(); 81 | } 82 | 83 | if (!window.clearImmediate) { 84 | window.clearImmediate = (function setupClearImmediate() { 85 | return ( 86 | window.msClearImmediate || 87 | window.webkitClearImmediate || 88 | window.mozClearImmediate || 89 | window.oClearImmediate || 90 | // "clearZeroTimeout" is implement on the previous block || 91 | // fallback 92 | function clearImmediateFallback(timer) { 93 | window.clearTimeout(timer); 94 | } 95 | ); 96 | })(); 97 | } 98 | 99 | // Check if WordCloud can run on this browser 100 | var isSupported = (function isSupported() { 101 | var canvas = document.createElement('canvas'); 102 | if (!canvas || !canvas.getContext) { 103 | return false; 104 | } 105 | 106 | var ctx = canvas.getContext('2d'); 107 | if (!ctx) { 108 | return false; 109 | } 110 | if (!ctx.getImageData) { 111 | return false; 112 | } 113 | if (!ctx.fillText) { 114 | return false; 115 | } 116 | 117 | if (!Array.prototype.some) { 118 | return false; 119 | } 120 | if (!Array.prototype.push) { 121 | return false; 122 | } 123 | 124 | return true; 125 | })(); 126 | 127 | // Find out if the browser impose minium font size by 128 | // drawing small texts on a canvas and measure it's width. 129 | var minFontSize = (function getMinFontSize() { 130 | if (!isSupported) { 131 | return; 132 | } 133 | 134 | var ctx = document.createElement('canvas').getContext('2d'); 135 | 136 | // start from 20 137 | var size = 20; 138 | 139 | // two sizes to measure 140 | var hanWidth, mWidth; 141 | 142 | while (size) { 143 | ctx.font = size.toString(10) + 'px sans-serif'; 144 | if ( 145 | ctx.measureText('\uFF37').width === hanWidth && 146 | ctx.measureText('m').width === mWidth 147 | ) { 148 | return size + 1; 149 | } 150 | 151 | hanWidth = ctx.measureText('\uFF37').width; 152 | mWidth = ctx.measureText('m').width; 153 | 154 | size--; 155 | } 156 | 157 | return 0; 158 | })(); 159 | 160 | var getItemExtraData = function (item) { 161 | if (Array.isArray(item)) { 162 | var itemCopy = item.slice(); 163 | // remove data we already have (word and weight) 164 | itemCopy.splice(0, 2); 165 | return itemCopy; 166 | } else { 167 | return []; 168 | } 169 | }; 170 | 171 | // Based on http://jsfromhell.com/array/shuffle 172 | var shuffleArray = function shuffleArray(arr) { 173 | for (var j, x, i = arr.length; i; ) { 174 | j = Math.floor(Math.random() * i); 175 | x = arr[--i]; 176 | arr[i] = arr[j]; 177 | arr[j] = x; 178 | } 179 | return arr; 180 | }; 181 | 182 | var timer = {}; 183 | var WordCloud = function WordCloud(elements, options) { 184 | if (!isSupported) { 185 | return; 186 | } 187 | 188 | var timerId = Math.floor(Math.random() * Date.now()); 189 | 190 | if (!Array.isArray(elements)) { 191 | elements = [elements]; 192 | } 193 | 194 | elements.forEach(function (el, i) { 195 | if (typeof el === 'string') { 196 | elements[i] = document.getElementById(el); 197 | if (!elements[i]) { 198 | throw new Error('The element id specified is not found.'); 199 | } 200 | } else if (!el.tagName && !el.appendChild) { 201 | throw new Error( 202 | 'You must pass valid HTML elements, or ID of the element.' 203 | ); 204 | } 205 | }); 206 | 207 | /* Default values to be overwritten by options object */ 208 | var settings = { 209 | list: [], 210 | fontFamily: 211 | '"Trebuchet MS", "Heiti TC", "微軟正黑體", ' + 212 | '"Arial Unicode MS", "Droid Fallback Sans", sans-serif', 213 | fontWeight: 'normal', 214 | color: 'random-dark', 215 | minSize: 0, // 0 to disable 216 | weightFactor: 1, 217 | clearCanvas: true, 218 | backgroundColor: '#fff', // opaque white = rgba(255, 255, 255, 1) 219 | 220 | gridSize: 8, 221 | drawOutOfBound: false, 222 | shrinkToFit: false, 223 | origin: null, 224 | 225 | drawMask: false, 226 | maskColor: 'rgba(255,0,0,0.3)', 227 | maskGapWidth: 0.3, 228 | 229 | layoutAnimation: true, 230 | 231 | wait: 0, 232 | abortThreshold: 0, // disabled 233 | abort: function noop() {}, 234 | 235 | minRotation: -Math.PI / 2, 236 | maxRotation: Math.PI / 2, 237 | rotationStep: 0.1, 238 | 239 | shuffle: true, 240 | rotateRatio: 0.1, 241 | 242 | shape: 'circle', 243 | ellipticity: 0.65, 244 | 245 | classes: null, 246 | 247 | hover: null, 248 | click: null 249 | }; 250 | 251 | if (options) { 252 | for (var key in options) { 253 | if (key in settings) { 254 | settings[key] = options[key]; 255 | } 256 | } 257 | } 258 | 259 | /* Convert weightFactor into a function */ 260 | if (typeof settings.weightFactor !== 'function') { 261 | var factor = settings.weightFactor; 262 | settings.weightFactor = function weightFactor(pt) { 263 | return pt * factor; // in px 264 | }; 265 | } 266 | 267 | /* Convert shape into a function */ 268 | if (typeof settings.shape !== 'function') { 269 | switch (settings.shape) { 270 | case 'circle': 271 | /* falls through */ 272 | default: 273 | // 'circle' is the default and a shortcut in the code loop. 274 | settings.shape = 'circle'; 275 | break; 276 | 277 | case 'cardioid': 278 | settings.shape = function shapeCardioid(theta) { 279 | return 1 - Math.sin(theta); 280 | }; 281 | break; 282 | 283 | /* 284 | To work out an X-gon, one has to calculate "m", 285 | where 1/(cos(2*PI/X)+m*sin(2*PI/X)) = 1/(cos(0)+m*sin(0)) 286 | http://www.wolframalpha.com/input/?i=1%2F%28cos%282*PI%2FX%29%2Bm*sin%28 287 | 2*PI%2FX%29%29+%3D+1%2F%28cos%280%29%2Bm*sin%280%29%29 288 | Copy the solution into polar equation r = 1/(cos(t') + m*sin(t')) 289 | where t' equals to mod(t, 2PI/X); 290 | */ 291 | 292 | case 'diamond': 293 | // http://www.wolframalpha.com/input/?i=plot+r+%3D+1%2F%28cos%28mod+ 294 | // %28t%2C+PI%2F2%29%29%2Bsin%28mod+%28t%2C+PI%2F2%29%29%29%2C+t+%3D 295 | // +0+..+2*PI 296 | settings.shape = function shapeSquare(theta) { 297 | var thetaPrime = theta % ((2 * Math.PI) / 4); 298 | return 1 / (Math.cos(thetaPrime) + Math.sin(thetaPrime)); 299 | }; 300 | break; 301 | 302 | case 'square': 303 | // http://www.wolframalpha.com/input/?i=plot+r+%3D+min(1%2Fabs(cos(t 304 | // )),1%2Fabs(sin(t)))),+t+%3D+0+..+2*PI 305 | settings.shape = function shapeSquare(theta) { 306 | return Math.min( 307 | 1 / Math.abs(Math.cos(theta)), 308 | 1 / Math.abs(Math.sin(theta)) 309 | ); 310 | }; 311 | break; 312 | 313 | case 'triangle-forward': 314 | // http://www.wolframalpha.com/input/?i=plot+r+%3D+1%2F%28cos%28mod+ 315 | // %28t%2C+2*PI%2F3%29%29%2Bsqrt%283%29sin%28mod+%28t%2C+2*PI%2F3%29 316 | // %29%29%2C+t+%3D+0+..+2*PI 317 | settings.shape = function shapeTriangle(theta) { 318 | var thetaPrime = theta % ((2 * Math.PI) / 3); 319 | return ( 320 | 1 / (Math.cos(thetaPrime) + Math.sqrt(3) * Math.sin(thetaPrime)) 321 | ); 322 | }; 323 | break; 324 | 325 | case 'triangle': 326 | case 'triangle-upright': 327 | settings.shape = function shapeTriangle(theta) { 328 | var thetaPrime = (theta + (Math.PI * 3) / 2) % ((2 * Math.PI) / 3); 329 | return ( 330 | 1 / (Math.cos(thetaPrime) + Math.sqrt(3) * Math.sin(thetaPrime)) 331 | ); 332 | }; 333 | break; 334 | 335 | case 'pentagon': 336 | settings.shape = function shapePentagon(theta) { 337 | var thetaPrime = (theta + 0.955) % ((2 * Math.PI) / 5); 338 | return 1 / (Math.cos(thetaPrime) + 0.726543 * Math.sin(thetaPrime)); 339 | }; 340 | break; 341 | 342 | case 'star': 343 | settings.shape = function shapeStar(theta) { 344 | var thetaPrime = (theta + 0.955) % ((2 * Math.PI) / 10); 345 | if ( 346 | ((theta + 0.955) % ((2 * Math.PI) / 5)) - (2 * Math.PI) / 10 >= 347 | 0 348 | ) { 349 | return ( 350 | 1 / 351 | (Math.cos((2 * Math.PI) / 10 - thetaPrime) + 352 | 3.07768 * Math.sin((2 * Math.PI) / 10 - thetaPrime)) 353 | ); 354 | } else { 355 | return 1 / (Math.cos(thetaPrime) + 3.07768 * Math.sin(thetaPrime)); 356 | } 357 | }; 358 | break; 359 | } 360 | } 361 | 362 | /* Make sure gridSize is a whole number and is not smaller than 4px */ 363 | settings.gridSize = Math.max(Math.floor(settings.gridSize), 4); 364 | 365 | /* shorthand */ 366 | var g = settings.gridSize; 367 | var maskRectWidth = g - settings.maskGapWidth; 368 | 369 | /* normalize rotation settings */ 370 | var rotationRange = Math.abs(settings.maxRotation - settings.minRotation); 371 | var minRotation = Math.min(settings.maxRotation, settings.minRotation); 372 | var rotationStep = settings.rotationStep; 373 | 374 | /* information/object available to all functions, set when start() */ 375 | var grid, // 2d array containing filling information 376 | ngx, 377 | ngy, // width and height of the grid 378 | center, // position of the center of the cloud 379 | maxRadius; 380 | 381 | /* timestamp for measuring each putWord() action */ 382 | var escapeTime; 383 | 384 | /* function for getting the color of the text */ 385 | var getTextColor; 386 | function randomHslColor(min, max) { 387 | return ( 388 | 'hsl(' + 389 | (Math.random() * 360).toFixed() + 390 | ',' + 391 | (Math.random() * 30 + 70).toFixed() + 392 | '%,' + 393 | (Math.random() * (max - min) + min).toFixed() + 394 | '%)' 395 | ); 396 | } 397 | switch (settings.color) { 398 | case 'random-dark': 399 | getTextColor = function getRandomDarkColor() { 400 | return randomHslColor(10, 50); 401 | }; 402 | break; 403 | 404 | case 'random-light': 405 | getTextColor = function getRandomLightColor() { 406 | return randomHslColor(50, 90); 407 | }; 408 | break; 409 | 410 | default: 411 | if (typeof settings.color === 'function') { 412 | getTextColor = settings.color; 413 | } 414 | break; 415 | } 416 | 417 | /* function for getting the font-weight of the text */ 418 | var getTextFontWeight; 419 | if (typeof settings.fontWeight === 'function') { 420 | getTextFontWeight = settings.fontWeight; 421 | } 422 | 423 | /* function for getting the classes of the text */ 424 | var getTextClasses = null; 425 | if (typeof settings.classes === 'function') { 426 | getTextClasses = settings.classes; 427 | } 428 | 429 | /* Interactive */ 430 | var interactive = false; 431 | var infoGrid = []; 432 | var hovered; 433 | 434 | var getInfoGridFromMouseTouchEvent = function getInfoGridFromMouseTouchEvent( 435 | evt 436 | ) { 437 | var canvas = evt.currentTarget; 438 | var rect = canvas.getBoundingClientRect(); 439 | var clientX; 440 | var clientY; 441 | /** Detect if touches are available */ 442 | if (evt.touches) { 443 | clientX = evt.touches[0].clientX; 444 | clientY = evt.touches[0].clientY; 445 | } else { 446 | clientX = evt.clientX; 447 | clientY = evt.clientY; 448 | } 449 | var eventX = clientX - rect.left; 450 | var eventY = clientY - rect.top; 451 | 452 | var x = Math.floor((eventX * (canvas.width / rect.width || 1)) / g); 453 | var y = Math.floor((eventY * (canvas.height / rect.height || 1)) / g); 454 | 455 | if (!infoGrid[x]) { 456 | return null 457 | } 458 | 459 | return infoGrid[x][y]; 460 | }; 461 | 462 | var wordcloudhover = function wordcloudhover(evt) { 463 | var info = getInfoGridFromMouseTouchEvent(evt); 464 | 465 | if (hovered === info) { 466 | return; 467 | } 468 | 469 | hovered = info; 470 | if (!info) { 471 | settings.hover(undefined, undefined, evt); 472 | 473 | return; 474 | } 475 | 476 | settings.hover(info.item, info.dimension, evt); 477 | }; 478 | 479 | var wordcloudclick = function wordcloudclick(evt) { 480 | var info = getInfoGridFromMouseTouchEvent(evt); 481 | if (!info) { 482 | return; 483 | } 484 | 485 | settings.click(info.item, info.dimension, evt); 486 | evt.preventDefault(); 487 | }; 488 | 489 | /* Get points on the grid for a given radius away from the center */ 490 | var pointsAtRadius = []; 491 | var getPointsAtRadius = function getPointsAtRadius(radius) { 492 | if (pointsAtRadius[radius]) { 493 | return pointsAtRadius[radius]; 494 | } 495 | 496 | // Look for these number of points on each radius 497 | var T = radius * 8; 498 | 499 | // Getting all the points at this radius 500 | var t = T; 501 | var points = []; 502 | 503 | if (radius === 0) { 504 | points.push([center[0], center[1], 0]); 505 | } 506 | 507 | while (t--) { 508 | // distort the radius to put the cloud in shape 509 | var rx = 1; 510 | if (settings.shape !== 'circle') { 511 | rx = settings.shape((t / T) * 2 * Math.PI); // 0 to 1 512 | } 513 | 514 | // Push [x, y, t]; t is used solely for getTextColor() 515 | points.push([ 516 | center[0] + radius * rx * Math.cos((-t / T) * 2 * Math.PI), 517 | center[1] + 518 | radius * rx * Math.sin((-t / T) * 2 * Math.PI) * settings.ellipticity, 519 | (t / T) * 2 * Math.PI 520 | ]); 521 | } 522 | 523 | pointsAtRadius[radius] = points; 524 | return points; 525 | }; 526 | 527 | /* Return true if we had spent too much time */ 528 | var exceedTime = function exceedTime() { 529 | return ( 530 | settings.abortThreshold > 0 && 531 | new Date().getTime() - escapeTime > settings.abortThreshold 532 | ); 533 | }; 534 | 535 | /* Get the deg of rotation according to settings, and luck. */ 536 | var getRotateDeg = function getRotateDeg() { 537 | if (settings.rotateRatio === 0) { 538 | return 0; 539 | } 540 | 541 | if (Math.random() > settings.rotateRatio) { 542 | return 0; 543 | } 544 | 545 | if (rotationRange === 0) { 546 | return minRotation; 547 | } 548 | 549 | return minRotation + Math.round(Math.random() * rotationRange / rotationStep) * rotationStep; 550 | }; 551 | 552 | var getTextInfo = function getTextInfo( 553 | word, 554 | weight, 555 | rotateDeg, 556 | extraDataArray 557 | ) { 558 | // calculate the acutal font size 559 | // fontSize === 0 means weightFactor function wants the text skipped, 560 | // and size < minSize means we cannot draw the text. 561 | var debug = false; 562 | var fontSize = settings.weightFactor(weight); 563 | if (fontSize <= settings.minSize) { 564 | return false; 565 | } 566 | 567 | // Scale factor here is to make sure fillText is not limited by 568 | // the minium font size set by browser. 569 | // It will always be 1 or 2n. 570 | var mu = 1; 571 | if (fontSize < minFontSize) { 572 | mu = (function calculateScaleFactor() { 573 | var mu = 2; 574 | while (mu * fontSize < minFontSize) { 575 | mu += 2; 576 | } 577 | return mu; 578 | })(); 579 | } 580 | 581 | // Get fontWeight that will be used to set fctx.font 582 | var fontWeight; 583 | if (getTextFontWeight) { 584 | fontWeight = getTextFontWeight(word, weight, fontSize, extraDataArray); 585 | } else { 586 | fontWeight = settings.fontWeight; 587 | } 588 | 589 | var fcanvas = document.createElement('canvas'); 590 | var fctx = fcanvas.getContext('2d', { willReadFrequently: true }); 591 | 592 | fctx.font = 593 | fontWeight + 594 | ' ' + 595 | (fontSize * mu).toString(10) + 596 | 'px ' + 597 | settings.fontFamily; 598 | 599 | // Estimate the dimension of the text with measureText(). 600 | var fw = fctx.measureText(word).width / mu; 601 | var fh = 602 | Math.max( 603 | fontSize * mu, 604 | fctx.measureText('m').width, 605 | fctx.measureText('\uFF37').width 606 | ) / mu; 607 | 608 | // Create a boundary box that is larger than our estimates, 609 | // so text don't get cut of (it sill might) 610 | var boxWidth = fw + fh * 2; 611 | var boxHeight = fh * 3; 612 | var fgw = Math.ceil(boxWidth / g); 613 | var fgh = Math.ceil(boxHeight / g); 614 | boxWidth = fgw * g; 615 | boxHeight = fgh * g; 616 | 617 | // Calculate the proper offsets to make the text centered at 618 | // the preferred position. 619 | 620 | // This is simply half of the width. 621 | var fillTextOffsetX = -fw / 2; 622 | // Instead of moving the box to the exact middle of the preferred 623 | // position, for Y-offset we move 0.4 instead, so Latin alphabets look 624 | // vertical centered. 625 | var fillTextOffsetY = -fh * 0.4; 626 | 627 | // Calculate the actual dimension of the canvas, considering the rotation. 628 | var cgh = Math.ceil( 629 | (boxWidth * Math.abs(Math.sin(rotateDeg)) + 630 | boxHeight * Math.abs(Math.cos(rotateDeg))) / 631 | g 632 | ); 633 | var cgw = Math.ceil( 634 | (boxWidth * Math.abs(Math.cos(rotateDeg)) + 635 | boxHeight * Math.abs(Math.sin(rotateDeg))) / 636 | g 637 | ); 638 | var width = cgw * g; 639 | var height = cgh * g; 640 | 641 | fcanvas.setAttribute('width', width); 642 | fcanvas.setAttribute('height', height); 643 | 644 | if (debug) { 645 | // Attach fcanvas to the DOM 646 | document.body.appendChild(fcanvas); 647 | // Save it's state so that we could restore and draw the grid correctly. 648 | fctx.save(); 649 | } 650 | 651 | // Scale the canvas with |mu|. 652 | fctx.scale(1 / mu, 1 / mu); 653 | fctx.translate((width * mu) / 2, (height * mu) / 2); 654 | fctx.rotate(-rotateDeg); 655 | 656 | // Once the width/height is set, ctx info will be reset. 657 | // Set it again here. 658 | fctx.font = 659 | fontWeight + 660 | ' ' + 661 | (fontSize * mu).toString(10) + 662 | 'px ' + 663 | settings.fontFamily; 664 | 665 | // Fill the text into the fcanvas. 666 | // XXX: We cannot because textBaseline = 'top' here because 667 | // Firefox and Chrome uses different default line-height for canvas. 668 | // Please read https://bugzil.la/737852#c6. 669 | // Here, we use textBaseline = 'middle' and draw the text at exactly 670 | // 0.5 * fontSize lower. 671 | fctx.fillStyle = '#000'; 672 | fctx.textBaseline = 'middle'; 673 | fctx.fillText( 674 | word, 675 | fillTextOffsetX * mu, 676 | (fillTextOffsetY + fontSize * 0.5) * mu 677 | ); 678 | 679 | // Get the pixels of the text 680 | var imageData = fctx.getImageData(0, 0, width, height).data; 681 | 682 | if (exceedTime()) { 683 | return false; 684 | } 685 | 686 | if (debug) { 687 | // Draw the box of the original estimation 688 | fctx.strokeRect(fillTextOffsetX * mu, fillTextOffsetY, fw * mu, fh * mu); 689 | fctx.restore(); 690 | } 691 | 692 | // Read the pixels and save the information to the occupied array 693 | var occupied = []; 694 | var gx = cgw; 695 | var gy, x, y; 696 | var bounds = [cgh / 2, cgw / 2, cgh / 2, cgw / 2]; 697 | while (gx--) { 698 | gy = cgh; 699 | while (gy--) { 700 | y = g; 701 | /* eslint no-labels: ['error', { 'allowLoop': true }] */ 702 | singleGridLoop: while (y--) { 703 | x = g; 704 | while (x--) { 705 | if (imageData[((gy * g + y) * width + (gx * g + x)) * 4 + 3]) { 706 | occupied.push([gx, gy]); 707 | 708 | if (gx < bounds[3]) { 709 | bounds[3] = gx; 710 | } 711 | if (gx > bounds[1]) { 712 | bounds[1] = gx; 713 | } 714 | if (gy < bounds[0]) { 715 | bounds[0] = gy; 716 | } 717 | if (gy > bounds[2]) { 718 | bounds[2] = gy; 719 | } 720 | 721 | if (debug) { 722 | fctx.fillStyle = 'rgba(255, 0, 0, 0.5)'; 723 | fctx.fillRect(gx * g, gy * g, g - 0.5, g - 0.5); 724 | } 725 | break singleGridLoop; 726 | } 727 | } 728 | } 729 | if (debug) { 730 | fctx.fillStyle = 'rgba(0, 0, 255, 0.5)'; 731 | fctx.fillRect(gx * g, gy * g, g - 0.5, g - 0.5); 732 | } 733 | } 734 | } 735 | 736 | if (debug) { 737 | fctx.fillStyle = 'rgba(0, 255, 0, 0.5)'; 738 | fctx.fillRect( 739 | bounds[3] * g, 740 | bounds[0] * g, 741 | (bounds[1] - bounds[3] + 1) * g, 742 | (bounds[2] - bounds[0] + 1) * g 743 | ); 744 | } 745 | 746 | // Return information needed to create the text on the real canvas 747 | return { 748 | mu: mu, 749 | occupied: occupied, 750 | bounds: bounds, 751 | gw: cgw, 752 | gh: cgh, 753 | fillTextOffsetX: fillTextOffsetX, 754 | fillTextOffsetY: fillTextOffsetY, 755 | fillTextWidth: fw, 756 | fillTextHeight: fh, 757 | fontSize: fontSize 758 | }; 759 | }; 760 | 761 | /* Determine if there is room available in the given dimension */ 762 | var canFitText = function canFitText(gx, gy, gw, gh, occupied) { 763 | // Go through the occupied points, 764 | // return false if the space is not available. 765 | var i = occupied.length; 766 | while (i--) { 767 | var px = gx + occupied[i][0]; 768 | var py = gy + occupied[i][1]; 769 | 770 | if (px >= ngx || py >= ngy || px < 0 || py < 0) { 771 | if (!settings.drawOutOfBound) { 772 | return false; 773 | } 774 | continue; 775 | } 776 | 777 | if (!grid[px][py]) { 778 | return false; 779 | } 780 | } 781 | return true; 782 | }; 783 | 784 | /* Actually draw the text on the grid */ 785 | var drawText = function drawText( 786 | gx, 787 | gy, 788 | info, 789 | word, 790 | weight, 791 | distance, 792 | theta, 793 | rotateDeg, 794 | attributes, 795 | extraDataArray 796 | ) { 797 | var fontSize = info.fontSize; 798 | var color; 799 | if (getTextColor) { 800 | color = getTextColor( 801 | word, 802 | weight, 803 | fontSize, 804 | distance, 805 | theta, 806 | extraDataArray 807 | ); 808 | } else { 809 | color = settings.color; 810 | } 811 | 812 | // get fontWeight that will be used to set ctx.font and font style rule 813 | var fontWeight; 814 | if (getTextFontWeight) { 815 | fontWeight = getTextFontWeight(word, weight, fontSize, extraDataArray); 816 | } else { 817 | fontWeight = settings.fontWeight; 818 | } 819 | 820 | var classes; 821 | if (getTextClasses) { 822 | classes = getTextClasses(word, weight, fontSize, extraDataArray); 823 | } else { 824 | classes = settings.classes; 825 | } 826 | 827 | elements.forEach(function (el) { 828 | if (el.getContext) { 829 | var ctx = el.getContext('2d'); 830 | var mu = info.mu; 831 | 832 | // Save the current state before messing it 833 | ctx.save(); 834 | ctx.scale(1 / mu, 1 / mu); 835 | 836 | ctx.font = 837 | fontWeight + 838 | ' ' + 839 | (fontSize * mu).toString(10) + 840 | 'px ' + 841 | settings.fontFamily; 842 | ctx.fillStyle = color; 843 | 844 | // Translate the canvas position to the origin coordinate of where 845 | // the text should be put. 846 | ctx.translate((gx + info.gw / 2) * g * mu, (gy + info.gh / 2) * g * mu); 847 | 848 | if (rotateDeg !== 0) { 849 | ctx.rotate(-rotateDeg); 850 | } 851 | 852 | // Finally, fill the text. 853 | 854 | // XXX: We cannot because textBaseline = 'top' here because 855 | // Firefox and Chrome uses different default line-height for canvas. 856 | // Please read https://bugzil.la/737852#c6. 857 | // Here, we use textBaseline = 'middle' and draw the text at exactly 858 | // 0.5 * fontSize lower. 859 | ctx.textBaseline = 'middle'; 860 | ctx.fillText( 861 | word, 862 | info.fillTextOffsetX * mu, 863 | (info.fillTextOffsetY + fontSize * 0.5) * mu 864 | ); 865 | 866 | // The below box is always matches how s are positioned 867 | /* ctx.strokeRect(info.fillTextOffsetX, info.fillTextOffsetY, 868 | info.fillTextWidth, info.fillTextHeight); */ 869 | 870 | // Restore the state. 871 | ctx.restore(); 872 | } else { 873 | // drawText on DIV element 874 | var span = document.createElement('span'); 875 | var transformRule = ''; 876 | transformRule = 'rotate(' + (-rotateDeg / Math.PI) * 180 + 'deg) '; 877 | if (info.mu !== 1) { 878 | transformRule += 879 | 'translateX(-' + 880 | info.fillTextWidth / 4 + 881 | 'px) ' + 882 | 'scale(' + 883 | 1 / info.mu + 884 | ')'; 885 | } 886 | var styleRules = { 887 | position: 'absolute', 888 | display: 'block', 889 | font: 890 | fontWeight + ' ' + fontSize * info.mu + 'px ' + settings.fontFamily, 891 | left: (gx + info.gw / 2) * g + info.fillTextOffsetX + 'px', 892 | top: (gy + info.gh / 2) * g + info.fillTextOffsetY + 'px', 893 | width: info.fillTextWidth + 'px', 894 | height: info.fillTextHeight + 'px', 895 | lineHeight: fontSize + 'px', 896 | whiteSpace: 'nowrap', 897 | transform: transformRule, 898 | webkitTransform: transformRule, 899 | msTransform: transformRule, 900 | transformOrigin: '50% 40%', 901 | webkitTransformOrigin: '50% 40%', 902 | msTransformOrigin: '50% 40%' 903 | }; 904 | if (color) { 905 | styleRules.color = color; 906 | } 907 | span.textContent = word; 908 | for (var cssProp in styleRules) { 909 | span.style[cssProp] = styleRules[cssProp]; 910 | } 911 | if (attributes) { 912 | for (var attribute in attributes) { 913 | span.setAttribute(attribute, attributes[attribute]); 914 | } 915 | } 916 | if (classes) { 917 | span.className += classes; 918 | } 919 | el.appendChild(span); 920 | } 921 | }); 922 | }; 923 | 924 | /* Help function to updateGrid */ 925 | var fillGridAt = function fillGridAt(x, y, drawMask, dimension, item) { 926 | if (x >= ngx || y >= ngy || x < 0 || y < 0) { 927 | return; 928 | } 929 | 930 | grid[x][y] = false; 931 | 932 | if (drawMask) { 933 | var ctx = elements[0].getContext('2d'); 934 | ctx.fillRect(x * g, y * g, maskRectWidth, maskRectWidth); 935 | } 936 | 937 | if (interactive) { 938 | infoGrid[x][y] = { item: item, dimension: dimension }; 939 | } 940 | }; 941 | 942 | /* Update the filling information of the given space with occupied points. 943 | Draw the mask on the canvas if necessary. */ 944 | var updateGrid = function updateGrid(gx, gy, gw, gh, info, item) { 945 | var occupied = info.occupied; 946 | var drawMask = settings.drawMask; 947 | var ctx; 948 | if (drawMask) { 949 | ctx = elements[0].getContext('2d'); 950 | ctx.save(); 951 | ctx.fillStyle = settings.maskColor; 952 | } 953 | 954 | var dimension; 955 | if (interactive) { 956 | var bounds = info.bounds; 957 | dimension = { 958 | x: (gx + bounds[3]) * g, 959 | y: (gy + bounds[0]) * g, 960 | w: (bounds[1] - bounds[3] + 1) * g, 961 | h: (bounds[2] - bounds[0] + 1) * g 962 | }; 963 | } 964 | 965 | var i = occupied.length; 966 | while (i--) { 967 | var px = gx + occupied[i][0]; 968 | var py = gy + occupied[i][1]; 969 | 970 | if (px >= ngx || py >= ngy || px < 0 || py < 0) { 971 | continue; 972 | } 973 | 974 | fillGridAt(px, py, drawMask, dimension, item); 975 | } 976 | 977 | if (drawMask) { 978 | ctx.restore(); 979 | } 980 | }; 981 | 982 | /* putWord() processes each item on the list, 983 | calculate it's size and determine it's position, and actually 984 | put it on the canvas. */ 985 | var putWord = function putWord(item, loopIndex) { 986 | if (loopIndex > 20) { 987 | return null; 988 | } 989 | 990 | var word, weight, attributes; 991 | if (Array.isArray(item)) { 992 | word = item[0]; 993 | weight = item[1]; 994 | } else { 995 | word = item.word; 996 | weight = item.weight; 997 | attributes = item.attributes; 998 | } 999 | var rotateDeg = getRotateDeg(); 1000 | 1001 | var extraDataArray = getItemExtraData(item); 1002 | 1003 | // get info needed to put the text onto the canvas 1004 | var info = getTextInfo(word, weight, rotateDeg, extraDataArray); 1005 | 1006 | // not getting the info means we shouldn't be drawing this one. 1007 | if (!info) { 1008 | return false; 1009 | } 1010 | 1011 | if (exceedTime()) { 1012 | return false; 1013 | } 1014 | 1015 | // If drawOutOfBound is set to false, 1016 | // skip the loop if we have already know the bounding box of 1017 | // word is larger than the canvas. 1018 | if (!settings.drawOutOfBound && !settings.shrinkToFit) { 1019 | var bounds = info.bounds; 1020 | if (bounds[1] - bounds[3] + 1 > ngx || bounds[2] - bounds[0] + 1 > ngy) { 1021 | return false; 1022 | } 1023 | } 1024 | 1025 | // Determine the position to put the text by 1026 | // start looking for the nearest points 1027 | var r = maxRadius + 1; 1028 | 1029 | var tryToPutWordAtPoint = function (gxy) { 1030 | var gx = Math.floor(gxy[0] - info.gw / 2); 1031 | var gy = Math.floor(gxy[1] - info.gh / 2); 1032 | var gw = info.gw; 1033 | var gh = info.gh; 1034 | 1035 | // If we cannot fit the text at this position, return false 1036 | // and go to the next position. 1037 | if (!canFitText(gx, gy, gw, gh, info.occupied)) { 1038 | return false; 1039 | } 1040 | 1041 | // Actually put the text on the canvas 1042 | drawText( 1043 | gx, 1044 | gy, 1045 | info, 1046 | word, 1047 | weight, 1048 | maxRadius - r, 1049 | gxy[2], 1050 | rotateDeg, 1051 | attributes, 1052 | extraDataArray 1053 | ); 1054 | 1055 | // Mark the spaces on the grid as filled 1056 | updateGrid(gx, gy, gw, gh, info, item); 1057 | 1058 | return { 1059 | gx: gx, 1060 | gy: gy, 1061 | rot: rotateDeg, 1062 | info: info 1063 | }; 1064 | }; 1065 | 1066 | while (r--) { 1067 | var points = getPointsAtRadius(maxRadius - r); 1068 | 1069 | if (settings.shuffle) { 1070 | points = [].concat(points); 1071 | shuffleArray(points); 1072 | } 1073 | 1074 | // Try to fit the words by looking at each point. 1075 | // array.some() will stop and return true 1076 | // when putWordAtPoint() returns true. 1077 | for (var i = 0; i < points.length; i++) { 1078 | var res = tryToPutWordAtPoint(points[i]); 1079 | if (res) { 1080 | return res; 1081 | } 1082 | } 1083 | 1084 | // var drawn = points.some(tryToPutWordAtPoint); 1085 | // if (drawn) { 1086 | // // leave putWord() and return true 1087 | // return true; 1088 | // } 1089 | } 1090 | 1091 | if (settings.shrinkToFit) { 1092 | if (Array.isArray(item)) { 1093 | item[1] = (item[1] * 3) / 4; 1094 | } else { 1095 | item.weight = (item.weight * 3) / 4; 1096 | } 1097 | return putWord(item, loopIndex + 1); 1098 | } 1099 | 1100 | // we tried all distances but text won't fit, return null 1101 | return null; 1102 | }; 1103 | 1104 | /* Send DOM event to all elements. Will stop sending event and return 1105 | if the previous one is canceled (for cancelable events). */ 1106 | var sendEvent = function sendEvent(type, cancelable, details) { 1107 | if (cancelable) { 1108 | return !elements.some(function (el) { 1109 | var event = new CustomEvent(type, { 1110 | detail: details || {} 1111 | }); 1112 | return !el.dispatchEvent(event); 1113 | }, this); 1114 | } else { 1115 | elements.forEach(function (el) { 1116 | var event = new CustomEvent(type, { 1117 | detail: details || {} 1118 | }); 1119 | el.dispatchEvent(event); 1120 | }, this); 1121 | } 1122 | }; 1123 | 1124 | /* Start drawing on a canvas */ 1125 | var start = function start() { 1126 | // For dimensions, clearCanvas etc., 1127 | // we only care about the first element. 1128 | var canvas = elements[0]; 1129 | 1130 | if (canvas.getContext) { 1131 | ngx = Math.ceil(canvas.width / g); 1132 | ngy = Math.ceil(canvas.height / g); 1133 | } else { 1134 | var rect = canvas.getBoundingClientRect(); 1135 | ngx = Math.ceil(rect.width / g); 1136 | ngy = Math.ceil(rect.height / g); 1137 | } 1138 | 1139 | // Sending a wordcloudstart event which cause the previous loop to stop. 1140 | // Do nothing if the event is canceled. 1141 | if (!sendEvent('wordcloudstart', true)) { 1142 | return; 1143 | } 1144 | 1145 | // Determine the center of the word cloud 1146 | center = settings.origin 1147 | ? [settings.origin[0] / g, settings.origin[1] / g] 1148 | : [ngx / 2, ngy / 2]; 1149 | 1150 | // Maxium radius to look for space 1151 | maxRadius = Math.floor(Math.sqrt(ngx * ngx + ngy * ngy)); 1152 | 1153 | /* Clear the canvas only if the clearCanvas is set, 1154 | if not, update the grid to the current canvas state */ 1155 | grid = []; 1156 | 1157 | var gx, gy, i; 1158 | if (!canvas.getContext || settings.clearCanvas) { 1159 | elements.forEach(function (el) { 1160 | if (el.getContext) { 1161 | var ctx = el.getContext('2d'); 1162 | ctx.fillStyle = settings.backgroundColor; 1163 | ctx.clearRect(0, 0, ngx * (g + 1), ngy * (g + 1)); 1164 | ctx.fillRect(0, 0, ngx * (g + 1), ngy * (g + 1)); 1165 | } else { 1166 | el.textContent = ''; 1167 | el.style.backgroundColor = settings.backgroundColor; 1168 | el.style.position = 'relative'; 1169 | } 1170 | }); 1171 | 1172 | /* fill the grid with empty state */ 1173 | gx = ngx; 1174 | while (gx--) { 1175 | grid[gx] = []; 1176 | gy = ngy; 1177 | while (gy--) { 1178 | grid[gx][gy] = true; 1179 | } 1180 | } 1181 | } else { 1182 | /* Determine bgPixel by creating 1183 | another canvas and fill the specified background color. */ 1184 | var bctx = document.createElement('canvas').getContext('2d'); 1185 | 1186 | bctx.fillStyle = settings.backgroundColor; 1187 | bctx.fillRect(0, 0, 1, 1); 1188 | var bgPixel = bctx.getImageData(0, 0, 1, 1).data; 1189 | 1190 | /* Read back the pixels of the canvas we got to tell which part of the 1191 | canvas is empty. 1192 | (no clearCanvas only works with a canvas, not divs) */ 1193 | var imageData = canvas 1194 | .getContext('2d') 1195 | .getImageData(0, 0, ngx * g, ngy * g).data; 1196 | 1197 | gx = ngx; 1198 | var x, y; 1199 | while (gx--) { 1200 | grid[gx] = []; 1201 | gy = ngy; 1202 | while (gy--) { 1203 | y = g; 1204 | /* eslint no-labels: ['error', { 'allowLoop': true }] */ 1205 | singleGridLoop: while (y--) { 1206 | x = g; 1207 | while (x--) { 1208 | i = 4; 1209 | while (i--) { 1210 | if ( 1211 | imageData[((gy * g + y) * ngx * g + (gx * g + x)) * 4 + i] !== 1212 | bgPixel[i] 1213 | ) { 1214 | grid[gx][gy] = false; 1215 | break singleGridLoop; 1216 | } 1217 | } 1218 | } 1219 | } 1220 | if (grid[gx][gy] !== false) { 1221 | grid[gx][gy] = true; 1222 | } 1223 | } 1224 | } 1225 | 1226 | imageData = bctx = bgPixel = undefined; 1227 | } 1228 | 1229 | // fill the infoGrid with empty state if we need it 1230 | if (settings.hover || settings.click) { 1231 | interactive = true; 1232 | 1233 | /* fill the grid with empty state */ 1234 | gx = ngx + 1; 1235 | while (gx--) { 1236 | infoGrid[gx] = []; 1237 | } 1238 | 1239 | if (settings.hover) { 1240 | canvas.addEventListener('mousemove', wordcloudhover); 1241 | } 1242 | 1243 | if (settings.click) { 1244 | canvas.addEventListener('click', wordcloudclick); 1245 | canvas.addEventListener('touchstart', wordcloudclick); 1246 | canvas.addEventListener('touchend', function (e) { 1247 | e.preventDefault(); 1248 | }); 1249 | canvas.style.webkitTapHighlightColor = 'rgba(0, 0, 0, 0)'; 1250 | } 1251 | 1252 | canvas.addEventListener('wordcloudstart', function stopInteraction() { 1253 | canvas.removeEventListener('wordcloudstart', stopInteraction); 1254 | 1255 | canvas.removeEventListener('mousemove', wordcloudhover); 1256 | canvas.removeEventListener('click', wordcloudclick); 1257 | hovered = undefined; 1258 | }); 1259 | } 1260 | 1261 | i = 0; 1262 | var loopingFunction, stoppingFunction; 1263 | var layouting = true; 1264 | if (!settings.layoutAnimation) { 1265 | loopingFunction = function (cb) { 1266 | cb(); 1267 | }; 1268 | stoppingFunction = function () { 1269 | layouting = false; 1270 | }; 1271 | } else if (settings.wait !== 0) { 1272 | loopingFunction = window.setTimeout; 1273 | stoppingFunction = window.clearTimeout; 1274 | } else { 1275 | loopingFunction = window.setImmediate; 1276 | stoppingFunction = window.clearImmediate; 1277 | } 1278 | 1279 | var addEventListener = function addEventListener(type, listener) { 1280 | elements.forEach(function (el) { 1281 | el.addEventListener(type, listener); 1282 | }, this); 1283 | }; 1284 | 1285 | var removeEventListener = function removeEventListener(type, listener) { 1286 | elements.forEach(function (el) { 1287 | el.removeEventListener(type, listener); 1288 | }, this); 1289 | }; 1290 | 1291 | var anotherWordCloudStart = function anotherWordCloudStart() { 1292 | removeEventListener('wordcloudstart', anotherWordCloudStart); 1293 | stoppingFunction(timer[timerId]); 1294 | }; 1295 | 1296 | addEventListener('wordcloudstart', anotherWordCloudStart); 1297 | 1298 | // At least wait the following code before call the first iteration. 1299 | timer[timerId] = (settings.layoutAnimation ? loopingFunction : setTimeout)( 1300 | function loop() { 1301 | if (!layouting) { 1302 | return; 1303 | } 1304 | if (i >= settings.list.length) { 1305 | stoppingFunction(timer[timerId]); 1306 | sendEvent('wordcloudstop', false); 1307 | removeEventListener('wordcloudstart', anotherWordCloudStart); 1308 | delete timer[timerId]; 1309 | return; 1310 | } 1311 | escapeTime = new Date().getTime(); 1312 | var drawn = putWord(settings.list[i], 0); 1313 | var canceled = !sendEvent('wordclouddrawn', true, { 1314 | item: settings.list[i], 1315 | drawn: drawn 1316 | }); 1317 | if (exceedTime() || canceled) { 1318 | stoppingFunction(timer[timerId]); 1319 | settings.abort(); 1320 | sendEvent('wordcloudabort', false); 1321 | sendEvent('wordcloudstop', false); 1322 | removeEventListener('wordcloudstart', anotherWordCloudStart); 1323 | return; 1324 | } 1325 | i++; 1326 | timer[timerId] = loopingFunction(loop, settings.wait); 1327 | }, 1328 | settings.wait 1329 | ); 1330 | }; 1331 | 1332 | // All set, start the drawing 1333 | start(); 1334 | }; 1335 | 1336 | WordCloud.isSupported = isSupported; 1337 | WordCloud.minFontSize = minFontSize; 1338 | 1339 | export default WordCloud; 1340 | -------------------------------------------------------------------------------- /src/wordCloud.js: -------------------------------------------------------------------------------- 1 | import * as echarts from 'echarts/lib/echarts'; 2 | 3 | import './WordCloudSeries'; 4 | import './WordCloudView'; 5 | 6 | import wordCloudLayoutHelper from './layout'; 7 | 8 | if (!wordCloudLayoutHelper.isSupported) { 9 | throw new Error('Sorry your browser not support wordCloud'); 10 | } 11 | 12 | // https://github.com/timdream/wordcloud2.js/blob/c236bee60436e048949f9becc4f0f67bd832dc5c/index.js#L233 13 | function updateCanvasMask(maskCanvas) { 14 | var ctx = maskCanvas.getContext('2d'); 15 | var imageData = ctx.getImageData(0, 0, maskCanvas.width, maskCanvas.height); 16 | var newImageData = ctx.createImageData(imageData); 17 | 18 | var toneSum = 0; 19 | var toneCnt = 0; 20 | for (var i = 0; i < imageData.data.length; i += 4) { 21 | var alpha = imageData.data[i + 3]; 22 | if (alpha > 128) { 23 | var tone = 24 | imageData.data[i] + imageData.data[i + 1] + imageData.data[i + 2]; 25 | toneSum += tone; 26 | ++toneCnt; 27 | } 28 | } 29 | var threshold = toneSum / toneCnt; 30 | 31 | for (var i = 0; i < imageData.data.length; i += 4) { 32 | var tone = 33 | imageData.data[i] + imageData.data[i + 1] + imageData.data[i + 2]; 34 | var alpha = imageData.data[i + 3]; 35 | 36 | if (alpha < 128 || tone > threshold) { 37 | // Area not to draw 38 | newImageData.data[i] = 0; 39 | newImageData.data[i + 1] = 0; 40 | newImageData.data[i + 2] = 0; 41 | newImageData.data[i + 3] = 0; 42 | } else { 43 | // Area to draw 44 | // The color must be same with backgroundColor 45 | newImageData.data[i] = 255; 46 | newImageData.data[i + 1] = 255; 47 | newImageData.data[i + 2] = 255; 48 | newImageData.data[i + 3] = 255; 49 | } 50 | } 51 | 52 | ctx.putImageData(newImageData, 0, 0); 53 | } 54 | 55 | echarts.registerLayout(function (ecModel, api) { 56 | ecModel.eachSeriesByType('wordCloud', function (seriesModel) { 57 | var gridRect = echarts.helper.getLayoutRect( 58 | seriesModel.getBoxLayoutParams(), 59 | { 60 | width: api.getWidth(), 61 | height: api.getHeight() 62 | } 63 | ); 64 | 65 | var keepAspect = seriesModel.get('keepAspect'); 66 | var maskImage = seriesModel.get('maskImage'); 67 | var ratio = maskImage ? maskImage.width / maskImage.height : 1; 68 | keepAspect && adjustRectAspect(gridRect, ratio); 69 | 70 | var data = seriesModel.getData(); 71 | 72 | var canvas = document.createElement('canvas'); 73 | canvas.width = gridRect.width; 74 | canvas.height = gridRect.height; 75 | 76 | var ctx = canvas.getContext('2d'); 77 | if (maskImage) { 78 | try { 79 | ctx.drawImage(maskImage, 0, 0, canvas.width, canvas.height); 80 | updateCanvasMask(canvas); 81 | } catch (e) { 82 | console.error('Invalid mask image'); 83 | console.error(e.toString()); 84 | } 85 | } 86 | 87 | var sizeRange = seriesModel.get('sizeRange'); 88 | var rotationRange = seriesModel.get('rotationRange'); 89 | var valueExtent = data.getDataExtent('value'); 90 | 91 | var DEGREE_TO_RAD = Math.PI / 180; 92 | var gridSize = seriesModel.get('gridSize'); 93 | wordCloudLayoutHelper(canvas, { 94 | list: data 95 | .mapArray('value', function (value, idx) { 96 | var itemModel = data.getItemModel(idx); 97 | return [ 98 | data.getName(idx), 99 | itemModel.get('textStyle.fontSize', true) || 100 | echarts.number.linearMap(value, valueExtent, sizeRange), 101 | idx 102 | ]; 103 | }) 104 | .sort(function (a, b) { 105 | // Sort from large to small in case there is no more room for more words 106 | return b[1] - a[1]; 107 | }), 108 | fontFamily: 109 | seriesModel.get('textStyle.fontFamily') || 110 | seriesModel.get('emphasis.textStyle.fontFamily') || 111 | ecModel.get('textStyle.fontFamily'), 112 | fontWeight: 113 | seriesModel.get('textStyle.fontWeight') || 114 | seriesModel.get('emphasis.textStyle.fontWeight') || 115 | ecModel.get('textStyle.fontWeight'), 116 | 117 | gridSize: gridSize, 118 | 119 | ellipticity: gridRect.height / gridRect.width, 120 | 121 | minRotation: rotationRange[0] * DEGREE_TO_RAD, 122 | maxRotation: rotationRange[1] * DEGREE_TO_RAD, 123 | 124 | clearCanvas: !maskImage, 125 | 126 | rotateRatio: 1, 127 | 128 | rotationStep: seriesModel.get('rotationStep') * DEGREE_TO_RAD, 129 | 130 | drawOutOfBound: seriesModel.get('drawOutOfBound'), 131 | shrinkToFit: seriesModel.get('shrinkToFit'), 132 | 133 | layoutAnimation: seriesModel.get('layoutAnimation'), 134 | 135 | shuffle: false, 136 | 137 | shape: seriesModel.get('shape') 138 | }); 139 | 140 | function onWordCloudDrawn(e) { 141 | var item = e.detail.item; 142 | if (e.detail.drawn && seriesModel.layoutInstance.ondraw) { 143 | e.detail.drawn.gx += gridRect.x / gridSize; 144 | e.detail.drawn.gy += gridRect.y / gridSize; 145 | seriesModel.layoutInstance.ondraw( 146 | item[0], 147 | item[1], 148 | item[2], 149 | e.detail.drawn 150 | ); 151 | } 152 | } 153 | 154 | canvas.addEventListener('wordclouddrawn', onWordCloudDrawn); 155 | 156 | if (seriesModel.layoutInstance) { 157 | // Dispose previous 158 | seriesModel.layoutInstance.dispose(); 159 | } 160 | 161 | seriesModel.layoutInstance = { 162 | ondraw: null, 163 | 164 | dispose: function () { 165 | canvas.removeEventListener('wordclouddrawn', onWordCloudDrawn); 166 | // Abort 167 | canvas.addEventListener('wordclouddrawn', function (e) { 168 | // Prevent default to cancle the event and stop the loop 169 | e.preventDefault(); 170 | }); 171 | } 172 | }; 173 | }); 174 | }); 175 | 176 | echarts.registerPreprocessor(function (option) { 177 | var series = (option || {}).series; 178 | !echarts.util.isArray(series) && (series = series ? [series] : []); 179 | 180 | var compats = ['shadowColor', 'shadowBlur', 'shadowOffsetX', 'shadowOffsetY']; 181 | 182 | echarts.util.each(series, function (seriesItem) { 183 | if (seriesItem && seriesItem.type === 'wordCloud') { 184 | var textStyle = seriesItem.textStyle || {}; 185 | 186 | compatTextStyle(textStyle.normal); 187 | compatTextStyle(textStyle.emphasis); 188 | } 189 | }); 190 | 191 | function compatTextStyle(textStyle) { 192 | textStyle && 193 | echarts.util.each(compats, function (key) { 194 | if (textStyle.hasOwnProperty(key)) { 195 | textStyle['text' + echarts.format.capitalFirst(key)] = textStyle[key]; 196 | } 197 | }); 198 | } 199 | }); 200 | 201 | function adjustRectAspect(gridRect, aspect) { 202 | // var outerWidth = gridRect.width + gridRect.x * 2; 203 | // var outerHeight = gridRect.height + gridRect.y * 2; 204 | var width = gridRect.width; 205 | var height = gridRect.height; 206 | if (width > height * aspect) { 207 | gridRect.x += (width - height * aspect) / 2; 208 | gridRect.width = height * aspect; 209 | } else { 210 | gridRect.y += (height - width / aspect) / 2; 211 | gridRect.height = width / aspect; 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = (env, options) => { 2 | return { 3 | entry: { 4 | 'echarts-wordcloud': __dirname + '/index.js' 5 | }, 6 | output: { 7 | libraryTarget: 'umd', 8 | library: ['echarts-wordcloud'], 9 | path: __dirname + '/dist', 10 | filename: options.mode === 'production' ? '[name].min.js' : '[name].js' 11 | }, 12 | optimization: { 13 | concatenateModules: true 14 | }, 15 | devtool: 'source-map', 16 | externals: { 17 | 'echarts/lib/echarts': 'echarts' 18 | } 19 | }; 20 | }; 21 | --------------------------------------------------------------------------------