Skip to content

Commit 25783a4

Browse files
committed
Allow grouping categories in packing charts.
The ability to express hierarchy is a useful feature. We technically could allow multiple levels of nesting but it can result in a confusing chart.
1 parent 321b4ef commit 25783a4

3 files changed

Lines changed: 104 additions & 17 deletions

File tree

spork/charts.janet

Lines changed: 85 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
### dependencies is very useful to have.
1111
###
1212
### Data is passed to most charts as a "data-frame", which is a table mapping keyword (or any Janet value) column names
13-
### to arrays of data points, usually numbers.
13+
### to arrays of data points, usually numbers. The columns of a dataframe are considered to be the sorted keys of table or struct.
14+
### Many visualizations will infer the X column as the "first" column and the Y column as the second column.
1415
###
1516
### Data frame example:
1617
###
@@ -1591,7 +1592,41 @@
15911592
(def xs @[])
15921593
(def ys @[])
15931594
(eachp [k v] x (array/push xs k) (array/push ys v))
1594-
@{:x xs :y ys})
1595+
{:x xs :y ys})
1596+
1597+
(defn- convert-to-nested
1598+
"Convert the dataframes to a nested representation, such that the label-column
1599+
is used to group rows. The grouped rows will be combined into a sub-dataframe that lives
1600+
inside another cell in the main dataframe. This is an unnatural representation but is convenient
1601+
for rendering packing charts with hierarchical data."
1602+
[df x-column area-column label-column]
1603+
(def df-columns (sort (keys df)))
1604+
(def len (length (get df area-column)))
1605+
(defn append-row [to i] (each col df-columns (array/push (get to col) (get (get df col) i))) to)
1606+
(defn make-new [] (let [ndf @{}] (each col df-columns (put ndf col @[])) ndf))
1607+
(def new-df (make-new))
1608+
(def cat-data (assert (or (get df label-column) (array/new-filled len nil))))
1609+
(def nested-dfs @{})
1610+
# Iterate over the rows and extract/remove rows with a non-nil label
1611+
(for i 0 len
1612+
(def label-category (get cat-data i))
1613+
(if (= nil label-category)
1614+
(append-row new-df i)
1615+
(let [sub-df (or (get nested-dfs label-category) (set (nested-dfs label-category) (make-new)))]
1616+
(append-row sub-df i))))
1617+
# Add the nested dataframes back to our copy of the original data-frame with the groupings removed.
1618+
(eachp [cat-label nested-df] nested-dfs
1619+
(def summed-area (sum (get nested-df area-column)))
1620+
(put nested-df label-column nil)
1621+
(each col df-columns
1622+
(def value-array (get new-df col))
1623+
(array/push value-array
1624+
(case col
1625+
x-column cat-label
1626+
area-column summed-area
1627+
label-column nested-df
1628+
nil))))
1629+
new-df)
15951630

15961631
(defn plot-packing-chart
15971632
```
@@ -1605,14 +1640,16 @@
16051640
* :height - (if no canvas provided) - make a new canvas with the given height in pixels
16061641
* :color-map - a color map keyword or function used to map numbers in the range [0, 1] to a color.
16071642
1643+
Data Table Input (quick and easy method):
1644+
* :data-map - A table or struct that maps keys as categories to values as proportional rectangle areas.
1645+
16081646
Data Frame Input:
1609-
* :data - a dataframe table that contains a grid of cell
1647+
* :data - a data-frame table that contains a grid of cell
16101648
* :x-column - a column name to use a the category identifiers. Defaults to the first column.
16111649
* :y-column - a column name to use for the area quantities. Defaults to the second column
1612-
* :c-column - a column name to use for color grading. Defaults to the same as the y-column, but mapped to a range from 0 to 1.
1613-
1614-
Data Table Input:
1615-
* :data-map - A table or struct that maps keys as categories to values as proportional rectangle areas.
1650+
* :c-column - a column name to use for color grading. Defaults to the same as the y-column, but mapped to a range from 0 to 1, such that
1651+
the minimum value is a color paramter of 0 and the maximum has a color parameter of 1.
1652+
* :group-column - a column name that contains a label for grouping areas. Optional.
16161653
16171654
Layout Parameters:
16181655
* :omega - a number between 0 and 1 used to decide how to split rectangular areas. The default is 0.5
@@ -1630,7 +1667,7 @@
16301667
[&named
16311668
canvas width height
16321669
data-map
1633-
data x-column y-column c-column
1670+
data x-column y-column c-column group-column
16341671
padding inner-padding
16351672
background-color
16361673
text-color
@@ -1640,7 +1677,6 @@
16401677
sort-bins]
16411678

16421679
(def [canvas canvas-w canvas-h] :shadow (canvas-and-dimensions canvas width height))
1643-
(def canvas (g/blank canvas-w canvas-h 4))
16441680
(default padding 2)
16451681
(default inner-padding 2)
16461682
(default background-color (dyn *background-color* default-background-color))
@@ -1654,7 +1690,6 @@
16541690
(def skeys (sort (keys data)))
16551691
(default x-column (first skeys))
16561692
(default y-column (get skeys 1))
1657-
(def xs (assert (get data x-column)))
16581693
(def ys (assert (get data y-column)))
16591694

16601695
# Allow a custom color column, but use the y-column by default with a reasonable color ramp.
@@ -1663,13 +1698,46 @@
16631698
(def value-range (- max-value min-value))
16641699
(def color-ramp-slope (cond c-column 1 (not= 0 value-range) (/ value-range) 0))
16651700
(def color-ramp-offset (cond c-column 0 (not= 0 value-range) (- (/ min-value value-range)) 0.5))
1666-
(default c-column y-column)
1667-
(def cs (assert (get data c-column)))
16681701

1669-
# Use zipped data for sorting
1670-
(def zipped-data (map tuple xs ys cs))
1702+
# Convert to a nested representation if we have a nested column
1703+
(def data :shadow (if group-column (convert-to-nested data x-column y-column group-column) data))
1704+
(def xs (assert (get data x-column)))
1705+
(def ys :shadow (assert (get data y-column)))
1706+
(def ns (or (if group-column (get data group-column)) (array/new-filled (length xs) nil)))
1707+
(def cs (assert (get data (or c-column y-column))))
16711708

1672-
(var custom-draw nil)
1709+
# Use zipped data for sorting
1710+
(def zipped-data (map tuple xs ys cs ns))
1711+
1712+
(defn- do-subgroup
1713+
[x y w h lab children]
1714+
(def text (string lab))
1715+
(def [tw th] (text-measure text font))
1716+
(def tscale 1)
1717+
(def tcolor (or text-color (if (< 0.6 (color-value background-color)) g/black g/white))) # black or white, maximizing contrast
1718+
(text-draw canvas (+ x (div (- w tw) 2)) (+ y inner-padding)
1719+
text tcolor font tscale 0)
1720+
(def h :shadow (- h th inner-padding))
1721+
(def y :shadow (+ y th inner-padding))
1722+
# Recurse on new view port
1723+
(def group-padding (* 2 inner-padding))
1724+
(def view (g/viewport canvas (+ group-padding x) y (- w (* 2 group-padding)) (- h group-padding)))
1725+
(plot-packing-chart :canvas view
1726+
:x-column x-column
1727+
:y-column y-column
1728+
:c-column c-column
1729+
:group-column group-column
1730+
:data children # children-as-data
1731+
:color-map color-map
1732+
:sort-bins sort-bins
1733+
:font font
1734+
:background-color background-color
1735+
:inner-padding inner-padding
1736+
:padding padding
1737+
:omega omega
1738+
:no-text-resize no-text-resize
1739+
:text-color text-color)
1740+
nil)
16731741

16741742
# Preprocess data
16751743
(each y ys (assert (>= y 0) "cannot have area measurements less than 0"))
@@ -1687,8 +1755,8 @@
16871755
# Leaf cases
16881756
(when (empty? categories) (break))
16891757
(when (= 1 (length categories))
1690-
(def [cat area colort] (first categories))
1691-
(when custom-draw (break (custom-draw x y w h cat area colort)))
1758+
(def [cat area colort children] (first categories))
1759+
(when (dictionary? children) (break (do-subgroup x y w h cat children)))
16921760
(def text (string cat))
16931761
(def t (+ color-ramp-offset (* color-ramp-slope colort)))
16941762
(def color (cmap t cat))

test/gold/packing-chart-nested.png

8.66 KB
Loading

test/suite-gfx2d.janet

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -605,4 +605,23 @@
605605

606606
(test-packing-chart-custom-color)
607607

608+
(defn test-packing-chart-nested
609+
[]
610+
(def p "Big Pig")
611+
(def c "Little Chicken")
612+
(def w "Medium Cow")
613+
(def o "Other")
614+
(def df2
615+
{:x (range 1 16)
616+
:y (range 1 16)
617+
:group [o w w w o p p p p o o c c c c]})
618+
(def c (charts/plot-packing-chart :data df2
619+
:color-map :turbo
620+
:font :tall
621+
:x-column :x :y-column :y
622+
:group-column :group :width 400 :height 400))
623+
(check-image c "packing-chart-nested.png"))
624+
625+
(test-packing-chart-nested)
626+
608627
(end-suite)

0 commit comments

Comments
 (0)