From c2da25762d6d9ac2263a1b8780de205965eecb69 Mon Sep 17 00:00:00 2001 From: Denys Konovalov Date: Thu, 1 Dec 2022 20:55:09 +0100 Subject: [PATCH] chronik first try --- layouts/partials/head.html | 6 + layouts/schulchronik/single.html | 4 + static/plugins/wordcloud/wordcloud2.js | 1241 ++++++++++++++++++++++++ 3 files changed, 1251 insertions(+) create mode 100644 static/plugins/wordcloud/wordcloud2.js diff --git a/layouts/partials/head.html b/layouts/partials/head.html index c00105de..a85527db 100644 --- a/layouts/partials/head.html +++ b/layouts/partials/head.html @@ -35,7 +35,13 @@ +<<<<<<< HEAD {{ template "_internal/opengraph.html" . }} {{ template "_internal/twitter_cards.html" . }} +======= + + + +>>>>>>> 789501f (chronik first try) diff --git a/layouts/schulchronik/single.html b/layouts/schulchronik/single.html index 455195cc..15d56f58 100644 --- a/layouts/schulchronik/single.html +++ b/layouts/schulchronik/single.html @@ -4,6 +4,10 @@
+
+ {{ .Content }}
{{ if .Params.Cantorpreisträger }} diff --git a/static/plugins/wordcloud/wordcloud2.js b/static/plugins/wordcloud/wordcloud2.js new file mode 100644 index 00000000..c4a607bd --- /dev/null +++ b/static/plugins/wordcloud/wordcloud2.js @@ -0,0 +1,1241 @@ +/*! + * wordcloud2.js + * http://timdream.org/wordcloud2.js/ + * + * Copyright 2011 - 2019 Tim Guan-tin Chien and contributors. + * Released under the MIT license + */ + +'use strict' + +// setImmediate +if (!window.setImmediate) { + window.setImmediate = (function setupSetImmediate () { + return window.msSetImmediate || + window.webkitSetImmediate || + window.mozSetImmediate || + window.oSetImmediate || + (function setupSetZeroTimeout () { + if (!window.postMessage || !window.addEventListener) { + return null + } + + var callbacks = [undefined] + var message = 'zero-timeout-message' + + // Like setTimeout, but only takes a function argument. There's + // no time argument (always zero) and no arguments (you have to + // use a closure). + var setZeroTimeout = function setZeroTimeout (callback) { + var id = callbacks.length + callbacks.push(callback) + window.postMessage(message + id.toString(36), '*') + + return id + } + + window.addEventListener('message', function setZeroTimeoutMessage (evt) { + // Skipping checking event source, retarded IE confused this window + // object with another in the presence of iframe + if (typeof evt.data !== 'string' || + evt.data.substr(0, message.length) !== message/* || + evt.source !== window */) { + return + } + + evt.stopImmediatePropagation() + + var id = parseInt(evt.data.substr(message.length), 36) + if (!callbacks[id]) { + return + } + + callbacks[id]() + callbacks[id] = undefined + }, true) + + /* specify clearImmediate() here since we need the scope */ + window.clearImmediate = function clearZeroTimeout (id) { + if (!callbacks[id]) { + return + } + + callbacks[id] = undefined + } + + return setZeroTimeout + })() || + // fallback + function setImmediateFallback (fn) { + window.setTimeout(fn, 0) + } + })() +} + +if (!window.clearImmediate) { + window.clearImmediate = (function setupClearImmediate () { + return window.msClearImmediate || + window.webkitClearImmediate || + window.mozClearImmediate || + window.oClearImmediate || + // "clearZeroTimeout" is implement on the previous block || + // fallback + function clearImmediateFallback (timer) { + window.clearTimeout(timer) + } + })() +} + +(function (global) { + // Check if WordCloud can run on this browser + var isSupported = (function isSupported () { + var canvas = document.createElement('canvas') + if (!canvas || !canvas.getContext) { + return false + } + + var ctx = canvas.getContext('2d') + if (!ctx) { + return false + } + if (!ctx.getImageData) { + return false + } + if (!ctx.fillText) { + return false + } + + if (!Array.prototype.some) { + return false + } + if (!Array.prototype.push) { + return false + } + + return true + }()) + + // Find out if the browser impose minium font size by + // drawing small texts on a canvas and measure it's width. + var minFontSize = (function getMinFontSize () { + if (!isSupported) { + return + } + + var ctx = document.createElement('canvas').getContext('2d') + + // start from 20 + var size = 20 + + // two sizes to measure + var hanWidth, mWidth + + while (size) { + ctx.font = size.toString(10) + 'px sans-serif' + if ((ctx.measureText('\uFF37').width === hanWidth) && + (ctx.measureText('m').width) === mWidth) { + return (size + 1) + } + + hanWidth = ctx.measureText('\uFF37').width + mWidth = ctx.measureText('m').width + + size-- + } + + return 0 + })() + + var getItemExtraData = function (item) { + if (Array.isArray(item)) { + var itemCopy = item.slice() + // remove data we already have (word and weight) + itemCopy.splice(0, 2) + return itemCopy + } else { + return [] + } + } + + // Based on http://jsfromhell.com/array/shuffle + var shuffleArray = function shuffleArray (arr) { + for (var j, x, i = arr.length; i;) { + j = Math.floor(Math.random() * i) + x = arr[--i] + arr[i] = arr[j] + arr[j] = x + } + return arr + } + + var timer = {}; + var WordCloud = function WordCloud (elements, options) { + if (!isSupported) { + return + } + + var timerId = Math.floor(Math.random() * Date.now()) + + if (!Array.isArray(elements)) { + elements = [elements] + } + + elements.forEach(function (el, i) { + if (typeof el === 'string') { + elements[i] = document.getElementById(el) + if (!elements[i]) { + throw new Error('The element id specified is not found.') + } + } else if (!el.tagName && !el.appendChild) { + throw new Error('You must pass valid HTML elements, or ID of the element.') + } + }) + + /* Default values to be overwritten by options object */ + var settings = { + list: [], + fontFamily: '"Trebuchet MS", "Heiti TC", "微軟正黑體", ' + + '"Arial Unicode MS", "Droid Fallback Sans", sans-serif', + fontWeight: 'normal', + color: 'random-dark', + minSize: 0, // 0 to disable + weightFactor: 1, + clearCanvas: true, + backgroundColor: '#fff', // opaque white = rgba(255, 255, 255, 1) + + gridSize: 8, + drawOutOfBound: false, + shrinkToFit: false, + origin: null, + + drawMask: false, + maskColor: 'rgba(255,0,0,0.3)', + maskGapWidth: 0.3, + + wait: 0, + abortThreshold: 0, // disabled + abort: function noop () {}, + + minRotation: -Math.PI / 2, + maxRotation: Math.PI / 2, + rotationSteps: 0, + + shuffle: true, + rotateRatio: 0.1, + + shape: 'circle', + ellipticity: 0.65, + + classes: null, + + hover: null, + click: null + } + + if (options) { + for (var key in options) { + if (key in settings) { + settings[key] = options[key] + } + } + } + + /* Convert weightFactor into a function */ + if (typeof settings.weightFactor !== 'function') { + var factor = settings.weightFactor + settings.weightFactor = function weightFactor (pt) { + return pt * factor // in px + } + } + + /* Convert shape into a function */ + if (typeof settings.shape !== 'function') { + switch (settings.shape) { + case 'circle': + /* falls through */ + default: + // 'circle' is the default and a shortcut in the code loop. + settings.shape = 'circle' + break + + case 'cardioid': + settings.shape = function shapeCardioid (theta) { + return 1 - Math.sin(theta) + } + break + + /* + To work out an X-gon, one has to calculate "m", + where 1/(cos(2*PI/X)+m*sin(2*PI/X)) = 1/(cos(0)+m*sin(0)) + http://www.wolframalpha.com/input/?i=1%2F%28cos%282*PI%2FX%29%2Bm*sin%28 + 2*PI%2FX%29%29+%3D+1%2F%28cos%280%29%2Bm*sin%280%29%29 + Copy the solution into polar equation r = 1/(cos(t') + m*sin(t')) + where t' equals to mod(t, 2PI/X) + */ + + case 'diamond': + // http://www.wolframalpha.com/input/?i=plot+r+%3D+1%2F%28cos%28mod+ + // %28t%2C+PI%2F2%29%29%2Bsin%28mod+%28t%2C+PI%2F2%29%29%29%2C+t+%3D + // +0+..+2*PI + settings.shape = function shapeSquare (theta) { + var thetaPrime = theta % (2 * Math.PI / 4) + return 1 / (Math.cos(thetaPrime) + Math.sin(thetaPrime)) + } + break + + case 'square': + // http://www.wolframalpha.com/input/?i=plot+r+%3D+min(1%2Fabs(cos(t + // )),1%2Fabs(sin(t)))),+t+%3D+0+..+2*PI + settings.shape = function shapeSquare (theta) { + return Math.min( + 1 / Math.abs(Math.cos(theta)), + 1 / Math.abs(Math.sin(theta)) + ) + } + break + + case 'triangle-forward': + // http://www.wolframalpha.com/input/?i=plot+r+%3D+1%2F%28cos%28mod+ + // %28t%2C+2*PI%2F3%29%29%2Bsqrt%283%29sin%28mod+%28t%2C+2*PI%2F3%29 + // %29%29%2C+t+%3D+0+..+2*PI + settings.shape = function shapeTriangle (theta) { + var thetaPrime = theta % (2 * Math.PI / 3) + return 1 / (Math.cos(thetaPrime) + + Math.sqrt(3) * Math.sin(thetaPrime)) + } + break + + case 'triangle': + case 'triangle-upright': + settings.shape = function shapeTriangle (theta) { + var thetaPrime = (theta + Math.PI * 3 / 2) % (2 * Math.PI / 3) + return 1 / (Math.cos(thetaPrime) + + Math.sqrt(3) * Math.sin(thetaPrime)) + } + break + + case 'pentagon': + settings.shape = function shapePentagon (theta) { + var thetaPrime = (theta + 0.955) % (2 * Math.PI / 5) + return 1 / (Math.cos(thetaPrime) + + 0.726543 * Math.sin(thetaPrime)) + } + break + + case 'star': + settings.shape = function shapeStar (theta) { + var thetaPrime = (theta + 0.955) % (2 * Math.PI / 10) + if ((theta + 0.955) % (2 * Math.PI / 5) - (2 * Math.PI / 10) >= 0) { + return 1 / (Math.cos((2 * Math.PI / 10) - thetaPrime) + + 3.07768 * Math.sin((2 * Math.PI / 10) - thetaPrime)) + } else { + return 1 / (Math.cos(thetaPrime) + + 3.07768 * Math.sin(thetaPrime)) + } + } + break + } + } + + /* Make sure gridSize is a whole number and is not smaller than 4px */ + settings.gridSize = Math.max(Math.floor(settings.gridSize), 4) + + /* shorthand */ + var g = settings.gridSize + var maskRectWidth = g - settings.maskGapWidth + + /* normalize rotation settings */ + var rotationRange = Math.abs(settings.maxRotation - settings.minRotation) + var rotationSteps = Math.abs(Math.floor(settings.rotationSteps)) + var minRotation = Math.min(settings.maxRotation, settings.minRotation) + + /* information/object available to all functions, set when start() */ + var grid, // 2d array containing filling information + ngx, ngy, // width and height of the grid + center, // position of the center of the cloud + maxRadius + + /* timestamp for measuring each putWord() action */ + var escapeTime + + /* function for getting the color of the text */ + var getTextColor + function randomHslColor (min, max) { + return 'hsl(' + + (Math.random() * 360).toFixed() + ',' + + (Math.random() * 30 + 70).toFixed() + '%,' + + (Math.random() * (max - min) + min).toFixed() + '%)' + } + switch (settings.color) { + case 'random-dark': + getTextColor = function getRandomDarkColor () { + return randomHslColor(10, 50) + } + break + + case 'random-light': + getTextColor = function getRandomLightColor () { + return randomHslColor(50, 90) + } + break + + default: + if (typeof settings.color === 'function') { + getTextColor = settings.color + } + break + } + + /* function for getting the font-weight of the text */ + var getTextFontWeight + if (typeof settings.fontWeight === 'function') { + getTextFontWeight = settings.fontWeight + } + + /* function for getting the classes of the text */ + var getTextClasses = null + if (typeof settings.classes === 'function') { + getTextClasses = settings.classes + } + + /* Interactive */ + var interactive = false + var infoGrid = [] + var hovered + + var getInfoGridFromMouseTouchEvent = + function getInfoGridFromMouseTouchEvent (evt) { + var canvas = evt.currentTarget + var rect = canvas.getBoundingClientRect() + var clientX + var clientY + /** Detect if touches are available */ + if (evt.touches) { + clientX = evt.touches[0].clientX + clientY = evt.touches[0].clientY + } else { + clientX = evt.clientX + clientY = evt.clientY + } + var eventX = clientX - rect.left + var eventY = clientY - rect.top + + var x = Math.floor(eventX * ((canvas.width / rect.width) || 1) / g) + var y = Math.floor(eventY * ((canvas.height / rect.height) || 1) / g) + + if (!infoGrid[x]) { + return null + } + + return infoGrid[x][y] + } + + var wordcloudhover = function wordcloudhover (evt) { + var info = getInfoGridFromMouseTouchEvent(evt) + + if (hovered === info) { + return + } + + hovered = info + if (!info) { + settings.hover(undefined, undefined, evt) + + return + } + + settings.hover(info.item, info.dimension, evt) + } + + var wordcloudclick = function wordcloudclick (evt) { + var info = getInfoGridFromMouseTouchEvent(evt) + if (!info) { + return + } + + settings.click(info.item, info.dimension, evt) + evt.preventDefault() + } + + /* Get points on the grid for a given radius away from the center */ + var pointsAtRadius = [] + var getPointsAtRadius = function getPointsAtRadius (radius) { + if (pointsAtRadius[radius]) { + return pointsAtRadius[radius] + } + + // Look for these number of points on each radius + var T = radius * 8 + + // Getting all the points at this radius + var t = T + var points = [] + + if (radius === 0) { + points.push([center[0], center[1], 0]) + } + + while (t--) { + // distort the radius to put the cloud in shape + var rx = 1 + if (settings.shape !== 'circle') { + rx = settings.shape(t / T * 2 * Math.PI) // 0 to 1 + } + + // Push [x, y, t] t is used solely for getTextColor() + points.push([ + center[0] + radius * rx * Math.cos(-t / T * 2 * Math.PI), + center[1] + radius * rx * Math.sin(-t / T * 2 * Math.PI) * + settings.ellipticity, + t / T * 2 * Math.PI]) + } + + pointsAtRadius[radius] = points + return points + } + + /* Return true if we had spent too much time */ + var exceedTime = function exceedTime () { + return ((settings.abortThreshold > 0) && + ((new Date()).getTime() - escapeTime > settings.abortThreshold)) + } + + /* Get the deg of rotation according to settings, and luck. */ + var getRotateDeg = function getRotateDeg () { + if (settings.rotateRatio === 0) { + return 0 + } + + if (Math.random() > settings.rotateRatio) { + return 0 + } + + if (rotationRange === 0) { + return minRotation + } + + if (rotationSteps > 0) { + // Min rotation + zero or more steps * span of one step + return minRotation + + Math.floor(Math.random() * rotationSteps) * + rotationRange / (rotationSteps - 1) + } else { + return minRotation + Math.random() * rotationRange + } + } + + var getTextInfo = function getTextInfo (word, weight, rotateDeg, extraDataArray) { + // calculate the acutal font size + // fontSize === 0 means weightFactor function wants the text skipped, + // and size < minSize means we cannot draw the text. + var debug = false + var fontSize = settings.weightFactor(weight) + if (fontSize <= settings.minSize) { + return false + } + + // Scale factor here is to make sure fillText is not limited by + // the minium font size set by browser. + // It will always be 1 or 2n. + var mu = 1 + if (fontSize < minFontSize) { + mu = (function calculateScaleFactor () { + var mu = 2 + while (mu * fontSize < minFontSize) { + mu += 2 + } + return mu + })() + } + + // Get fontWeight that will be used to set fctx.font + var fontWeight + if (getTextFontWeight) { + fontWeight = getTextFontWeight(word, weight, fontSize, extraDataArray) + } else { + fontWeight = settings.fontWeight + } + + var fcanvas = document.createElement('canvas') + var fctx = fcanvas.getContext('2d', { willReadFrequently: true }) + + fctx.font = fontWeight + ' ' + + (fontSize * mu).toString(10) + 'px ' + settings.fontFamily + + // Estimate the dimension of the text with measureText(). + var fw = fctx.measureText(word).width / mu + var fh = Math.max(fontSize * mu, + fctx.measureText('m').width, + fctx.measureText('\uFF37').width + ) / mu + + // Create a boundary box that is larger than our estimates, + // so text don't get cut of (it sill might) + var boxWidth = fw + fh * 2 + var boxHeight = fh * 3 + var fgw = Math.ceil(boxWidth / g) + var fgh = Math.ceil(boxHeight / g) + boxWidth = fgw * g + boxHeight = fgh * g + + // Calculate the proper offsets to make the text centered at + // the preferred position. + + // This is simply half of the width. + var fillTextOffsetX = -fw / 2 + // Instead of moving the box to the exact middle of the preferred + // position, for Y-offset we move 0.4 instead, so Latin alphabets look + // vertical centered. + var fillTextOffsetY = -fh * 0.4 + + // Calculate the actual dimension of the canvas, considering the rotation. + var cgh = Math.ceil((boxWidth * Math.abs(Math.sin(rotateDeg)) + + boxHeight * Math.abs(Math.cos(rotateDeg))) / g) + var cgw = Math.ceil((boxWidth * Math.abs(Math.cos(rotateDeg)) + + boxHeight * Math.abs(Math.sin(rotateDeg))) / g) + var width = cgw * g + var height = cgh * g + + fcanvas.setAttribute('width', width) + fcanvas.setAttribute('height', height) + + if (debug) { + // Attach fcanvas to the DOM + document.body.appendChild(fcanvas) + // Save it's state so that we could restore and draw the grid correctly. + fctx.save() + } + + // Scale the canvas with |mu|. + fctx.scale(1 / mu, 1 / mu) + fctx.translate(width * mu / 2, height * mu / 2) + fctx.rotate(-rotateDeg) + + // Once the width/height is set, ctx info will be reset. + // Set it again here. + fctx.font = fontWeight + ' ' + + (fontSize * mu).toString(10) + 'px ' + settings.fontFamily + + // Fill the text into the fcanvas. + // XXX: We cannot because textBaseline = 'top' here because + // Firefox and Chrome uses different default line-height for canvas. + // Please read https://bugzil.la/737852#c6. + // Here, we use textBaseline = 'middle' and draw the text at exactly + // 0.5 * fontSize lower. + fctx.fillStyle = '#000' + fctx.textBaseline = 'middle' + fctx.fillText( + word, fillTextOffsetX * mu, + (fillTextOffsetY + fontSize * 0.5) * mu + ) + + // Get the pixels of the text + var imageData = fctx.getImageData(0, 0, width, height).data + + if (exceedTime()) { + return false + } + + if (debug) { + // Draw the box of the original estimation + fctx.strokeRect( + fillTextOffsetX * mu, + fillTextOffsetY, fw * mu, fh * mu + ) + fctx.restore() + } + + // Read the pixels and save the information to the occupied array + var occupied = [] + var gx = cgw + var gy, x, y + var bounds = [cgh / 2, cgw / 2, cgh / 2, cgw / 2] + while (gx--) { + gy = cgh + while (gy--) { + y = g + /* eslint no-labels: ["error", { "allowLoop": true }] */ + singleGridLoop: while (y--) { + x = g + while (x--) { + if (imageData[((gy * g + y) * width + + (gx * g + x)) * 4 + 3]) { + occupied.push([gx, gy]) + + if (gx < bounds[3]) { + bounds[3] = gx + } + if (gx > bounds[1]) { + bounds[1] = gx + } + if (gy < bounds[0]) { + bounds[0] = gy + } + if (gy > bounds[2]) { + bounds[2] = gy + } + + if (debug) { + fctx.fillStyle = 'rgba(255, 0, 0, 0.5)' + fctx.fillRect(gx * g, gy * g, g - 0.5, g - 0.5) + } + break singleGridLoop + } + } + } + if (debug) { + fctx.fillStyle = 'rgba(0, 0, 255, 0.5)' + fctx.fillRect(gx * g, gy * g, g - 0.5, g - 0.5) + } + } + } + + if (debug) { + fctx.fillStyle = 'rgba(0, 255, 0, 0.5)' + fctx.fillRect( + bounds[3] * g, + bounds[0] * g, + (bounds[1] - bounds[3] + 1) * g, + (bounds[2] - bounds[0] + 1) * g + ) + } + + // Return information needed to create the text on the real canvas + return { + mu: mu, + occupied: occupied, + bounds: bounds, + gw: cgw, + gh: cgh, + fillTextOffsetX: fillTextOffsetX, + fillTextOffsetY: fillTextOffsetY, + fillTextWidth: fw, + fillTextHeight: fh, + fontSize: fontSize + } + } + + /* Determine if there is room available in the given dimension */ + var canFitText = function canFitText (gx, gy, gw, gh, occupied) { + // Go through the occupied points, + // return false if the space is not available. + var i = occupied.length + while (i--) { + var px = gx + occupied[i][0] + var py = gy + occupied[i][1] + + if (px >= ngx || py >= ngy || px < 0 || py < 0) { + if (!settings.drawOutOfBound) { + return false + } + continue + } + + if (!grid[px][py]) { + return false + } + } + return true + } + + /* Actually draw the text on the grid */ + var drawText = function drawText (gx, gy, info, word, weight, distance, theta, rotateDeg, attributes, extraDataArray) { + var fontSize = info.fontSize + var color + if (getTextColor) { + color = getTextColor(word, weight, fontSize, distance, theta, extraDataArray) + } else { + color = settings.color + } + + // get fontWeight that will be used to set ctx.font and font style rule + var fontWeight + if (getTextFontWeight) { + fontWeight = getTextFontWeight(word, weight, fontSize, extraDataArray) + } else { + fontWeight = settings.fontWeight + } + + var classes + if (getTextClasses) { + classes = getTextClasses(word, weight, fontSize, extraDataArray) + } else { + classes = settings.classes + } + + elements.forEach(function (el) { + if (el.getContext) { + var ctx = el.getContext('2d') + var mu = info.mu + + // Save the current state before messing it + ctx.save() + ctx.scale(1 / mu, 1 / mu) + + ctx.font = fontWeight + ' ' + + (fontSize * mu).toString(10) + 'px ' + settings.fontFamily + ctx.fillStyle = color + + // Translate the canvas position to the origin coordinate of where + // the text should be put. + ctx.translate( + (gx + info.gw / 2) * g * mu, + (gy + info.gh / 2) * g * mu + ) + + if (rotateDeg !== 0) { + ctx.rotate(-rotateDeg) + } + + // Finally, fill the text. + + // XXX: We cannot because textBaseline = 'top' here because + // Firefox and Chrome uses different default line-height for canvas. + // Please read https://bugzil.la/737852#c6. + // Here, we use textBaseline = 'middle' and draw the text at exactly + // 0.5 * fontSize lower. + ctx.textBaseline = 'middle' + ctx.fillText( + word, info.fillTextOffsetX * mu, + (info.fillTextOffsetY + fontSize * 0.5) * mu + ) + + // The below box is always matches how s are positioned + /* ctx.strokeRect(info.fillTextOffsetX, info.fillTextOffsetY, + info.fillTextWidth, info.fillTextHeight) */ + + // Restore the state. + ctx.restore() + } else { + // drawText on DIV element + var span = document.createElement('span') + var transformRule = '' + transformRule = 'rotate(' + (-rotateDeg / Math.PI * 180) + 'deg) ' + if (info.mu !== 1) { + transformRule += + 'translateX(-' + (info.fillTextWidth / 4) + 'px) ' + + 'scale(' + (1 / info.mu) + ')' + } + var styleRules = { + position: 'absolute', + display: 'block', + font: fontWeight + ' ' + + (fontSize * info.mu) + 'px ' + settings.fontFamily, + left: ((gx + info.gw / 2) * g + info.fillTextOffsetX) + 'px', + top: ((gy + info.gh / 2) * g + info.fillTextOffsetY) + 'px', + width: info.fillTextWidth + 'px', + height: info.fillTextHeight + 'px', + lineHeight: fontSize + 'px', + whiteSpace: 'nowrap', + transform: transformRule, + webkitTransform: transformRule, + msTransform: transformRule, + transformOrigin: '50% 40%', + webkitTransformOrigin: '50% 40%', + msTransformOrigin: '50% 40%' + } + if (color) { + styleRules.color = color + } + span.textContent = word + for (var cssProp in styleRules) { + span.style[cssProp] = styleRules[cssProp] + } + if (attributes) { + for (var attribute in attributes) { + span.setAttribute(attribute, attributes[attribute]) + } + } + if (classes) { + span.className += classes + } + el.appendChild(span) + } + }) + } + + /* Help function to updateGrid */ + var fillGridAt = function fillGridAt (x, y, drawMask, dimension, item) { + if (x >= ngx || y >= ngy || x < 0 || y < 0) { + return + } + + grid[x][y] = false + + if (drawMask) { + var ctx = elements[0].getContext('2d') + ctx.fillRect(x * g, y * g, maskRectWidth, maskRectWidth) + } + + if (interactive) { + infoGrid[x][y] = { item: item, dimension: dimension } + } + } + + /* Update the filling information of the given space with occupied points. + Draw the mask on the canvas if necessary. */ + var updateGrid = function updateGrid (gx, gy, gw, gh, info, item) { + var occupied = info.occupied + var drawMask = settings.drawMask + var ctx + if (drawMask) { + ctx = elements[0].getContext('2d') + ctx.save() + ctx.fillStyle = settings.maskColor + } + + var dimension + if (interactive) { + var bounds = info.bounds + dimension = { + x: (gx + bounds[3]) * g, + y: (gy + bounds[0]) * g, + w: (bounds[1] - bounds[3] + 1) * g, + h: (bounds[2] - bounds[0] + 1) * g + } + } + + var i = occupied.length + while (i--) { + var px = gx + occupied[i][0] + var py = gy + occupied[i][1] + + if (px >= ngx || py >= ngy || px < 0 || py < 0) { + continue + } + + fillGridAt(px, py, drawMask, dimension, item) + } + + if (drawMask) { + ctx.restore() + } + } + + /* putWord() processes each item on the list, + calculate it's size and determine it's position, and actually + put it on the canvas. */ + var putWord = function putWord (item) { + var word, weight, attributes + if (Array.isArray(item)) { + word = item[0] + weight = item[1] + } else { + word = item.word + weight = item.weight + attributes = item.attributes + } + var rotateDeg = getRotateDeg() + + var extraDataArray = getItemExtraData(item) + + // get info needed to put the text onto the canvas + var info = getTextInfo(word, weight, rotateDeg, extraDataArray) + + // not getting the info means we shouldn't be drawing this one. + if (!info) { + return false + } + + if (exceedTime()) { + return false + } + + // If drawOutOfBound is set to false, + // skip the loop if we have already know the bounding box of + // word is larger than the canvas. + if (!settings.drawOutOfBound && !settings.shrinkToFit) { + var bounds = info.bounds; + if ((bounds[1] - bounds[3] + 1) > ngx || + (bounds[2] - bounds[0] + 1) > ngy) { + return false + } + } + + // Determine the position to put the text by + // start looking for the nearest points + var r = maxRadius + 1 + + var tryToPutWordAtPoint = function (gxy) { + var gx = Math.floor(gxy[0] - info.gw / 2) + var gy = Math.floor(gxy[1] - info.gh / 2) + var gw = info.gw + var gh = info.gh + + // If we cannot fit the text at this position, return false + // and go to the next position. + if (!canFitText(gx, gy, gw, gh, info.occupied)) { + return false + } + + // Actually put the text on the canvas + drawText(gx, gy, info, word, weight, + (maxRadius - r), gxy[2], rotateDeg, attributes, extraDataArray) + + // Mark the spaces on the grid as filled + updateGrid(gx, gy, gw, gh, info, item) + + // Return true so some() will stop and also return true. + return true + } + + while (r--) { + var points = getPointsAtRadius(maxRadius - r) + + if (settings.shuffle) { + points = [].concat(points) + shuffleArray(points) + } + + // Try to fit the words by looking at each point. + // array.some() will stop and return true + // when putWordAtPoint() returns true. + // If all the points returns false, array.some() returns false. + var drawn = points.some(tryToPutWordAtPoint) + + if (drawn) { + // leave putWord() and return true + return true + } + } + if (settings.shrinkToFit) { + if (Array.isArray(item)) { + item[1] = item[1] * 3 / 4 + } else { + item.weight = item.weight * 3 / 4 + } + return putWord(item) + } + // we tried all distances but text won't fit, return false + return false + } + + /* Send DOM event to all elements. Will stop sending event and return + if the previous one is canceled (for cancelable events). */ + var sendEvent = function sendEvent (type, cancelable, details) { + if (cancelable) { + return !elements.some(function (el) { + var event = new CustomEvent(type, { + detail: details || {} + }) + return !el.dispatchEvent(event) + }, this) + } else { + elements.forEach(function (el) { + var event = new CustomEvent(type, { + detail: details || {} + }) + el.dispatchEvent(event) + }, this) + } + } + + /* Start drawing on a canvas */ + var start = function start () { + // For dimensions, clearCanvas etc., + // we only care about the first element. + var canvas = elements[0] + + if (canvas.getContext) { + ngx = Math.ceil(canvas.width / g) + ngy = Math.ceil(canvas.height / g) + } else { + var rect = canvas.getBoundingClientRect() + ngx = Math.ceil(rect.width / g) + ngy = Math.ceil(rect.height / g) + } + + // Sending a wordcloudstart event which cause the previous loop to stop. + // Do nothing if the event is canceled. + if (!sendEvent('wordcloudstart', true)) { + return + } + + // Determine the center of the word cloud + center = (settings.origin) + ? [settings.origin[0] / g, settings.origin[1] / g] + : [ngx / 2, ngy / 2] + + // Maxium radius to look for space + maxRadius = Math.floor(Math.sqrt(ngx * ngx + ngy * ngy)) + + /* Clear the canvas only if the clearCanvas is set, + if not, update the grid to the current canvas state */ + grid = [] + + var gx, gy, i + if (!canvas.getContext || settings.clearCanvas) { + elements.forEach(function (el) { + if (el.getContext) { + var ctx = el.getContext('2d') + ctx.fillStyle = settings.backgroundColor + ctx.clearRect(0, 0, ngx * (g + 1), ngy * (g + 1)) + ctx.fillRect(0, 0, ngx * (g + 1), ngy * (g + 1)) + } else { + el.textContent = '' + el.style.backgroundColor = settings.backgroundColor + el.style.position = 'relative' + } + }) + + /* fill the grid with empty state */ + gx = ngx + while (gx--) { + grid[gx] = [] + gy = ngy + while (gy--) { + grid[gx][gy] = true + } + } + } else { + /* Determine bgPixel by creating + another canvas and fill the specified background color. */ + var bctx = document.createElement('canvas').getContext('2d') + + bctx.fillStyle = settings.backgroundColor + bctx.fillRect(0, 0, 1, 1) + var bgPixel = bctx.getImageData(0, 0, 1, 1).data + + /* Read back the pixels of the canvas we got to tell which part of the + canvas is empty. + (no clearCanvas only works with a canvas, not divs) */ + var imageData = + canvas.getContext('2d').getImageData(0, 0, ngx * g, ngy * g).data + + gx = ngx + var x, y + while (gx--) { + grid[gx] = [] + gy = ngy + while (gy--) { + y = g + /* eslint no-labels: ["error", { "allowLoop": true }] */ + singleGridLoop: while (y--) { + x = g + while (x--) { + i = 4 + while (i--) { + if (imageData[((gy * g + y) * ngx * g + + (gx * g + x)) * 4 + i] !== bgPixel[i]) { + grid[gx][gy] = false + break singleGridLoop + } + } + } + } + if (grid[gx][gy] !== false) { + grid[gx][gy] = true + } + } + } + + imageData = bctx = bgPixel = undefined + } + + // fill the infoGrid with empty state if we need it + if (settings.hover || settings.click) { + interactive = true + + /* fill the grid with empty state */ + gx = ngx + 1 + while (gx--) { + infoGrid[gx] = [] + } + + if (settings.hover) { + canvas.addEventListener('mousemove', wordcloudhover) + } + + if (settings.click) { + canvas.addEventListener('click', wordcloudclick) + canvas.style.webkitTapHighlightColor = 'rgba(0, 0, 0, 0)' + } + + canvas.addEventListener('wordcloudstart', function stopInteraction () { + canvas.removeEventListener('wordcloudstart', stopInteraction) + canvas.removeEventListener('mousemove', wordcloudhover) + canvas.removeEventListener('click', wordcloudclick) + hovered = undefined + }) + } + + i = 0 + var loopingFunction, stoppingFunction + if (settings.wait !== 0) { + loopingFunction = window.setTimeout + stoppingFunction = window.clearTimeout + } else { + loopingFunction = window.setImmediate + stoppingFunction = window.clearImmediate + } + + var addEventListener = function addEventListener (type, listener) { + elements.forEach(function (el) { + el.addEventListener(type, listener) + }, this) + } + + var removeEventListener = function removeEventListener (type, listener) { + elements.forEach(function (el) { + el.removeEventListener(type, listener) + }, this) + } + + var anotherWordCloudStart = function anotherWordCloudStart () { + removeEventListener('wordcloudstart', anotherWordCloudStart) + stoppingFunction(timer[timerId]) + } + + addEventListener('wordcloudstart', anotherWordCloudStart) + timer[timerId] = loopingFunction(function loop () { + if (i >= settings.list.length) { + stoppingFunction(timer[timerId]) + sendEvent('wordcloudstop', false) + removeEventListener('wordcloudstart', anotherWordCloudStart) + delete timer[timerId]; + return + } + escapeTime = (new Date()).getTime() + var drawn = putWord(settings.list[i]) + var canceled = !sendEvent('wordclouddrawn', true, { + item: settings.list[i], + drawn: drawn + }) + if (exceedTime() || canceled) { + stoppingFunction(timer[timerId]) + settings.abort() + sendEvent('wordcloudabort', false) + sendEvent('wordcloudstop', false) + removeEventListener('wordcloudstart', anotherWordCloudStart) + delete timer[timerId] + return + } + i++ + timer[timerId] = loopingFunction(loop, settings.wait) + }, settings.wait) + } + + // All set, start the drawing + start() + } + + WordCloud.isSupported = isSupported + WordCloud.minFontSize = minFontSize + WordCloud.stop = function stop () { + if (timer) { + for (var timerId in timer) { + window.clearImmediate(timer[timerId]) + } + } + } + + // Expose the library as an AMD module + if (typeof define === 'function' && define.amd) { // eslint-disable-line no-undef + global.WordCloud = WordCloud + define('wordcloud', [], function () { return WordCloud }) // eslint-disable-line no-undef + } else if (typeof module !== 'undefined' && module.exports) { // eslint-disable-line no-undef + module.exports = WordCloud // eslint-disable-line no-undef + } else { + global.WordCloud = WordCloud + } +})(this) // jshint ignore:line