Skip to content

Commit

Permalink
word cloud redux: extra.Packer + extra.textDimensions = ulysses.lib.p…
Browse files Browse the repository at this point in the history
…acker
  • Loading branch information
andrew committed Jun 12, 2016
1 parent a529b14 commit fded57c
Show file tree
Hide file tree
Showing 6 changed files with 245 additions and 15 deletions.
126 changes: 122 additions & 4 deletions resources/public/js/extra.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@
// NOTE should be matched with externs in extra.externs.js
//

var extra = (function () {
var extra = (function() {
"use strict";

/**
* Given a multiselect element, get array of selected values
*/
var multiselect_values = function (el) {
var multiselect_values = function(el) {
var values = [];

for (var i = 0, n = el.options.length; i < n; i++) {
Expand All @@ -27,14 +27,132 @@ var extra = (function () {
/**
* Given a select element, get selected value (or null)
*/
var select_values = function (el) {
var select_values = function(el) {
var mv = multiselect_values(el);

return mv.length > 0 ? mv[0] : null;
};

/**
* Pack blocks into bin
* Adapted from:
* http://codeincomplete.com/posts/2011/5/7/bin_packing/
*/
var Packer = (function() {

Packer = function(w, h) {
this.init(w, h);
};

Packer.prototype = {
init: function(w, h) {
this.root = { x: 0, y: 0, w: w, h: h };
},

fit: function(blocks) {
var n, node, block, final;

final = [];

for (n = 0; n < blocks.length; n++) {
block = blocks[n];

if (node = this.findNode(this.root, block.w, block.h)) {
block.fit = this.splitNode(node, block.w, block.h);
final.push(block);
}
}

return final;
},

findNode: function(root, w, h) {
if (root.used) {
return this.findNode(root.right, w, h) || this.findNode(root.down, w, h);
} else if ((w <= root.w) && (h <= root.h)) {
return root;
} else {
return null;
}
},

splitNode: function(node, w, h) {
node.used = true;
node.down = { x: node.x, y: node.y + h, w: node.w, h: node.h - h };
node.right = { x: node.x + w, y: node.y, w: node.w - w, h: h };
return node;
}
};

return Packer;
})();

/**
* Get the dimensions of a piece of text
* textDimensions('hello world!',
* { fontFamily: 'Arial', fontSize: '16px', fontWeight: 'normal'})
*/
var textDimensions = (function() {
var createDummyElement = function(name, options) {
var element = document.createElement('div'),
nameNode = document.createTextNode(name);

element.appendChild(nameNode);

element.style.fontFamily = options.fontFamily;
element.style.fontSize = options.fontSize;
element.style.fontWeight = options.fontWeight;
element.style.position = 'absolute';
element.style.visibility = 'hidden';
element.style.left = '-999px';
element.style.top = '-999px';
element.style.width = 'auto';
element.style.height = 'auto';

document.body.appendChild(element);

return element;
};

var destroyElement = function(element) {
element.parentNode.removeChild(element);
}

var cache = {};

return function(name, options) {
var cacheKey = JSON.stringify({ name: name, options: options });

if (cache[cacheKey]) {
return cache[cacheKey];
}

// prepare options
options = options || {};
options.fontFamily = options.fontFamily || 'Times';
options.fontSize = options.fontSize || '16px';
options.fontWeight = options.fontWeight || 'normal';

var element = createDummyElement(name, options);

var result = {
w: element.offsetWidth,
h: element.offsetHeight,
name: name
};

destroyElement(element);

cache[cacheKey] = result;

return result;
};
})();

return {
multiselect_values: multiselect_values,
select_values: select_values
select_values: select_values,
Packer: Packer,
textDimensions: textDimensions
};
})();
15 changes: 9 additions & 6 deletions src/cljs/ulysses/components/word_cloud.cljs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
(ns ulysses.components.word-cloud
(:require [ulysses.utils :refer [map-subels]]))
(:require [ulysses.utils :refer [map-subels]]
[ulysses.lib.packer :refer [fit-words]]
[clojure.string :as string]))

;; ----------------------------------------------------------------------------
;; helpers
Expand All @@ -12,29 +14,30 @@
denom (- s-max s-min)]
(map
(fn [kw]
(assoc kw :nscore
(assoc kw :weight
(/ (- (get-in kw [:pivot :score]) s-min)
denom)))
keywords)))

(defn normalize-sort-take [n keywords]
(->> keywords
(normalize)
(sort-by :nscore)
(sort-by :weight)
(reverse)
(take n)))

;; ----------------------------------------------------------------------------
;; sub-components
;; ----------------------------------------------------------------------------

(defn word-cloud-keyword [{:keys [name nscore]}]
[:a.keyword {:class (str "weight-" (Math/round (* 100 nscore)))} name])
(defn word-cloud-keyword [{:keys [name weight]}]
[:a.keyword {:class (str "weight-" (Math/round (* 100 weight)))} name])

;; ----------------------------------------------------------------------------
;; main
;; ----------------------------------------------------------------------------

(defn word-cloud [n keywords]
[:div.word-cloud
(map-subels word-cloud-keyword (normalize-sort-take n keywords))])
[fit-words 900 500
(normalize-sort-take n keywords)]])
58 changes: 58 additions & 0 deletions src/cljs/ulysses/lib/packer.cljs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
(ns ulysses.lib.packer
(:require [clojure.walk :refer [keywordize-keys]]
[ulysses.utils :refer [map-subels ptr]]))

(def jsPacker (.-Packer js/extra))

(defn fit
[w h blocks]
(let [p (new jsPacker w h)]
(->> blocks
(sort-by :h)
(reverse)
(clj->js)
(.fit p)
(js->clj)
(keywordize-keys)
(map
(fn [block]
(update block :fit dissoc
:used :right :down :w :h))))))

(def jsTextDimensions (.-textDimensions js/extra))

(defn text-dimensions
[text options]
(-> text
(jsTextDimensions (clj->js options))
(js->clj)
(keywordize-keys)))

(defn fit-words
[w h words]
(let [style-base {:fontFamily "Source Sans Pro" :fontWeight "100"}]
[:div
{:style {:position :relative :width w :height h}}
(->> words
(map
(fn [{:keys [name weight]}]
(let [style (assoc style-base :fontSize (-> weight (* 10) (Math/log) (* 35) (str "px")))]
(-> name
(text-dimensions style)
(update :w
(partial + 8))
(assoc :style style)
(assoc :weight weight)))))
(fit w h)
(map-subels
(fn [{:keys [w h fit name style weight]}]
[:div.keyword
{:style
(assoc style
:width w
:height h
:left (:x fit)
:top (:y fit))
:class
(str "weight-" (Math/round (* 100 weight)))}
name])))]))
1 change: 1 addition & 0 deletions src/cljs/ulysses/pages/builder.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
[ulysses.components.misc :refer [grant-op-meta]]
[ulysses.components.word-cloud :refer [word-cloud]]
[ulysses.utils :refer [map-subels map-lookup str->int classes-attr]]
[ulysses.lib.packer :as packer]
[reagent.core :as r]
[clojure.string :as string]
[cljs.pprint :as pprint]))
Expand Down
53 changes: 51 additions & 2 deletions src/externs/extra.externs.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,63 @@
*/
var extra = {};

// ----------------------------------------------------------------------------
// Select values
// ----------------------------------------------------------------------------

/**
* @param {Element} el
* @return {Array.<string>}
*/
extra.multiselect_values = function (el) {};
extra.multiselect_values = function(el) {};

/**
* @param {Element} el
* @return {string|null}
*/
extra.select_values = function (el) {};
extra.select_values = function(el) {};

// ----------------------------------------------------------------------------
// Packer
// ----------------------------------------------------------------------------

/**
* @param {Int} w
* @param {Int} h
*/
extra.Packer = function(w, h) {};

/**
* @param {Int} w
* @param {Int} h
*/
extra.Packer.prototype.init = function(w, h) {};

/**
* @param {Array} blocks
*/
extra.Packer.prototype.fit = function(blocks) {};

/**
* @param {Object} root
* @param {Int} w
* @param {Int} h
*/
extra.Packer.prototype.findNode = function(root, w, h) {};

/**
* @param {Object} node
* @param {Int} w
* @param {Int} h
*/
extra.Packer.prototype.splitNode = function(node, w, h) {};

// ----------------------------------------------------------------------------
// Text dimensions
// ----------------------------------------------------------------------------

/**
* @param {string} text
* @param {Object} options
*/
extra.textDimensions = function(text, options) {};
7 changes: 4 additions & 3 deletions src/sass/components/_word-cloud.scss
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@
background: $black;
color: $white;
display: block;
float: left;
margin: .5em .5em 0 0;
padding: .3em .7em .2em;
overflow: hidden;
padding: 0 4px;
position: absolute;
text-decoration: none;
white-space: nowrap;

@for $i from 0 through 100 {
&.weight-#{$i} {
Expand Down

0 comments on commit fded57c

Please sign in to comment.