/
Boxmaptile.jl
181 lines (155 loc) · 4.65 KB
/
Boxmaptile.jl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
struct BoxmapTile
x::Float64
y::Float64
w::Float64
h::Float64
end
"""
buildrow(A, x, y, w, h)
Make a row of tiles from A.
"""
function buildrow(A, x, y, w, h)
covered = sum(A)
width = covered/h
tiles = BoxmapTile[]
for value in A
push!(tiles, BoxmapTile(x, y, width, value/width))
y += value/width
end
return tiles
end
"""
buildcolumn(A, x, y, w, h)
Make a column of tiles from A.
"""
function buildcolumn(A, x, y, w, h)
covered = sum(A)
height = covered/w
tiles = BoxmapTile[]
for value in A
push!(tiles, BoxmapTile(x, y, value/height, height))
x += value/height
end
return tiles
end
"""
layout(A, x, y, w, h)
From A, make a row of tiles (if wider than tall) or a column of tiles (if taller than wide).
"""
function layout(A, x, y, w, h)
if w >= h
return buildrow(A, x, y, w, h)
else
return buildcolumn(A, x, y, w, h)
end
end
function rowlayoutleftover(A, x, y, w, h)
covered = sum(A)
width = covered/h
layoutleftoverx = x + width
layoutleftoverw = w - width
return layoutleftoverx, y, layoutleftoverw, h
end
function columnlayoutleftover(A, x, y, w, h)
covered = sum(A)
height = covered/w
layoutleftovery = y + height
layoutleftoverh = h - height
return x, layoutleftovery, w, layoutleftoverh
end
function layoutleftover(A, x, y, w, h)
if w >= h
return rowlayoutleftover(A, x, y, w, h)
else
return columnlayoutleftover(A, x, y, w, h)
end
end
"""
highestaspectratio()
Find the highest aspect ratio of a list of rectangles, given the length
of the side along which they are to be laid out.
"""
function highestaspectratio(A, x, y, w, h)
return maximum([max(tile.w/tile.h, tile.h/tile.w) for tile in layout(A, x, y, w, h)])
end
function buildboxmap(A, x, y, w, h)
length(A) == 0 && return []
length(A) == 1 && return layout(A, x, y, w, h)
i = 1
while i < length(A) &&
(highestaspectratio(A[1:i], x, y, w, h) >= highestaspectratio(A[1:i+1], x, y, w, h))
i += 1
end
layoutleftoverx, layoutleftovery, layoutleftoverw, layoutleftoverh = layoutleftover(A[1:i], x, y, w, h)
return append!(layout(A[1:i], x, y, w, h), buildboxmap(A[i+1:end], layoutleftoverx, layoutleftovery, layoutleftoverw, layoutleftoverh))
end
"""
boxmap(A::Array, pt, w, h)
Build a box map of the values in `A` with one corner at `pt` and width `w` and
height `h`. There are `length(A)` boxes. The areas of the boxes are proportional
to the original values, scaled as necessary.
The return value is an array of BoxmapTiles. For example:
```
[BoxmapTile(0.0, 0.0, 10.0, 20.0)
BoxmapTile(10.0, 0.0, 10.0, 13.3333)
BoxmapTile(10.0, 13.3333, 10.0, 6.66667)]
```
with each tile containing `(x, y, w, h)`. `box()` and `BoundingBox()` can work with
BoxmapTiles as well.
# Example
```
using Luxor
@svg begin
fontsize(16)
fontface("HelveticaBold")
pt = Point(-200, -200)
a = rand(10:200, 15)
tiles = boxmap(a, Point(-200, -200), 400, 400)
for (n, t) in enumerate(tiles)
randomhue()
bb = BoundingBox(t)
box(bb - 2, :stroke)
box(bb - 5, :fill)
sethue("white")
text(string(n), midpoint(bb[1], bb[2]), halign=:center)
end
end 400 400 "boxmap.svg"
```
"""
function boxmap(A::Array, pt::Point, w, h)
# algorithm basically from Bruls, Huizing, van Wijk, "Squarified Treemaps"
# without the silly name
!all(n -> n > 0, A) && error("all values must be positive")
sort!(A, rev=true)
totalvalue = sum(A)
totalarea = w * h
normalizedA = A .* (totalarea/totalvalue)
return buildboxmap(normalizedA, pt.x, pt.y, w, h)
end
"""
box(tile::BoxmapTile, action::Symbol=:none; vertices=false)
box(tile::BoxmapTile, action=:none, vertices=false)
Use a Boxmaptile to make or draw a rectangular box. Use `vertices=true` to obtain
the coordinates.
Create boxmaps using `boxmap()`.
"""
function box(tile::BoxmapTile, action::Symbol; vertices=false)
if vertices
return [Point(tile.x, tile.y + tile.h),
Point(tile.x, tile.y),
Point(tile.x + tile.w, tile.y),
Point(tile.x + tile.w, tile.y + tile.h)]
end
rect(tile.x, tile.y, tile.w, tile.h, action)
end
box(tile::BoxmapTile; action=:none, vertices=false) =
box(tile, action, vertices=vertices)
"""
BoundingBox(tile::BoxmapTile)
Return a BoundingBox of a BoxmapTile (as created with `boxmap()`).
"""
function BoundingBox(tile::BoxmapTile)
lcorner = Point(tile.x, tile.y)
ocorner = Point(lcorner.x + tile.w, lcorner.y + tile.h)
return BoundingBox(lcorner, ocorner)
end