1 module treemap; 2 3 /++ 4 + The challenge to create a reusable Treemap is how to return the 5 + rects for the nodes. From the top of my hear there are the 6 + following options: 7 + 1. force the client to provide a rect member in the used type. 8 + 2. return an associative array from Rect[Node] -> path taken. 9 + 3. return a new type that holds the node, the rect and childs of 10 + the type. 11 +/ 12 import std.file; 13 import std.stdio; 14 import std.format; 15 import std.conv; 16 import std.algorithm; 17 import std.range; 18 import std.variant; 19 20 /++ 21 + Rect struct used to store the positions of the Nodes in the 22 + treemap. The position is always relative to the initial rect. 23 +/ 24 struct Rect { 25 double x; 26 double y; 27 double width; 28 double height; 29 this(double x, double y, double width, double height) { 30 this.x = x; 31 this.y = y; 32 this.width = width; 33 this.height = height; 34 } 35 double left() { 36 return x; 37 } 38 double right() { 39 return x+width; 40 } 41 double top() { 42 return y; 43 } 44 double bottom() { 45 return y+height; 46 } 47 bool contains(double x, double y) { 48 return x >= this.x && x < right() && 49 y >= this.y && y < bottom(); 50 } 51 } 52 53 @("Rect.basics") 54 unittest { 55 import unit_threaded; 56 auto r = Rect(10, 11, 20, 30); 57 r.contains(15, 15).shouldEqual(true); 58 r.left.shouldEqual(10); 59 r.top.shouldEqual(11); 60 r.right.shouldEqual(30); 61 r.bottom.shouldEqual(41); 62 } 63 64 template TreeMap(Node) { 65 double size(Node[] nodes) { 66 return nodes.map!(v => v.size).sum; 67 } 68 69 /++ 70 + A Treemap is a compact two dimensional representation of the 71 + "size" of Nodes to each other. 72 + Each Node has a size that is used to determine how much space is 73 + reserved for this Node when laying the Treemap out on a Rect. 74 +/ 75 class TreeMap { 76 Rect[Node] treeMap; 77 Node rootNode; 78 this(Node root) { 79 this.rootNode = root; 80 } 81 82 /++ 83 + Layouts the treemap for a Rect. 84 +/ 85 TreeMap layout(Rect rect) { 86 layout(rootNode.childs, rootNode.size, rect); 87 return this; 88 } 89 90 alias Maybe = Algebraic!(Node, typeof(null)); 91 /++ 92 + Find the Node for a position. 93 + @return Maybe!Node 94 + @see Unittest on how to use it. 95 +/ 96 Maybe findFor(double x, double y) { 97 foreach (kv; treeMap.byKeyValue()) { 98 auto fileNode = kv.key; 99 auto rect = kv.value; 100 if (rect.contains(x, y)) { 101 Maybe res = fileNode; 102 return res; 103 } 104 } 105 Maybe res = null; 106 return res; 107 } 108 109 /++ 110 + @param n which rect to return. 111 + @return the Rect of the given Node. 112 +/ 113 Rect get(Node n) { 114 return treeMap[n]; 115 } 116 117 private void layout(Node[] childs, double size, Rect rect) { 118 Row row = Row(rect, size); 119 Node[] rest = childs; 120 while (rest.length > 0) { 121 Node child = rest.front(); 122 Row newRow = row.add(child); 123 124 if (newRow.worstAspectRatio > row.worstAspectRatio) { 125 layout(rest, rest.size(), row.imprint(treeMap)); 126 return; 127 } 128 129 row = newRow; 130 rest.popFront(); 131 } 132 row.imprint(treeMap); 133 } 134 135 /++ 136 + A Row collects childnodes and provides means to layout them. 137 + To find the best rectangular treemap layout it also can find 138 + the child with the worst aspect ratio. The layouting performed in 139 + TreeMap is a two step process: 140 + - first the best row to fill the area is searched (this is done 141 + incrementally child Node by child Node. 142 + - second the found Row is imprinted (which means the layout 143 + coordinates are added to the child Nodes). 144 +/ 145 private struct Row { 146 /// the total area that the row could take 147 Rect rect; 148 /// the total size that corresponds to the total area 149 double size; 150 151 double fixedLength; 152 double variableLength; 153 154 Node[] childs; 155 double worstAspectRatio; 156 double area; 157 158 public this(Rect rect, double size) { 159 this.rect = rect; 160 this.size = size; 161 this.fixedLength = min(rect.width, rect.height); 162 this.variableLength = 0; 163 this.worstAspectRatio = double.max; 164 this.area = 0; 165 } 166 167 private static double aspectRatio(double size, double sharedLength, double l2) { 168 double l1 = sharedLength * size; 169 return max(l1/l2, l2/l1); 170 } 171 172 public this(Rect rect, Node[] childs, double size) { 173 this(rect, size); 174 this.childs = childs; 175 double sizeOfAllChilds = childs.size(); 176 double percentageOfTotalArea = sizeOfAllChilds / size; 177 this.variableLength = max(rect.width, rect.height) * percentageOfTotalArea; 178 double height = min(rect.width, rect.height); 179 this.worstAspectRatio = childs.map!(n => aspectRatio(n.size, height / sizeOfAllChilds, variableLength)).reduce!max; 180 } 181 182 public Row add(Node n) { 183 Node[] tmp = childs ~ n; 184 return Row(rect, childs~n, size); 185 } 186 187 public Rect imprint(ref Rect[Node] treemap) { 188 if (rect.height < rect.width) { 189 return imprintLeft(treemap); 190 } else { 191 return imprintTop(treemap); 192 } 193 } 194 195 private Rect imprintLeft(ref Rect[Node] treemap) { 196 double offset = 0; 197 foreach (child; childs) { 198 double percentage = child.size / childs.size(); 199 double height = percentage * rect.height; 200 treemap[child] = Rect(rect.x, rect.y+offset, variableLength, height); 201 offset += height; 202 } 203 return Rect(rect.x+variableLength, rect.y, rect.width-variableLength, rect.height); 204 } 205 206 private Rect imprintTop(ref Rect[Node] treemap) { 207 double offset = 0; 208 foreach(child; childs) { 209 double percentage = child.size / childs.size(); 210 double width = percentage * rect.width; 211 treemap[child] = Rect(rect.x+offset, rect.y+0, width, variableLength); 212 offset += width; 213 } 214 return Rect(rect.x, rect.y+variableLength, rect.width, rect.height-variableLength); 215 } 216 } 217 } 218 } 219 220 @("Treemap") 221 unittest { 222 class Node { 223 double size; 224 Node[] childs; 225 this(double size) { 226 this.size = size; 227 } 228 this(Node[] childs) { 229 this.childs = childs; 230 this.size = childs.map!(v => v.size).sum; 231 } 232 } 233 234 import std.math : approxEqual; 235 import std.algorithm : equal; 236 import unit_threaded; 237 238 void shouldEqual2(Rect r1, Rect r2) { 239 r1.x.shouldEqual(r2.x); 240 r1.y.shouldEqual(r2.y); 241 r1.width.shouldEqual(r2.width); 242 r1.height.shouldEqual(r2.height); 243 } 244 245 auto childs = [ 6, 6, 4, 3, 2, 2, 1 ].map!(v => new Node(v)).array; 246 auto n = new Node(childs); 247 auto res = new TreeMap!(Node)(n).layout(Rect(0, 0, 600, 400)); 248 void check(int idx, Rect r) { 249 shouldEqual2(res.get(childs[idx]), r); 250 } 251 check(0, Rect(0, 0, 300, 200)); 252 check(1, Rect(0, 200, 300, 200)); 253 254 check(2, Rect(300, 0, 171, 233)); 255 check(3, Rect(471, 0, 129, 233)); 256 257 check(4, Rect(300, 233, 120, 166)); 258 check(5, Rect(420, 233, 120, 166)); 259 check(6, Rect(540, 233, 60, 166)); 260 261 auto found = res.findFor(1, 1); 262 found.tryVisit!( 263 (Node n) {}, 264 () {assert(false);} 265 )(); 266 auto notFound = res.findFor(-1, -1); 267 notFound.tryVisit!( 268 (typeof(null) n) {}, 269 () {assert(false);} 270 )(); 271 }