    [\"Bar Widget\" 200]]"
   (let [cols columns
         rows (issues->rows cols issues)
-        data (into [(vals cols)] (map vals rows)) ]
+        groups (group-rows groups rows)
+        data (into [(vals cols)] (map vals groups)) ]
     (println "---------------")
     (clojure.pprint/pprint data)
     data))
+
+
+;; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+;; Workbook Manipulation fuctions
+;; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 (defn issues->wb [proj issues]
   "Creates a spreadsheet containing data from issues"

;;  - http://stackoverflow.com/questions/1676891/mapping-a-function-on-the-values-of-a-map-in-clojure
;; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

(def columns (array-map :title "Task", :state "Status", [:milestone :title] "Milestone", [:assignee :name] "Assigned To"))

(defn fmap [f m]
  "Based on corresponding multi-method in https://github.com/clojure/algo.generic"

      [k (get-in v (ks->path colk) v)]
      entry)))

(defn render [v]
  "Renders value based on type"
  (cond
    (instance? ISeq v) (join ", " v)
    :else (str v)))

(defn issues->rows [cols issues]
  "Produces rows array, with shape like this:
  [{:name \"Foo Widget\" :price 100}
   {:name \"Bar Widget\" :price 200}]"
  ; Create rows by transforming  issue objects as follows:
  ; (A) add placeholder key for group column
  ; (B) only contain keys from cols map;
  ; (C) resolves nested values for path keys (e.g. replaces milestone object with title if key is [:milestone :title])
  ; (D) render values (by default converts to strings)
  ; (E) entries are in same order as cols entries
  (let [topkeys (cols->topkeys cols)
        transform-issue-fn (comp
                             #(into (sorted-map-by (partial idx-comparator topkeys)) %)
                             ;#(into (sorted-map) %)
                             #(fmap render %)
                             #(map (partial resolve-nested-val cols) %)
                             #(select-keys % topkeys)
                             #(merge % {:group ""}))
        rows (map transform-issue-fn issues)]
    (println "---------------")
    (clojure.pprint/pprint rows)
    rows))

(defn any-label? [labels row]
  "return true if one of row :labels contains any items from labels set, otherwise return false"
  (let [row-labels (map trim (split (:labels row) #","))]
    (some labels row-labels)))

(defn has-label? [label row]
  "return true if one of row :labels is label, otherwise return false"
  (any-label? #{label} row))

(defn group-rows [groups rows]
  "Transforms rows vector into new vector with rows re-arranged (and possibly duplicated)
  per groups definition. Each group of rows is demarcated by a group-header-row containing
  a single group-header object."
  (let [reduce-fn (fn [grouped-rows {:keys [label display :as group]}]
                    (let [group-header-row (vector {:group display})
                          group-rows (filter #(has-label? label %) rows)]
                      (-> grouped-rows
                          (into group-header-row)
                          (into group-rows))))
        group-labels (set (map :label groups))
        misc (filter #(not (any-label? group-labels %)) rows)]
    (-> (reduce reduce-fn [] groups)
        (into (vector {:group "Misc"}))
        (into misc))))

(defn group-fixed-rows [rows]
  "Groups rows by labels: dev, ops, demo, docs. Include any ungrouped rows under a misc group."
  {:deprecated "This fn hardcodes the groups; use the more flexible group-rows fn instead."}
  (let [dev (filter #(has-label? "dev" %) rows)
        ops (filter #(has-label? "ops" %) rows)
        demo (filter #(has-label? "demo" %) rows)
        docs (filter #(has-label? "docs" %) rows)
        misc (filter #(not (any-label? #{"dev", "ops", "demo", "docs"} %)) rows)]
    (-> []
        (into (vector {:group "Development"}))
        (into dev)
        (into (vector {:group "Ops"}))
        (into ops)
        (into (vector {:group "Demo Prep"}))
        (into demo)
        (into (vector {:group "Documentation"}))
        (into docs)
        (into (vector {:group "Misc"}))
        (into misc))))

(defn issues->celldata [issues]
  "Create celldata array, with shape like this:
