From 796c20d445738e82fad6124768a7df28883da45d Mon Sep 17 00:00:00 2001 From: Andrew Suzuki Date: Wed, 22 Jun 2016 16:54:29 -0400 Subject: [PATCH] word cloud optimizations and memoization --- src/cljs/ulysses/components/word_cloud.cljs | 5 +- src/cljs/ulysses/handlers.cljs | 14 ++-- src/cljs/ulysses/lib/packer.cljs | 84 +++++++++++---------- src/cljs/ulysses/utils.cljs | 1 + src/js/extra.js | 74 ++++++++---------- src/sass/components/_word-cloud.scss | 3 + 6 files changed, 91 insertions(+), 90 deletions(-) diff --git a/src/cljs/ulysses/components/word_cloud.cljs b/src/cljs/ulysses/components/word_cloud.cljs index bf5c282..97822c1 100644 --- a/src/cljs/ulysses/components/word_cloud.cljs +++ b/src/cljs/ulysses/components/word_cloud.cljs @@ -1,7 +1,7 @@ (ns ulysses.components.word-cloud (:require [re-frame.core :refer [subscribe]] [ulysses.utils :refer [map-subels]] - [ulysses.lib.packer :refer [fit-words]] + [ulysses.lib.packer :refer [fit-words-memoized]] [clojure.string :as string])) ;; ---------------------------------------------------------------------------- @@ -29,6 +29,7 @@ (defn normalize-sort-scale [keywords] (->> keywords + (take 200) (normalize) (sort-by :weight) (reverse) @@ -51,5 +52,5 @@ (fn [keywords] (let [w (width-from-window @window-size)] [:div.word-cloud - [fit-words w 500 + [fit-words-memoized w 500 (normalize-sort-scale keywords)]])))) diff --git a/src/cljs/ulysses/handlers.cljs b/src/cljs/ulysses/handlers.cljs index bd109a3..d111091 100644 --- a/src/cljs/ulysses/handlers.cljs +++ b/src/cljs/ulysses/handlers.cljs @@ -4,6 +4,7 @@ [ulysses.config :as config] [ulysses.db :as db] [ulysses.lib.moment :as m] + [ulysses.lib.packer :refer [fit-words-memoized]] [ulysses.components.misc :refer [inspector]] [ulysses.utils :refer [document-size window-size @@ -202,11 +203,14 @@ (fn [faculties] (merge faculties - (-> response - :faculty - clean-incoming-faculty - list - by-id))))))) + (let [faculty (-> response + :faculty + clean-incoming-faculty)] + ; memoize word cloud keyword sizes + (doall (fit-words-memoized (:keywords faculty))) + (-> faculty + list + by-id)))))))) ;; ---------------------------------------------------------------------------- ;; user data diff --git a/src/cljs/ulysses/lib/packer.cljs b/src/cljs/ulysses/lib/packer.cljs index c39b056..25f13dc 100644 --- a/src/cljs/ulysses/lib/packer.cljs +++ b/src/cljs/ulysses/lib/packer.cljs @@ -7,56 +7,60 @@ [w h blocks] (let [p (new Packer 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)))))) + (update block "fit" dissoc + "used" "right" "down" "w" "h")))))) (defn text-dimensions - [text options] + [text font-size] + (println text) (-> text - (textDimensions (clj->js options)) + (textDimensions font-size) (js->clj) (keywordize-keys))) +(def text-dimensions-memoized + (memoize text-dimensions)) + (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 [keyword weight]}] - (let [style (assoc style-base :fontSize (-> weight (* 80) (str "px")))] - (-> keyword - (text-dimensions style) - ; padding - (update :w - (partial + 8)) - ; margin - (update :w - (partial + 4)) - (update :h - (partial + 4)) - ; style - (assoc :style style) - (assoc :weight weight))))) - (fit w h) - (map-subels - (fn [{:keys [w h fit name style weight]}] - [:a.keyword - {:style - (assoc style - :width (- w 4) - :height (- h 4) - :left (:x fit) - :top (:y fit)) - :class - (str "weight-" (Math/round (* 100 weight)))} - name])))])) + [:div + {:style {:position :relative :width w :height h}} + (->> words + (map + (fn [{:keys [keyword weight]}] + (let [font-size (-> weight (* 80) (str "px"))] + (-> keyword + (text-dimensions-memoized font-size) + ; padding + (update :w + (partial + 8)) + ; margin + (update :w + (partial + 4)) + (update :h + (partial + 4)) + ; style + (assoc :font-size font-size) + (assoc :weight weight))))) + (doall) + (fit w h) + (map-subels + (fn [{:strs [w h fit name font-size weight]}] + [:a.keyword + {:style + {:fontSize font-size + :width (- w 4) + :height (- h 4) + :left (get fit "x") + :top (get fit "y")} + :class + (str "weight-" (Math/round (* 100 weight)))} + name])))]) + +(def fit-words-memoized + (memoize fit-words)) diff --git a/src/cljs/ulysses/utils.cljs b/src/cljs/ulysses/utils.cljs index 81c08f5..51c6817 100644 --- a/src/cljs/ulysses/utils.cljs +++ b/src/cljs/ulysses/utils.cljs @@ -43,6 +43,7 @@ "print x, then return it. useful for debugging inside threading macros or map" [x] + (println (new js/Date)) (cljs.pprint/pprint x) x) diff --git a/src/js/extra.js b/src/js/extra.js index d46b0bf..df99f3d 100644 --- a/src/js/extra.js +++ b/src/js/extra.js @@ -87,64 +87,52 @@ ulysses.extra = (function() { /** * Get the dimensions of a piece of text - * textDimensions('hello world!', - * { fontFamily: 'Arial', fontSize: '16px', fontWeight: 'normal'}) + * ex. textDimensions('hello world!', '16px'); */ var textDimensions = (function() { - var createDummyElement = function(name, options) { - var element = document.createElement('div'), - nameNode = document.createTextNode(name); + var element = null; + var nameNode = null; - element.appendChild(nameNode); + var createDummyElement = function(name, fontSize) { + if (!element) { + element = document.createElement('div'); + nameNode = document.createTextNode(name); - 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'; + element.appendChild(nameNode); + + element.style.lineHeight = '1.3'; + element.style.fontFamily = 'Source Sans Pro'; + element.style.fontWeight = '100'; + 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); + } else { + var newNameNode = document.createTextNode(name); + element.replaceChild(newNameNode, nameNode); + nameNode = newNameNode; + } - document.body.appendChild(element); + element.style.fontSize = fontSize; 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 getDomTextDimensions = function(name, fontSize) { + var element = createDummyElement(name, fontSize); - var element = createDummyElement(name, options); - - var result = { + return { w: element.offsetWidth, h: element.offsetHeight, name: name }; - - destroyElement(element); - - cache[cacheKey] = result; - - return result; }; + + return getDomTextDimensions; })(); return { diff --git a/src/sass/components/_word-cloud.scss b/src/sass/components/_word-cloud.scss index 1a7363d..79a22d1 100644 --- a/src/sass/components/_word-cloud.scss +++ b/src/sass/components/_word-cloud.scss @@ -7,6 +7,9 @@ background: $black; color: $white; display: block; + font-family: 'Source Sans Pro', sans-serif; + font-weight: 100; + line-height: 1.3; overflow: hidden; position: absolute; text-align: center;