       [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) only contain keys from cols map;
-  ; (B) resolves nested values for path keys (e.g. replaces milestone object with title if key is [:milestone :title])
-  ; (C) values are all strings
-  ; (D) entries are in same order as cols entries
+  ; (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 (comp
-                          #(into (sorted-map-by (partial idx-comparator topkeys)) %)
-                          ;#(into (sorted-map) %)
-                          #(fmap str %)
-                          #(map (partial resolve-nested-val cols) %)
-                          #(select-keys % topkeys))
-        rows (map transform-issue issues)]
+        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:

   [\"Bar Widget\" 200]]"
  (let [cols columns
        rows (issues->rows cols issues)
        data (into [(vals cols)] (map vals rows)) ]
    (println "---------------")
    (clojure.pprint/pprint data)
    data))

(defn issues->wb [proj issues]
  "Creates a spreadsheet containing data from issues"

(ns labradoodle.excel
  (:use [dk.ative.docjure.spreadsheet])
  (:require [clojure.string :as s :refer [trim join split]])
  (:import (clojure.lang ISeq)))

;; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
;; Referenced while coding this ns:
