wordcloud2.js 37 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237
  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. 'use strict'
  9. // setImmediate
  10. if (!window.setImmediate) {
  11. window.setImmediate = (function setupSetImmediate () {
  12. return window.msSetImmediate ||
  13. window.webkitSetImmediate ||
  14. window.mozSetImmediate ||
  15. window.oSetImmediate ||
  16. (function setupSetZeroTimeout () {
  17. if (!window.postMessage || !window.addEventListener) {
  18. return null
  19. }
  20. var callbacks = [undefined]
  21. var message = 'zero-timeout-message'
  22. // Like setTimeout, but only takes a function argument. There's
  23. // no time argument (always zero) and no arguments (you have to
  24. // use a closure).
  25. var setZeroTimeout = function setZeroTimeout (callback) {
  26. var id = callbacks.length
  27. callbacks.push(callback)
  28. window.postMessage(message + id.toString(36), '*')
  29. return id
  30. }
  31. window.addEventListener('message', function setZeroTimeoutMessage (evt) {
  32. // Skipping checking event source, retarded IE confused this window
  33. // object with another in the presence of iframe
  34. if (typeof evt.data !== 'string' ||
  35. evt.data.substr(0, message.length) !== message/* ||
  36. evt.source !== window */) {
  37. return
  38. }
  39. evt.stopImmediatePropagation()
  40. var id = parseInt(evt.data.substr(message.length), 36)
  41. if (!callbacks[id]) {
  42. return
  43. }
  44. callbacks[id]()
  45. callbacks[id] = undefined
  46. }, true)
  47. /* specify clearImmediate() here since we need the scope */
  48. window.clearImmediate = function clearZeroTimeout (id) {
  49. if (!callbacks[id]) {
  50. return
  51. }
  52. callbacks[id] = undefined
  53. }
  54. return setZeroTimeout
  55. })() ||
  56. // fallback
  57. function setImmediateFallback (fn) {
  58. window.setTimeout(fn, 0)
  59. }
  60. })()
  61. }
  62. if (!window.clearImmediate) {
  63. window.clearImmediate = (function setupClearImmediate () {
  64. return window.msClearImmediate ||
  65. window.webkitClearImmediate ||
  66. window.mozClearImmediate ||
  67. window.oClearImmediate ||
  68. // "clearZeroTimeout" is implement on the previous block ||
  69. // fallback
  70. function clearImmediateFallback (timer) {
  71. window.clearTimeout(timer)
  72. }
  73. })()
  74. }
  75. (function (global) {
  76. // Check if WordCloud can run on this browser
  77. var isSupported = (function isSupported () {
  78. var canvas = document.createElement('canvas')
  79. if (!canvas || !canvas.getContext) {
  80. return false
  81. }
  82. var ctx = canvas.getContext('2d')
  83. if (!ctx) {
  84. return false
  85. }
  86. if (!ctx.getImageData) {
  87. return false
  88. }
  89. if (!ctx.fillText) {
  90. return false
  91. }
  92. if (!Array.prototype.some) {
  93. return false
  94. }
  95. if (!Array.prototype.push) {
  96. return false
  97. }
  98. return true
  99. }())
  100. // Find out if the browser impose minium font size by
  101. // drawing small texts on a canvas and measure it's width.
  102. var minFontSize = (function getMinFontSize () {
  103. if (!isSupported) {
  104. return
  105. }
  106. var ctx = document.createElement('canvas').getContext('2d')
  107. // start from 20
  108. var size = 20
  109. // two sizes to measure
  110. var hanWidth, mWidth
  111. while (size) {
  112. ctx.font = size.toString(10) + 'px sans-serif'
  113. if ((ctx.measureText('\uFF37').width === hanWidth) &&
  114. (ctx.measureText('m').width) === mWidth) {
  115. return (size + 1)
  116. }
  117. hanWidth = ctx.measureText('\uFF37').width
  118. mWidth = ctx.measureText('m').width
  119. size--
  120. }
  121. return 0
  122. })()
  123. var getItemExtraData = function (item) {
  124. if (Array.isArray(item)) {
  125. var itemCopy = item.slice()
  126. // remove data we already have (word and weight)
  127. itemCopy.splice(0, 2)
  128. return itemCopy
  129. } else {
  130. return []
  131. }
  132. }
  133. // Based on http://jsfromhell.com/array/shuffle
  134. var shuffleArray = function shuffleArray (arr) {
  135. for (var j, x, i = arr.length; i;) {
  136. j = Math.floor(Math.random() * i)
  137. x = arr[--i]
  138. arr[i] = arr[j]
  139. arr[j] = x
  140. }
  141. return arr
  142. }
  143. var timer = {};
  144. var WordCloud = function WordCloud (elements, options) {
  145. if (!isSupported) {
  146. return
  147. }
  148. var timerId = Math.floor(Math.random() * Date.now())
  149. if (!Array.isArray(elements)) {
  150. elements = [elements]
  151. }
  152. elements.forEach(function (el, i) {
  153. if (typeof el === 'string') {
  154. elements[i] = document.getElementById(el)
  155. if (!elements[i]) {
  156. throw new Error('The element id specified is not found.')
  157. }
  158. } else if (!el.tagName && !el.appendChild) {
  159. throw new Error('You must pass valid HTML elements, or ID of the element.')
  160. }
  161. })
  162. /* Default values to be overwritten by options object */
  163. var settings = {
  164. list: [],
  165. fontFamily: '"Trebuchet MS", "Heiti TC", "微軟正黑體", ' +
  166. '"Arial Unicode MS", "Droid Fallback Sans", sans-serif',
  167. fontWeight: 'normal',
  168. color: 'random-dark',
  169. minSize: 0, // 0 to disable
  170. weightFactor: 1,
  171. clearCanvas: true,
  172. backgroundColor: '#fff', // opaque white = rgba(255, 255, 255, 1)
  173. gridSize: 8,
  174. drawOutOfBound: false,
  175. shrinkToFit: false,
  176. origin: null,
  177. drawMask: false,
  178. maskColor: 'rgba(255,0,0,0.3)',
  179. maskGapWidth: 0.3,
  180. wait: 0,
  181. abortThreshold: 0, // disabled
  182. abort: function noop () {},
  183. minRotation: -Math.PI / 2,
  184. maxRotation: Math.PI / 2,
  185. rotationSteps: 0,
  186. shuffle: true,
  187. rotateRatio: 0.1,
  188. shape: 'circle',
  189. ellipticity: 0.65,
  190. classes: null,
  191. hover: null,
  192. click: null
  193. }
  194. if (options) {
  195. for (var key in options) {
  196. if (key in settings) {
  197. settings[key] = options[key]
  198. }
  199. }
  200. }
  201. /* Convert weightFactor into a function */
  202. if (typeof settings.weightFactor !== 'function') {
  203. var factor = settings.weightFactor
  204. settings.weightFactor = function weightFactor (pt) {
  205. return pt * factor // in px
  206. }
  207. }
  208. /* Convert shape into a function */
  209. if (typeof settings.shape !== 'function') {
  210. switch (settings.shape) {
  211. case 'circle':
  212. /* falls through */
  213. default:
  214. // 'circle' is the default and a shortcut in the code loop.
  215. settings.shape = 'circle'
  216. break
  217. case 'cardioid':
  218. settings.shape = function shapeCardioid (theta) {
  219. return 1 - Math.sin(theta)
  220. }
  221. break
  222. /*
  223. To work out an X-gon, one has to calculate "m",
  224. where 1/(cos(2*PI/X)+m*sin(2*PI/X)) = 1/(cos(0)+m*sin(0))
  225. http://www.wolframalpha.com/input/?i=1%2F%28cos%282*PI%2FX%29%2Bm*sin%28
  226. 2*PI%2FX%29%29+%3D+1%2F%28cos%280%29%2Bm*sin%280%29%29
  227. Copy the solution into polar equation r = 1/(cos(t') + m*sin(t'))
  228. where t' equals to mod(t, 2PI/X)
  229. */
  230. case 'diamond':
  231. // http://www.wolframalpha.com/input/?i=plot+r+%3D+1%2F%28cos%28mod+
  232. // %28t%2C+PI%2F2%29%29%2Bsin%28mod+%28t%2C+PI%2F2%29%29%29%2C+t+%3D
  233. // +0+..+2*PI
  234. settings.shape = function shapeSquare (theta) {
  235. var thetaPrime = theta % (2 * Math.PI / 4)
  236. return 1 / (Math.cos(thetaPrime) + Math.sin(thetaPrime))
  237. }
  238. break
  239. case 'square':
  240. // http://www.wolframalpha.com/input/?i=plot+r+%3D+min(1%2Fabs(cos(t
  241. // )),1%2Fabs(sin(t)))),+t+%3D+0+..+2*PI
  242. settings.shape = function shapeSquare (theta) {
  243. return Math.min(
  244. 1 / Math.abs(Math.cos(theta)),
  245. 1 / Math.abs(Math.sin(theta))
  246. )
  247. }
  248. break
  249. case 'triangle-forward':
  250. // http://www.wolframalpha.com/input/?i=plot+r+%3D+1%2F%28cos%28mod+
  251. // %28t%2C+2*PI%2F3%29%29%2Bsqrt%283%29sin%28mod+%28t%2C+2*PI%2F3%29
  252. // %29%29%2C+t+%3D+0+..+2*PI
  253. settings.shape = function shapeTriangle (theta) {
  254. var thetaPrime = theta % (2 * Math.PI / 3)
  255. return 1 / (Math.cos(thetaPrime) +
  256. Math.sqrt(3) * Math.sin(thetaPrime))
  257. }
  258. break
  259. case 'triangle':
  260. case 'triangle-upright':
  261. settings.shape = function shapeTriangle (theta) {
  262. var thetaPrime = (theta + Math.PI * 3 / 2) % (2 * Math.PI / 3)
  263. return 1 / (Math.cos(thetaPrime) +
  264. Math.sqrt(3) * Math.sin(thetaPrime))
  265. }
  266. break
  267. case 'pentagon':
  268. settings.shape = function shapePentagon (theta) {
  269. var thetaPrime = (theta + 0.955) % (2 * Math.PI / 5)
  270. return 1 / (Math.cos(thetaPrime) +
  271. 0.726543 * Math.sin(thetaPrime))
  272. }
  273. break
  274. case 'star':
  275. settings.shape = function shapeStar (theta) {
  276. var thetaPrime = (theta + 0.955) % (2 * Math.PI / 10)
  277. if ((theta + 0.955) % (2 * Math.PI / 5) - (2 * Math.PI / 10) >= 0) {
  278. return 1 / (Math.cos((2 * Math.PI / 10) - thetaPrime) +
  279. 3.07768 * Math.sin((2 * Math.PI / 10) - thetaPrime))
  280. } else {
  281. return 1 / (Math.cos(thetaPrime) +
  282. 3.07768 * Math.sin(thetaPrime))
  283. }
  284. }
  285. break
  286. }
  287. }
  288. /* Make sure gridSize is a whole number and is not smaller than 4px */
  289. settings.gridSize = Math.max(Math.floor(settings.gridSize), 4)
  290. /* shorthand */
  291. var g = settings.gridSize
  292. var maskRectWidth = g - settings.maskGapWidth
  293. /* normalize rotation settings */
  294. var rotationRange = Math.abs(settings.maxRotation - settings.minRotation)
  295. var rotationSteps = Math.abs(Math.floor(settings.rotationSteps))
  296. var minRotation = Math.min(settings.maxRotation, settings.minRotation)
  297. /* information/object available to all functions, set when start() */
  298. var grid, // 2d array containing filling information
  299. ngx, ngy, // width and height of the grid
  300. center, // position of the center of the cloud
  301. maxRadius
  302. /* timestamp for measuring each putWord() action */
  303. var escapeTime
  304. /* function for getting the color of the text */
  305. var getTextColor
  306. function randomHslColor (min, max) {
  307. return 'hsl(' +
  308. (Math.random() * 360).toFixed() + ',' +
  309. (Math.random() * 30 + 70).toFixed() + '%,' +
  310. (Math.random() * (max - min) + min).toFixed() + '%)'
  311. }
  312. switch (settings.color) {
  313. case 'random-dark':
  314. getTextColor = function getRandomDarkColor () {
  315. return randomHslColor(10, 50)
  316. }
  317. break
  318. case 'random-light':
  319. getTextColor = function getRandomLightColor () {
  320. return randomHslColor(50, 90)
  321. }
  322. break
  323. default:
  324. if (typeof settings.color === 'function') {
  325. getTextColor = settings.color
  326. }
  327. break
  328. }
  329. /* function for getting the font-weight of the text */
  330. var getTextFontWeight
  331. if (typeof settings.fontWeight === 'function') {
  332. getTextFontWeight = settings.fontWeight
  333. }
  334. /* function for getting the classes of the text */
  335. var getTextClasses = null
  336. if (typeof settings.classes === 'function') {
  337. getTextClasses = settings.classes
  338. }
  339. /* Interactive */
  340. var interactive = false
  341. var infoGrid = []
  342. var hovered
  343. var getInfoGridFromMouseTouchEvent =
  344. function getInfoGridFromMouseTouchEvent (evt) {
  345. var canvas = evt.currentTarget
  346. var rect = canvas.getBoundingClientRect()
  347. var clientX
  348. var clientY
  349. /** Detect if touches are available */
  350. if (evt.touches) {
  351. clientX = evt.touches[0].clientX
  352. clientY = evt.touches[0].clientY
  353. } else {
  354. clientX = evt.clientX
  355. clientY = evt.clientY
  356. }
  357. var eventX = clientX - rect.left
  358. var eventY = clientY - rect.top
  359. var x = Math.floor(eventX * ((canvas.width / rect.width) || 1) / g)
  360. var y = Math.floor(eventY * ((canvas.height / rect.height) || 1) / g)
  361. return infoGrid[x][y]
  362. }
  363. var wordcloudhover = function wordcloudhover (evt) {
  364. var info = getInfoGridFromMouseTouchEvent(evt)
  365. if (hovered === info) {
  366. return
  367. }
  368. hovered = info
  369. if (!info) {
  370. settings.hover(undefined, undefined, evt)
  371. return
  372. }
  373. settings.hover(info.item, info.dimension, evt)
  374. }
  375. var wordcloudclick = function wordcloudclick (evt) {
  376. var info = getInfoGridFromMouseTouchEvent(evt)
  377. if (!info) {
  378. return
  379. }
  380. settings.click(info.item, info.dimension, evt)
  381. evt.preventDefault()
  382. }
  383. /* Get points on the grid for a given radius away from the center */
  384. var pointsAtRadius = []
  385. var getPointsAtRadius = function getPointsAtRadius (radius) {
  386. if (pointsAtRadius[radius]) {
  387. return pointsAtRadius[radius]
  388. }
  389. // Look for these number of points on each radius
  390. var T = radius * 8
  391. // Getting all the points at this radius
  392. var t = T
  393. var points = []
  394. if (radius === 0) {
  395. points.push([center[0], center[1], 0])
  396. }
  397. while (t--) {
  398. // distort the radius to put the cloud in shape
  399. var rx = 1
  400. if (settings.shape !== 'circle') {
  401. rx = settings.shape(t / T * 2 * Math.PI) // 0 to 1
  402. }
  403. // Push [x, y, t] t is used solely for getTextColor()
  404. points.push([
  405. center[0] + radius * rx * Math.cos(-t / T * 2 * Math.PI),
  406. center[1] + radius * rx * Math.sin(-t / T * 2 * Math.PI) *
  407. settings.ellipticity,
  408. t / T * 2 * Math.PI])
  409. }
  410. pointsAtRadius[radius] = points
  411. return points
  412. }
  413. /* Return true if we had spent too much time */
  414. var exceedTime = function exceedTime () {
  415. return ((settings.abortThreshold > 0) &&
  416. ((new Date()).getTime() - escapeTime > settings.abortThreshold))
  417. }
  418. /* Get the deg of rotation according to settings, and luck. */
  419. var getRotateDeg = function getRotateDeg () {
  420. if (settings.rotateRatio === 0) {
  421. return 0
  422. }
  423. if (Math.random() > settings.rotateRatio) {
  424. return 0
  425. }
  426. if (rotationRange === 0) {
  427. return minRotation
  428. }
  429. if (rotationSteps > 0) {
  430. // Min rotation + zero or more steps * span of one step
  431. return minRotation +
  432. Math.floor(Math.random() * rotationSteps) *
  433. rotationRange / (rotationSteps - 1)
  434. } else {
  435. return minRotation + Math.random() * rotationRange
  436. }
  437. }
  438. var getTextInfo = function getTextInfo (word, weight, rotateDeg, extraDataArray) {
  439. // calculate the acutal font size
  440. // fontSize === 0 means weightFactor function wants the text skipped,
  441. // and size < minSize means we cannot draw the text.
  442. var debug = false
  443. var fontSize = settings.weightFactor(weight)
  444. if (fontSize <= settings.minSize) {
  445. return false
  446. }
  447. // Scale factor here is to make sure fillText is not limited by
  448. // the minium font size set by browser.
  449. // It will always be 1 or 2n.
  450. var mu = 1
  451. if (fontSize < minFontSize) {
  452. mu = (function calculateScaleFactor () {
  453. var mu = 2
  454. while (mu * fontSize < minFontSize) {
  455. mu += 2
  456. }
  457. return mu
  458. })()
  459. }
  460. // Get fontWeight that will be used to set fctx.font
  461. var fontWeight
  462. if (getTextFontWeight) {
  463. fontWeight = getTextFontWeight(word, weight, fontSize, extraDataArray)
  464. } else {
  465. fontWeight = settings.fontWeight
  466. }
  467. var fcanvas = document.createElement('canvas')
  468. var fctx = fcanvas.getContext('2d', { willReadFrequently: true })
  469. fctx.font = fontWeight + ' ' +
  470. (fontSize * mu).toString(10) + 'px ' + settings.fontFamily
  471. // Estimate the dimension of the text with measureText().
  472. var fw = fctx.measureText(word).width / mu
  473. var fh = Math.max(fontSize * mu,
  474. fctx.measureText('m').width,
  475. fctx.measureText('\uFF37').width
  476. ) / mu
  477. // Create a boundary box that is larger than our estimates,
  478. // so text don't get cut of (it sill might)
  479. var boxWidth = fw + fh * 2
  480. var boxHeight = fh * 3
  481. var fgw = Math.ceil(boxWidth / g)
  482. var fgh = Math.ceil(boxHeight / g)
  483. boxWidth = fgw * g
  484. boxHeight = fgh * g
  485. // Calculate the proper offsets to make the text centered at
  486. // the preferred position.
  487. // This is simply half of the width.
  488. var fillTextOffsetX = -fw / 2
  489. // Instead of moving the box to the exact middle of the preferred
  490. // position, for Y-offset we move 0.4 instead, so Latin alphabets look
  491. // vertical centered.
  492. var fillTextOffsetY = -fh * 0.4
  493. // Calculate the actual dimension of the canvas, considering the rotation.
  494. var cgh = Math.ceil((boxWidth * Math.abs(Math.sin(rotateDeg)) +
  495. boxHeight * Math.abs(Math.cos(rotateDeg))) / g)
  496. var cgw = Math.ceil((boxWidth * Math.abs(Math.cos(rotateDeg)) +
  497. boxHeight * Math.abs(Math.sin(rotateDeg))) / g)
  498. var width = cgw * g
  499. var height = cgh * g
  500. fcanvas.setAttribute('width', width)
  501. fcanvas.setAttribute('height', height)
  502. if (debug) {
  503. // Attach fcanvas to the DOM
  504. document.body.appendChild(fcanvas)
  505. // Save it's state so that we could restore and draw the grid correctly.
  506. fctx.save()
  507. }
  508. // Scale the canvas with |mu|.
  509. fctx.scale(1 / mu, 1 / mu)
  510. fctx.translate(width * mu / 2, height * mu / 2)
  511. fctx.rotate(-rotateDeg)
  512. // Once the width/height is set, ctx info will be reset.
  513. // Set it again here.
  514. fctx.font = fontWeight + ' ' +
  515. (fontSize * mu).toString(10) + 'px ' + settings.fontFamily
  516. // Fill the text into the fcanvas.
  517. // XXX: We cannot because textBaseline = 'top' here because
  518. // Firefox and Chrome uses different default line-height for canvas.
  519. // Please read https://bugzil.la/737852#c6.
  520. // Here, we use textBaseline = 'middle' and draw the text at exactly
  521. // 0.5 * fontSize lower.
  522. fctx.fillStyle = '#000'
  523. fctx.textBaseline = 'middle'
  524. fctx.fillText(
  525. word, fillTextOffsetX * mu,
  526. (fillTextOffsetY + fontSize * 0.5) * mu
  527. )
  528. // Get the pixels of the text
  529. var imageData = fctx.getImageData(0, 0, width, height).data
  530. if (exceedTime()) {
  531. return false
  532. }
  533. if (debug) {
  534. // Draw the box of the original estimation
  535. fctx.strokeRect(
  536. fillTextOffsetX * mu,
  537. fillTextOffsetY, fw * mu, fh * mu
  538. )
  539. fctx.restore()
  540. }
  541. // Read the pixels and save the information to the occupied array
  542. var occupied = []
  543. var gx = cgw
  544. var gy, x, y
  545. var bounds = [cgh / 2, cgw / 2, cgh / 2, cgw / 2]
  546. while (gx--) {
  547. gy = cgh
  548. while (gy--) {
  549. y = g
  550. /* eslint no-labels: ["error", { "allowLoop": true }] */
  551. singleGridLoop: while (y--) {
  552. x = g
  553. while (x--) {
  554. if (imageData[((gy * g + y) * width +
  555. (gx * g + x)) * 4 + 3]) {
  556. occupied.push([gx, gy])
  557. if (gx < bounds[3]) {
  558. bounds[3] = gx
  559. }
  560. if (gx > bounds[1]) {
  561. bounds[1] = gx
  562. }
  563. if (gy < bounds[0]) {
  564. bounds[0] = gy
  565. }
  566. if (gy > bounds[2]) {
  567. bounds[2] = gy
  568. }
  569. if (debug) {
  570. fctx.fillStyle = 'rgba(255, 0, 0, 0.5)'
  571. fctx.fillRect(gx * g, gy * g, g - 0.5, g - 0.5)
  572. }
  573. break singleGridLoop
  574. }
  575. }
  576. }
  577. if (debug) {
  578. fctx.fillStyle = 'rgba(0, 0, 255, 0.5)'
  579. fctx.fillRect(gx * g, gy * g, g - 0.5, g - 0.5)
  580. }
  581. }
  582. }
  583. if (debug) {
  584. fctx.fillStyle = 'rgba(0, 255, 0, 0.5)'
  585. fctx.fillRect(
  586. bounds[3] * g,
  587. bounds[0] * g,
  588. (bounds[1] - bounds[3] + 1) * g,
  589. (bounds[2] - bounds[0] + 1) * g
  590. )
  591. }
  592. // Return information needed to create the text on the real canvas
  593. return {
  594. mu: mu,
  595. occupied: occupied,
  596. bounds: bounds,
  597. gw: cgw,
  598. gh: cgh,
  599. fillTextOffsetX: fillTextOffsetX,
  600. fillTextOffsetY: fillTextOffsetY,
  601. fillTextWidth: fw,
  602. fillTextHeight: fh,
  603. fontSize: fontSize
  604. }
  605. }
  606. /* Determine if there is room available in the given dimension */
  607. var canFitText = function canFitText (gx, gy, gw, gh, occupied) {
  608. // Go through the occupied points,
  609. // return false if the space is not available.
  610. var i = occupied.length
  611. while (i--) {
  612. var px = gx + occupied[i][0]
  613. var py = gy + occupied[i][1]
  614. if (px >= ngx || py >= ngy || px < 0 || py < 0) {
  615. if (!settings.drawOutOfBound) {
  616. return false
  617. }
  618. continue
  619. }
  620. if (!grid[px][py]) {
  621. return false
  622. }
  623. }
  624. return true
  625. }
  626. /* Actually draw the text on the grid */
  627. var drawText = function drawText (gx, gy, info, word, weight, distance, theta, rotateDeg, attributes, extraDataArray) {
  628. var fontSize = info.fontSize
  629. var color
  630. if (getTextColor) {
  631. color = getTextColor(word, weight, fontSize, distance, theta, extraDataArray)
  632. } else {
  633. color = settings.color
  634. }
  635. // get fontWeight that will be used to set ctx.font and font style rule
  636. var fontWeight
  637. if (getTextFontWeight) {
  638. fontWeight = getTextFontWeight(word, weight, fontSize, extraDataArray)
  639. } else {
  640. fontWeight = settings.fontWeight
  641. }
  642. var classes
  643. if (getTextClasses) {
  644. classes = getTextClasses(word, weight, fontSize, extraDataArray)
  645. } else {
  646. classes = settings.classes
  647. }
  648. elements.forEach(function (el) {
  649. if (el.getContext) {
  650. var ctx = el.getContext('2d')
  651. var mu = info.mu
  652. // Save the current state before messing it
  653. ctx.save()
  654. ctx.scale(1 / mu, 1 / mu)
  655. ctx.font = fontWeight + ' ' +
  656. (fontSize * mu).toString(10) + 'px ' + settings.fontFamily
  657. ctx.fillStyle = color
  658. // Translate the canvas position to the origin coordinate of where
  659. // the text should be put.
  660. ctx.translate(
  661. (gx + info.gw / 2) * g * mu,
  662. (gy + info.gh / 2) * g * mu
  663. )
  664. if (rotateDeg !== 0) {
  665. ctx.rotate(-rotateDeg)
  666. }
  667. // Finally, fill the text.
  668. // XXX: We cannot because textBaseline = 'top' here because
  669. // Firefox and Chrome uses different default line-height for canvas.
  670. // Please read https://bugzil.la/737852#c6.
  671. // Here, we use textBaseline = 'middle' and draw the text at exactly
  672. // 0.5 * fontSize lower.
  673. ctx.textBaseline = 'middle'
  674. ctx.fillText(
  675. word, info.fillTextOffsetX * mu,
  676. (info.fillTextOffsetY + fontSize * 0.5) * mu
  677. )
  678. // The below box is always matches how <span>s are positioned
  679. /* ctx.strokeRect(info.fillTextOffsetX, info.fillTextOffsetY,
  680. info.fillTextWidth, info.fillTextHeight) */
  681. // Restore the state.
  682. ctx.restore()
  683. } else {
  684. // drawText on DIV element
  685. var span = document.createElement('span')
  686. var transformRule = ''
  687. transformRule = 'rotate(' + (-rotateDeg / Math.PI * 180) + 'deg) '
  688. if (info.mu !== 1) {
  689. transformRule +=
  690. 'translateX(-' + (info.fillTextWidth / 4) + 'px) ' +
  691. 'scale(' + (1 / info.mu) + ')'
  692. }
  693. var styleRules = {
  694. position: 'absolute',
  695. display: 'block',
  696. font: fontWeight + ' ' +
  697. (fontSize * info.mu) + 'px ' + settings.fontFamily,
  698. left: ((gx + info.gw / 2) * g + info.fillTextOffsetX) + 'px',
  699. top: ((gy + info.gh / 2) * g + info.fillTextOffsetY) + 'px',
  700. width: info.fillTextWidth + 'px',
  701. height: info.fillTextHeight + 'px',
  702. lineHeight: fontSize + 'px',
  703. whiteSpace: 'nowrap',
  704. transform: transformRule,
  705. webkitTransform: transformRule,
  706. msTransform: transformRule,
  707. transformOrigin: '50% 40%',
  708. webkitTransformOrigin: '50% 40%',
  709. msTransformOrigin: '50% 40%'
  710. }
  711. if (color) {
  712. styleRules.color = color
  713. }
  714. span.textContent = word
  715. for (var cssProp in styleRules) {
  716. span.style[cssProp] = styleRules[cssProp]
  717. }
  718. if (attributes) {
  719. for (var attribute in attributes) {
  720. span.setAttribute(attribute, attributes[attribute])
  721. }
  722. }
  723. if (classes) {
  724. span.className += classes
  725. }
  726. el.appendChild(span)
  727. }
  728. })
  729. }
  730. /* Help function to updateGrid */
  731. var fillGridAt = function fillGridAt (x, y, drawMask, dimension, item) {
  732. if (x >= ngx || y >= ngy || x < 0 || y < 0) {
  733. return
  734. }
  735. grid[x][y] = false
  736. if (drawMask) {
  737. var ctx = elements[0].getContext('2d')
  738. ctx.fillRect(x * g, y * g, maskRectWidth, maskRectWidth)
  739. }
  740. if (interactive) {
  741. infoGrid[x][y] = { item: item, dimension: dimension }
  742. }
  743. }
  744. /* Update the filling information of the given space with occupied points.
  745. Draw the mask on the canvas if necessary. */
  746. var updateGrid = function updateGrid (gx, gy, gw, gh, info, item) {
  747. var occupied = info.occupied
  748. var drawMask = settings.drawMask
  749. var ctx
  750. if (drawMask) {
  751. ctx = elements[0].getContext('2d')
  752. ctx.save()
  753. ctx.fillStyle = settings.maskColor
  754. }
  755. var dimension
  756. if (interactive) {
  757. var bounds = info.bounds
  758. dimension = {
  759. x: (gx + bounds[3]) * g,
  760. y: (gy + bounds[0]) * g,
  761. w: (bounds[1] - bounds[3] + 1) * g,
  762. h: (bounds[2] - bounds[0] + 1) * g
  763. }
  764. }
  765. var i = occupied.length
  766. while (i--) {
  767. var px = gx + occupied[i][0]
  768. var py = gy + occupied[i][1]
  769. if (px >= ngx || py >= ngy || px < 0 || py < 0) {
  770. continue
  771. }
  772. fillGridAt(px, py, drawMask, dimension, item)
  773. }
  774. if (drawMask) {
  775. ctx.restore()
  776. }
  777. }
  778. /* putWord() processes each item on the list,
  779. calculate it's size and determine it's position, and actually
  780. put it on the canvas. */
  781. var putWord = function putWord (item) {
  782. var word, weight, attributes
  783. if (Array.isArray(item)) {
  784. word = item[0]
  785. weight = item[1]
  786. } else {
  787. word = item.word
  788. weight = item.weight
  789. attributes = item.attributes
  790. }
  791. var rotateDeg = getRotateDeg()
  792. var extraDataArray = getItemExtraData(item)
  793. // get info needed to put the text onto the canvas
  794. var info = getTextInfo(word, weight, rotateDeg, extraDataArray)
  795. // not getting the info means we shouldn't be drawing this one.
  796. if (!info) {
  797. return false
  798. }
  799. if (exceedTime()) {
  800. return false
  801. }
  802. // If drawOutOfBound is set to false,
  803. // skip the loop if we have already know the bounding box of
  804. // word is larger than the canvas.
  805. if (!settings.drawOutOfBound && !settings.shrinkToFit) {
  806. var bounds = info.bounds;
  807. if ((bounds[1] - bounds[3] + 1) > ngx ||
  808. (bounds[2] - bounds[0] + 1) > ngy) {
  809. return false
  810. }
  811. }
  812. // Determine the position to put the text by
  813. // start looking for the nearest points
  814. var r = maxRadius + 1
  815. var tryToPutWordAtPoint = function (gxy) {
  816. var gx = Math.floor(gxy[0] - info.gw / 2)
  817. var gy = Math.floor(gxy[1] - info.gh / 2)
  818. var gw = info.gw
  819. var gh = info.gh
  820. // If we cannot fit the text at this position, return false
  821. // and go to the next position.
  822. if (!canFitText(gx, gy, gw, gh, info.occupied)) {
  823. return false
  824. }
  825. // Actually put the text on the canvas
  826. drawText(gx, gy, info, word, weight,
  827. (maxRadius - r), gxy[2], rotateDeg, attributes, extraDataArray)
  828. // Mark the spaces on the grid as filled
  829. updateGrid(gx, gy, gw, gh, info, item)
  830. // Return true so some() will stop and also return true.
  831. return true
  832. }
  833. while (r--) {
  834. var points = getPointsAtRadius(maxRadius - r)
  835. if (settings.shuffle) {
  836. points = [].concat(points)
  837. shuffleArray(points)
  838. }
  839. // Try to fit the words by looking at each point.
  840. // array.some() will stop and return true
  841. // when putWordAtPoint() returns true.
  842. // If all the points returns false, array.some() returns false.
  843. var drawn = points.some(tryToPutWordAtPoint)
  844. if (drawn) {
  845. // leave putWord() and return true
  846. return true
  847. }
  848. }
  849. if (settings.shrinkToFit) {
  850. if (Array.isArray(item)) {
  851. item[1] = item[1] * 3 / 4
  852. } else {
  853. item.weight = item.weight * 3 / 4
  854. }
  855. return putWord(item)
  856. }
  857. // we tried all distances but text won't fit, return false
  858. return false
  859. }
  860. /* Send DOM event to all elements. Will stop sending event and return
  861. if the previous one is canceled (for cancelable events). */
  862. var sendEvent = function sendEvent (type, cancelable, details) {
  863. if (cancelable) {
  864. return !elements.some(function (el) {
  865. var event = new CustomEvent(type, {
  866. detail: details || {}
  867. })
  868. return !el.dispatchEvent(event)
  869. }, this)
  870. } else {
  871. elements.forEach(function (el) {
  872. var event = new CustomEvent(type, {
  873. detail: details || {}
  874. })
  875. el.dispatchEvent(event)
  876. }, this)
  877. }
  878. }
  879. /* Start drawing on a canvas */
  880. var start = function start () {
  881. // For dimensions, clearCanvas etc.,
  882. // we only care about the first element.
  883. var canvas = elements[0]
  884. if (canvas.getContext) {
  885. ngx = Math.ceil(canvas.width / g)
  886. ngy = Math.ceil(canvas.height / g)
  887. } else {
  888. var rect = canvas.getBoundingClientRect()
  889. ngx = Math.ceil(rect.width / g)
  890. ngy = Math.ceil(rect.height / g)
  891. }
  892. // Sending a wordcloudstart event which cause the previous loop to stop.
  893. // Do nothing if the event is canceled.
  894. if (!sendEvent('wordcloudstart', true)) {
  895. return
  896. }
  897. // Determine the center of the word cloud
  898. center = (settings.origin)
  899. ? [settings.origin[0] / g, settings.origin[1] / g]
  900. : [ngx / 2, ngy / 2]
  901. // Maxium radius to look for space
  902. maxRadius = Math.floor(Math.sqrt(ngx * ngx + ngy * ngy))
  903. /* Clear the canvas only if the clearCanvas is set,
  904. if not, update the grid to the current canvas state */
  905. grid = []
  906. var gx, gy, i
  907. if (!canvas.getContext || settings.clearCanvas) {
  908. elements.forEach(function (el) {
  909. if (el.getContext) {
  910. var ctx = el.getContext('2d')
  911. ctx.fillStyle = settings.backgroundColor
  912. ctx.clearRect(0, 0, ngx * (g + 1), ngy * (g + 1))
  913. ctx.fillRect(0, 0, ngx * (g + 1), ngy * (g + 1))
  914. } else {
  915. el.textContent = ''
  916. el.style.backgroundColor = settings.backgroundColor
  917. el.style.position = 'relative'
  918. }
  919. })
  920. /* fill the grid with empty state */
  921. gx = ngx
  922. while (gx--) {
  923. grid[gx] = []
  924. gy = ngy
  925. while (gy--) {
  926. grid[gx][gy] = true
  927. }
  928. }
  929. } else {
  930. /* Determine bgPixel by creating
  931. another canvas and fill the specified background color. */
  932. var bctx = document.createElement('canvas').getContext('2d')
  933. bctx.fillStyle = settings.backgroundColor
  934. bctx.fillRect(0, 0, 1, 1)
  935. var bgPixel = bctx.getImageData(0, 0, 1, 1).data
  936. /* Read back the pixels of the canvas we got to tell which part of the
  937. canvas is empty.
  938. (no clearCanvas only works with a canvas, not divs) */
  939. var imageData =
  940. canvas.getContext('2d').getImageData(0, 0, ngx * g, ngy * g).data
  941. gx = ngx
  942. var x, y
  943. while (gx--) {
  944. grid[gx] = []
  945. gy = ngy
  946. while (gy--) {
  947. y = g
  948. /* eslint no-labels: ["error", { "allowLoop": true }] */
  949. singleGridLoop: while (y--) {
  950. x = g
  951. while (x--) {
  952. i = 4
  953. while (i--) {
  954. if (imageData[((gy * g + y) * ngx * g +
  955. (gx * g + x)) * 4 + i] !== bgPixel[i]) {
  956. grid[gx][gy] = false
  957. break singleGridLoop
  958. }
  959. }
  960. }
  961. }
  962. if (grid[gx][gy] !== false) {
  963. grid[gx][gy] = true
  964. }
  965. }
  966. }
  967. imageData = bctx = bgPixel = undefined
  968. }
  969. // fill the infoGrid with empty state if we need it
  970. if (settings.hover || settings.click) {
  971. interactive = true
  972. /* fill the grid with empty state */
  973. gx = ngx + 1
  974. while (gx--) {
  975. infoGrid[gx] = []
  976. }
  977. if (settings.hover) {
  978. canvas.addEventListener('mousemove', wordcloudhover)
  979. }
  980. if (settings.click) {
  981. canvas.addEventListener('click', wordcloudclick)
  982. canvas.style.webkitTapHighlightColor = 'rgba(0, 0, 0, 0)'
  983. }
  984. canvas.addEventListener('wordcloudstart', function stopInteraction () {
  985. canvas.removeEventListener('wordcloudstart', stopInteraction)
  986. canvas.removeEventListener('mousemove', wordcloudhover)
  987. canvas.removeEventListener('click', wordcloudclick)
  988. hovered = undefined
  989. })
  990. }
  991. i = 0
  992. var loopingFunction, stoppingFunction
  993. if (settings.wait !== 0) {
  994. loopingFunction = window.setTimeout
  995. stoppingFunction = window.clearTimeout
  996. } else {
  997. loopingFunction = window.setImmediate
  998. stoppingFunction = window.clearImmediate
  999. }
  1000. var addEventListener = function addEventListener (type, listener) {
  1001. elements.forEach(function (el) {
  1002. el.addEventListener(type, listener)
  1003. }, this)
  1004. }
  1005. var removeEventListener = function removeEventListener (type, listener) {
  1006. elements.forEach(function (el) {
  1007. el.removeEventListener(type, listener)
  1008. }, this)
  1009. }
  1010. var anotherWordCloudStart = function anotherWordCloudStart () {
  1011. removeEventListener('wordcloudstart', anotherWordCloudStart)
  1012. stoppingFunction(timer[timerId])
  1013. }
  1014. addEventListener('wordcloudstart', anotherWordCloudStart)
  1015. timer[timerId] = loopingFunction(function loop () {
  1016. if (i >= settings.list.length) {
  1017. stoppingFunction(timer[timerId])
  1018. sendEvent('wordcloudstop', false)
  1019. removeEventListener('wordcloudstart', anotherWordCloudStart)
  1020. delete timer[timerId];
  1021. return
  1022. }
  1023. escapeTime = (new Date()).getTime()
  1024. var drawn = putWord(settings.list[i])
  1025. var canceled = !sendEvent('wordclouddrawn', true, {
  1026. item: settings.list[i],
  1027. drawn: drawn
  1028. })
  1029. if (exceedTime() || canceled) {
  1030. stoppingFunction(timer[timerId])
  1031. settings.abort()
  1032. sendEvent('wordcloudabort', false)
  1033. sendEvent('wordcloudstop', false)
  1034. removeEventListener('wordcloudstart', anotherWordCloudStart)
  1035. delete timer[timerId]
  1036. return
  1037. }
  1038. i++
  1039. timer[timerId] = loopingFunction(loop, settings.wait)
  1040. }, settings.wait)
  1041. }
  1042. // All set, start the drawing
  1043. start()
  1044. }
  1045. WordCloud.isSupported = isSupported
  1046. WordCloud.minFontSize = minFontSize
  1047. WordCloud.stop = function stop () {
  1048. if (timer) {
  1049. for (var timerId in timer) {
  1050. window.clearImmediate(timer[timerId])
  1051. }
  1052. }
  1053. }
  1054. // Expose the library as an AMD module
  1055. if (typeof define === 'function' && define.amd) { // eslint-disable-line no-undef
  1056. global.WordCloud = WordCloud
  1057. define('wordcloud', [], function () { return WordCloud }) // eslint-disable-line no-undef
  1058. } else if (typeof module !== 'undefined' && module.exports) { // eslint-disable-line no-undef
  1059. module.exports = WordCloud // eslint-disable-line no-undef
  1060. } else {
  1061. global.WordCloud = WordCloud
  1062. }
  1063. })(this) // jshint ignore:line