diff --git a/client/app/app.less b/client/app/app.less index de2c1a0..a7e9cee 100644 --- a/client/app/app.less +++ b/client/app/app.less @@ -420,4 +420,5 @@ label{ @import 'date-histogram-aggregation/date-histogram-aggregation.less'; @import 'expanded-listing-view/expanded-listing-view.less'; @import 'modal/modal.less'; +@import 'wordle/wordle-chart.less'; // endinjector \ No newline at end of file diff --git a/client/app/search/search-results/details/details.html b/client/app/search/search-results/details/details.html index c4060c4..ddc2d37 100644 --- a/client/app/search/search-results/details/details.html +++ b/client/app/search/search-results/details/details.html @@ -80,3 +80,4 @@ + diff --git a/client/app/search/search-results/search-results.partial.html b/client/app/search/search-results/search-results.partial.html index b580fd3..5ee8168 100644 --- a/client/app/search/search-results/search-results.partial.html +++ b/client/app/search/search-results/search-results.partial.html @@ -50,6 +50,10 @@
{{facets.simFilter.title}} --> + +
+
- +
diff --git a/client/components/wordle/scripts/wordcloud2.js b/client/components/wordle/scripts/wordcloud2.js new file mode 100644 index 0000000..cd12623 --- /dev/null +++ b/client/components/wordle/scripts/wordcloud2.js @@ -0,0 +1,1108 @@ +/*! + * wordcloud2.js + * http://timdream.org/wordcloud2.js/ + * + * Copyright 2011 - 2013 Tim Chien + * 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.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 miniumFontSize = (function getMiniumFontSize() { + 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; + })(); + + // 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 WordCloud = function WordCloud(elements, options) { + if (!isSupported) { + return; + } + + if (!Array.isArray(elements)) { + elements = [elements]; + } + + elements.forEach(function(el, i) { + if (typeof el === 'string') { + elements[i] = document.getElementById(el); + if (!elements[i]) { + throw 'The element id specified is not found.'; + } + } else if (!el.tagName && !el.appendChild) { + throw '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, + 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, + + shuffle: true, + rotateRatio: 0.1, + + shape: 'circle', + ellipticity: 0.65, + + 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': + case 'square': + // 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 '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 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; + switch (settings.color) { + case 'random-dark': + getTextColor = function getRandomDarkColor() { + return 'rgb(' + + Math.floor(Math.random() * 128).toString(10) + ',' + + Math.floor(Math.random() * 128).toString(10) + ',' + + Math.floor(Math.random() * 128).toString(10) + ')'; + }; + break; + + case 'random-light': + getTextColor = function getRandomLightColor() { + return 'rgb(' + + Math.floor(Math.random() * 128 + 128).toString(10) + ',' + + Math.floor(Math.random() * 128 + 128).toString(10) + ',' + + Math.floor(Math.random() * 128 + 128).toString(10) + ')'; + }; + break; + + default: + if (typeof settings.color === 'function') { + getTextColor = settings.color; + } + break; + } + + /* Interactive */ + var interactive = false; + var infoGrid = []; + var hovered; + + var getInfoGridFromMouseEvent = function getInfoGridFromMouseEvent(evt) { + var canvas = evt.currentTarget; + var rect = canvas.getBoundingClientRect(); + var eventX = evt.clientX - rect.left; + var eventY = evt.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); + + return infoGrid[x][y]; + }; + + var wordcloudhover = function wordcloudhover(evt) { + var info = getInfoGridFromMouseEvent(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 = getInfoGridFromMouseEvent(evt); + if (!info) { + return; + } + + settings.click(info.item, info.dimension, evt); + }; + + /* 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; + } + + return minRotation + Math.random() * rotationRange; + }; + + var getTextInfo = function getTextInfo(word, weight, rotateDeg) { + // 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 < miniumFontSize) { + mu = (function calculateScaleFactor() { + var mu = 2; + while (mu * fontSize < miniumFontSize) { + mu += 2; + } + return mu; + })(); + } + + var fcanvas = document.createElement('canvas'); + var fctx = fcanvas.getContext('2d', { willReadFrequently: true }); + + fctx.font = settings.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 = settings.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, gy, x, y; + var bounds = [cgh / 2, cgw / 2, cgh / 2, cgw / 2]; + while (gx--) { + gy = cgh; + while (gy--) { + y = g; + 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 || !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) { + + var fontSize = info.fontSize; + var color; + if (getTextColor) { + color = getTextColor(word, weight, fontSize, distance, theta); + } else { + color = settings.color; + } + + var dimension; + 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 + }; + + 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 = settings.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': settings.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', + 'color': color, + 'lineHeight': fontSize + 'px', + 'whiteSpace': 'nowrap', + 'transform': transformRule, + 'webkitTransform': transformRule, + 'msTransform': transformRule, + 'transformOrigin': '50% 40%', + 'webkitTransformOrigin': '50% 40%', + 'msTransformOrigin': '50% 40%' + }; + 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]); + } + } + 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--) { + fillGridAt(gx + occupied[i][0], gy + occupied[i][1], + 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(); + + // get info needed to put the text onto the canvas + var info = getTextInfo(word, weight, rotateDeg); + + // not getting the info means we shouldn't be drawing this one. + if (!info) { + return false; + } + + if (exceedTime()) { + return false; + } + + // Skip the loop if we have already know the bounding box of + // word is larger than the canvas. + 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); + + // 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; + } + } + // 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, detail) { + if (cancelable) { + return !elements.some(function(el) { + var evt = document.createEvent('CustomEvent'); + evt.initCustomEvent(type, true, cancelable, detail || {}); + return !el.dispatchEvent(evt); + }, this); + } else { + elements.forEach(function(el) { + var evt = document.createEvent('CustomEvent'); + evt.initCustomEvent(type, true, cancelable, detail || {}); + el.dispatchEvent(evt); + }, 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.floor(canvas.width / g); + ngy = Math.floor(canvas.height / g); + } else { + var rect = canvas.getBoundingClientRect(); + ngx = Math.floor(rect.width / g); + ngy = Math.floor(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; + } + }); + + /* 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; + 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.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); + }; + + addEventListener('wordcloudstart', anotherWordCloudStart); + + var timer = loopingFunction(function loop() { + if (i >= settings.list.length) { + stoppingFunction(timer); + sendEvent('wordcloudstop', false); + removeEventListener('wordcloudstart', anotherWordCloudStart); + + 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); + settings.abort(); + sendEvent('wordcloudabort', false); + sendEvent('wordcloudstop', false); + removeEventListener('wordcloudstart', anotherWordCloudStart); + return; + } + i++; + timer = loopingFunction(loop, settings.wait); + }, settings.wait); + }; + + // All set, start the drawing + start(); + }; + + WordCloud.isSupported = isSupported; + WordCloud.miniumFontSize = miniumFontSize; + + // Expose the library as an AMD module + if (typeof global.define === 'function' && global.define.amd) { + global.define('wordcloud', [], function() { return WordCloud; }); + } else { + global.WordCloud = WordCloud; + } + +})(window); diff --git a/client/components/wordle/wordle-chart.directive.js b/client/components/wordle/wordle-chart.directive.js new file mode 100644 index 0000000..9cc1d40 --- /dev/null +++ b/client/components/wordle/wordle-chart.directive.js @@ -0,0 +1,112 @@ +'use strict'; + +angular.module('digApp').directive('wordleChart', ['$timeout', function ($timeout) { + return { + templateUrl: 'components/wordle/wordle-chart.html', + restrict: 'E', + scope: { + aggregationName: '@', + aggregationSize: '@', + aggregationKey: '@', + indexVM: '=indexvm', + ejs: '=', + filters: '=' + }, + link: function ($scope, element) { + $scope.chartEl = $(element).find('.wordle-chart')[0]; + $scope.chart = null; + $scope.htmlCanvas = null; + + $scope.drawWordle = function(options) { + var htmlCanvas = $("
"); + $($scope.chartEl).empty(); + $($scope.chartEl).append(htmlCanvas); + $scope.htmlCanvas = $($scope.chartEl).find('.wordle-html-canvas')[0]; + $timeout(function() { + WordCloud([$scope.chartEl, $scope.htmlCanvas], options); + $scope.chart = $($scope.chartEl); + }); + }; + + var formatData = function(data) { + var list = []; + var stopwords = ['i','a','about', 'an','and','are','as','at', + 'be', 'been','by','com','for', 'from','how','in', 'so','use', + 'is','it','not', 'of','on','or','that', 'self', 'their', 'such', + 'the','this','to','was', 'what','when','where', 'which', 'has', + 'who','will','with', 'www','the','
','br', 'no', 'both', 'due', 'up', 'more', + 'u','me','you','your','my','we', 'have','am', 'can', 'were', 'than','also'] + + if(data.buckets.length > 0) { + + data.buckets.forEach(function(item) { + var row = []; + if($.inArray(item.key, stopwords) == -1 && item.key.length > 1) { + row.push(item.key); + row.push(item.doc_count); + list.push(row); + //console.log(row[0] + ":" + row[1]); + + } + }); + } + return list; + }; + + $scope.render = function(data) { + if(data) { + var list = formatData(data); + + var options={}; + options.list = list; + options.gridSize = 15; + + var multiplier = 15; + if(list.length > 0) { + var size = list[0][1]; + multiplier = Math.min(multiplier, 20 / Math.log(size)) + + var sqrt = Math.sqrt(size); + var first = multiplier * sqrt; + if(first > 70) + multiplier = 70/sqrt; + console.log("mult:" + multiplier + ", sqrt(size):" + sqrt + "=" + multiplier * sqrt); + } + options.weightFactor=function(size) { + //console.log("mult:" + multiplier + ", sqrt(size):" + Math.sqrt(size) + "=" + multiplier * Math.sqrt(size)); + return multiplier * Math.sqrt(size); + }; + + options.fontFamily = 'Times, serif'; + options.color = 'random-dark'; //random-light'; + options.backgroundColor = "None"; + options.rotateRatio=0.5; + //options.wait = 2; + options.abortThreshold = 500; + options.abort = function() { + console.log("Wordle draw aborted..was taking too long"); + } + //options.origin= [90, 0]; + $scope.drawWordle(options); + } + }; + + $scope.$watch('indexVM.results.aggregations', function() { + var data = $scope.$eval('indexVM.results.aggregations.' + $scope.aggregationName + + ' || indexVM.results.aggregations.filtered_' + $scope.aggregationName + '.' + $scope.aggregationName); + $scope.render(data); + $timeout(function() { + // TODO: Consider replacing with something more element. + // Defer a resize on initial render to later in the update cycle to force the chart size to + // update after the page has rendered. This avoids sizing issues on initial render since + // most of the page elements have not been built and the area available to fill is of + // size 0 on the initial render or in flux. + if ($scope.chart) { + $scope.chart.resize({height:100}); + } + }); + }, true); + + } + }; +}]); diff --git a/client/components/wordle/wordle-chart.directive.spec.js b/client/components/wordle/wordle-chart.directive.spec.js new file mode 100644 index 0000000..d8b8792 --- /dev/null +++ b/client/components/wordle/wordle-chart.directive.spec.js @@ -0,0 +1,21 @@ +'use strict'; + +describe('Directive: wordleChart', function () { + + // load the directive's module and view + beforeEach(module('digApp')); + beforeEach(module('components/wordle/wordle-chart.html')); + + var element, scope; + + beforeEach(inject(function ($rootScope) { + scope = $rootScope.$new(); + })); + + it('should make hidden element visible', inject(function ($compile) { + element = angular.element(''); + element = $compile(element)(scope); + scope.$apply(); + expect(element.text()).toBe('this is the wordleChart directive'); + })); +}); \ No newline at end of file diff --git a/client/components/wordle/wordle-chart.html b/client/components/wordle/wordle-chart.html new file mode 100644 index 0000000..bd7bf5c --- /dev/null +++ b/client/components/wordle/wordle-chart.html @@ -0,0 +1,6 @@ +
+
+
+
+
\ No newline at end of file diff --git a/client/components/wordle/wordle-chart.less b/client/components/wordle/wordle-chart.less new file mode 100644 index 0000000..42ca2f4 --- /dev/null +++ b/client/components/wordle/wordle-chart.less @@ -0,0 +1,37 @@ +.wordle-chart .tick:first-of-type text { + text-anchor: start !important; +} + +.wordle-chart .tick:nth-of-type(5) text { + text-anchor: end !important; +} + +.wordle-chart { + overflow-x: visible; + overflow-y: visible; + position: relative; + margin-top: 20px; + margin-bottom: 20px; + min-height: 150px; +} + +.wordle-canvas { + display: block; + position: relative; + overflow: hidden; + } + + .wordle-canvas.hide { + display: none; + } + + .wordle-html-canvas > span { + transition: text-shadow 1s ease, opacity 1s ease; + -webkit-transition: text-shadow 1s ease, opacity 1s ease; + -ms-transition: text-shadow 1s ease, opacity 1s ease; + } + + .wordle-html-canvas > span:hover { + text-shadow: 0 0 10px, 0 0 10px #fff, 0 0 10px #fff, 0 0 10px #fff; + opacity: 0.5; + } \ No newline at end of file diff --git a/client/index.html b/client/index.html index 675bb46..8e8985d 100644 --- a/client/index.html +++ b/client/index.html @@ -96,6 +96,8 @@ + + diff --git a/server/config/environment/index.js b/server/config/environment/index.js index 2b76e34..57d2c53 100644 --- a/server/config/environment/index.js +++ b/server/config/environment/index.js @@ -43,9 +43,9 @@ var all = { }, appVersion: pjson.version, - euiServerUrl: process.env.EUI_SERVER_URL || 'http://localhost', - euiServerPort: process.env.EUI_SERVER_PORT || 9200, - euiSearchIndex: process.env.EUI_SEARCH_INDEX || 'dig', + euiServerUrl: process.env.EUI_SERVER_URL || 'karma-dig-service.cloudapp.net', + euiServerPort: process.env.EUI_SERVER_PORT || 55310, + euiSearchIndex: process.env.EUI_SEARCH_INDEX || 'dig-mrs-latest', imageSimUrl: process.env.IMAGE_SIM_URL || 'http://localhost', imageSimPort: process.env.IMAGE_SIM_PORT || 3001, @@ -430,6 +430,11 @@ var all = { euiFilters: [], //simFilter: {}, aggFilters: [{ + title: 'Provider', + type: 'eui-aggregation', + field: 'provider_agg', + terms: 'hasFeatureCollection.provider_name_feature.provider_name', + }, { title: 'Author', type: 'eui-aggregation', field: 'author_agg', @@ -453,6 +458,21 @@ var all = { field: 'compound_agg', terms: 'hasFeatureCollection.compound_feature.compound', count: 20 + }, { + title: 'Organization', + type: 'eui-aggregation', + field: 'organization_agg', + terms: 'hasFeatureCollection.organization_feature.organization', + }, { + title: 'Country', + type: 'eui-aggregation', + field: 'country_agg', + terms: 'hasFeatureCollection.wipo_country_feature.wipo_country_code', + },{ + title: 'Keywords', + type: 'eui-aggregation', + field: 'keywords_agg', + terms: 'hasFeatureCollection.keywords_feature.keywords', }] }, @@ -465,7 +485,7 @@ var all = { }], short: [{ title: 'Date', - field: "doc._source.dateCreated | date:'MM/dd/yyyy HH:mm:ss UTC'", + field: "doc._source.dateCreated || doc._source.datePublished | date:'MM/dd/yyyy HH:mm:ss UTC'", classes: 'date' },{ title: 'Author', @@ -494,12 +514,30 @@ var all = { field: 'doc._source.hasFeatureCollection.compound_feature.compound', featureArray: 'doc._source.hasFeatureCollection.compound_feature', featureValue: 'compound' + }, { + title: 'DOI', + field: 'doc._source.digitalObjectIdentifier', + classes: 'location' + }, { + title: 'Provider', + field: 'doc._source.hasFeatureCollection.provider_name_feature.provider_name', + classes: 'location' + }, { + title: 'Country', + field: 'doc._source.hasFeatureCollection.wipo_country_feature.wipo_country_code', + classes: 'location', + hideIfMissing: true + },{ + title: 'Organization', + field: 'doc._source.hasFeatureCollection.organization_feature.organization || doc._source.hasFeatureCollection.organization_feature[0].organization', + classes: 'location', + hideIfMissing: true },{ title: 'Abstract', field: "doc['_source']['hasAbstractPart']['text']" },{ title: 'Date', - field: "doc._source.dateCreated | date:'MM/dd/yyyy HH:mm:ss UTC'" + field: "doc._source.dateCreated || doc._source.datePublished | date:'MM/dd/yyyy HH:mm:ss UTC'" }] } } @@ -510,7 +548,7 @@ var all = { classes: 'listing-details', fields: [{ title: 'Date', - field: "doc._source.dateCreated | date:'MM/dd/yyyy HH:mm:ss UTC'", + field: "doc._source.dateCreated || doc._source.datePublished | date:'MM/dd/yyyy HH:mm:ss UTC'", classes: 'date' },{ title: 'Authors(s)', @@ -527,9 +565,50 @@ var all = { field: 'doc._source.hasFeatureCollection.compound_feature.compound', featureArray: 'doc._source.hasFeatureCollection.compound_feature', featureValue: 'compound' + }, { + title: 'DOI', + field: 'doc._source.digitalObjectIdentifier', + classes: 'location' + }, { + title: 'Provider', + field: 'doc._source.hasFeatureCollection.provider_name_feature.provider_name', + classes: 'location' + }, { + title: 'Country', + field: 'doc._source.hasFeatureCollection.wipo_country_feature.wipo_country_code', + classes: 'location', + hideIfMissing: true + },{ + title: 'Organization', + field: 'doc._source.hasFeatureCollection.organization_feature.organization || doc._source.hasFeatureCollection.organization_feature[0].organization', + classes: 'location', + hideIfMissing: true + }, { + title: 'Keyword(s)', + field: 'doc._source.hasFeatureCollection.keywords_feature.keywords', + featureArray: 'doc._source.hasFeatureCollection.keywords_feature', + featureValue: 'keywords', + hideIfMissing: true + }, { + title: 'Reference(s)', + field: 'doc._source.isCitationOf.hasIdentifier.label', + featureArray: 'doc._source.isCitationOf', + featureValue: 'hasIdentifier.label', + hideIfMissing: true + }, { + title: 'Citation(s)', + field: 'doc._source.citation.hasIdentifier.label', + featureArray: 'doc._source.citation', + featureValue: 'hasIdentifier.label', + hideIfMissing: true },{ title: 'Abstract', field: "doc['_source']['hasAbstractPart']['text']" + }, { + title: 'Full Text', + field: 'doc._source.hasBodyPart.text', + featureArray: 'doc._source.hasBodyPart', + featureValue: 'text', }] } } @@ -541,4 +620,4 @@ var all = { // ============================================== module.exports = _.merge( all, - require('./' + process.env.NODE_ENV + '.js') || {}); + require('./' + process.env.NODE_ENV + '.js') || {}); \ No newline at end of file