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.variant; 16 import std.conv; 17 import std.algorithm; 18 import std.range; 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 + expands the rect with a border. 53 + the width may also be smaller than 0 then its a shrink. 54 +/ 55 Rect expand(int delta) { 56 return Rect(x-delta, y-delta, width+2*delta, height+2*delta); 57 } 58 bool empty() { 59 return width <= 0 && height <= 0; 60 } 61 } 62 63 @("Rect.basics") 64 unittest { 65 auto r = Rect(10, 11, 20, 30); 66 assert(true == r.contains(15, 15)); 67 assert(10 == r.left); 68 assert(11 == r.top); 69 assert(30 == r.right); 70 assert(41 == r.bottom); 71 } 72 73 template TreeMap(Node) { 74 double weight(Node[] nodes) { 75 return nodes.map!(v => v.weight).sum; 76 } 77 78 /++ 79 + A Treemap is a compact two dimensional representation of the 80 + "weigth" of Nodes to each other. 81 + Each Node has a weight that is used to determine how much space is 82 + reserved for this Node when laying the Treemap out on a Rect. 83 +/ 84 class TreeMap { 85 Rect[Node] treeMap; 86 Node rootNode; 87 int delta; 88 this(Node root, int delta=0) { 89 this.rootNode = root; 90 this.delta = delta; 91 } 92 93 /++ 94 + Layouts the treemap for a Rect. 95 +/ 96 TreeMap layout(Rect rect, int depth=3) { 97 treeMap[rootNode] = rect; 98 layout(rootNode.childs, rootNode.weight, rect.expand(delta), depth); 99 return this; 100 } 101 102 alias Maybe = Algebraic!(Node, typeof(null)); 103 104 105 /++ 106 + Finds the deepest Node that contains the given position. 107 + @return Maybe!Node 108 + @see Unittest on how to use it. 109 +/ 110 Maybe findFor(double x, double y) { 111 return findFor(x, y, rootNode); 112 } 113 114 Maybe findFor(double x, double y, Node n) { 115 auto r = n in treeMap; 116 if (!r || !((*r).contains(x, y))) { 117 return Maybe(null); 118 } 119 120 foreach (child; n.childs) { 121 auto h = findFor(x, y, child); 122 if (h.type == typeid(Node)) { 123 return Maybe(h); 124 } 125 } 126 return Maybe(n); 127 } 128 129 /++ 130 + @param n which rect to return. 131 + @return the Rect of the given Node. 132 +/ 133 Rect* get(Node n) { 134 return n in treeMap; 135 } 136 137 private void layout(Node[] childs, double weight, Rect rect, int depth) { 138 if (rect.empty()) { 139 return; 140 } 141 142 Row row = Row(rect, weight); 143 Node[] rest = childs; 144 while (rest.length > 0) { 145 Node child = rest.front(); 146 Row newRow = row.add(child); 147 148 if (newRow.worstAspectRatio > row.worstAspectRatio) { 149 auto h = row.imprint(treeMap); 150 if (depth > 1) { 151 foreach (rowChild; row.childs) { 152 layout(rowChild.childs, rowChild.weight, treeMap[rowChild].expand(delta), depth-1); 153 } 154 } 155 layout(rest, rest.weight(), h, depth); 156 return; 157 } 158 159 row = newRow; 160 rest.popFront(); 161 } 162 row.imprint(treeMap); 163 if (depth > 1) { 164 foreach (rowChild; row.childs) { 165 layout(rowChild.childs, rowChild.weight, treeMap[rowChild].expand(delta), depth-1); 166 } 167 } 168 } 169 170 /++ 171 + A Row collects childnodes and provides means to layout them. 172 + To find the best rectangular treemap layout it also can find 173 + the child with the worst aspect ratio. The layouting performed in 174 + TreeMap is a two step process: 175 + - first the best row to fill the area is searched (this is done 176 + incrementally child Node by child Node. 177 + - second the found Row is imprinted (which means the layout 178 + coordinates are added to the child Nodes). 179 +/ 180 private struct Row { 181 /// the total area that the row could take 182 Rect rect; 183 /// the total weight that corresponds to the total area 184 double weight; 185 186 double fixedLength; 187 double variableLength; 188 189 Node[] childs; 190 double worstAspectRatio; 191 double area; 192 193 public this(Rect rect, double weigth) { 194 this.rect = rect; 195 this.weight = weigth; 196 this.fixedLength = min(rect.width, rect.height); 197 this.variableLength = 0; 198 this.worstAspectRatio = double.max; 199 this.area = 0; 200 } 201 202 private static double aspectRatio(double weight, double sharedLength, double l2) { 203 double l1 = sharedLength * weight; 204 return max(l1/l2, l2/l1); 205 } 206 207 public this(Rect rect, Node[] childs, double weight) { 208 this(rect, weight); 209 this.childs = childs; 210 double weightOfAllChilds = childs.weight(); 211 double percentageOfTotalArea = weightOfAllChilds / weight; 212 this.variableLength = max(rect.width, rect.height) * percentageOfTotalArea; 213 double height = min(rect.width, rect.height); 214 this.worstAspectRatio = childs.map!(n => aspectRatio(n.weight, height / weightOfAllChilds, variableLength)).reduce!max; 215 } 216 217 public Row add(Node n) { 218 Node[] tmp = childs ~ n; 219 return Row(rect, childs~n, weight); 220 } 221 222 /++ 223 + adds rect for all nodes in the row to the treemap. 224 + returns the unused rect 225 +/ 226 public Rect imprint(ref Rect[Node] treemap) { 227 if (rect.height < rect.width) { 228 return imprintLeft(treemap); 229 } else { 230 return imprintTop(treemap); 231 } 232 } 233 234 private Rect imprintLeft(ref Rect[Node] treemap) { 235 double offset = 0; 236 foreach (child; childs) { 237 double percentage = child.weight / childs.weight(); 238 double height = percentage * rect.height; 239 treemap[child] = Rect(rect.x, rect.y+offset, variableLength, height); 240 offset += height; 241 } 242 return Rect(rect.x+variableLength, rect.y, rect.width-variableLength, rect.height); 243 } 244 245 private Rect imprintTop(ref Rect[Node] treemap) { 246 double offset = 0; 247 foreach(child; childs) { 248 double percentage = child.weight / childs.weight(); 249 double width = percentage * rect.width; 250 treemap[child] = Rect(rect.x+offset, rect.y+0, width, variableLength); 251 offset += width; 252 } 253 return Rect(rect.x, rect.y+variableLength, rect.width, rect.height-variableLength); 254 } 255 } 256 } 257 } 258 259 @("Treemap") 260 unittest { 261 class Node { 262 double weight; 263 Node[] childs; 264 this(double weight) { 265 this.weight = weight; 266 } 267 this(Node[] childs) { 268 this.childs = childs; 269 this.weight = childs.map!(v => v.weight).sum; 270 } 271 } 272 273 import std.math : approxEqual; 274 import std.algorithm : equal; 275 276 void shouldEqual2(Rect r1, Rect r2) { 277 assert(approxEqual(r2.x, r1.x)); 278 assert(approxEqual(r2.y, r1.y)); 279 assert(approxEqual(r2.width, r1.width)); 280 assert(approxEqual(r2.height, r1.height)); 281 } 282 283 auto childs = [ 6, 6, 4, 3, 2, 2, 1 ].map!(v => new Node(v)).array; 284 auto n = new Node(childs); 285 auto res = new TreeMap!(Node)(n).layout(Rect(0, 0, 600, 400)); 286 void check(int idx, Rect r) { 287 shouldEqual2(*res.get(childs[idx]), r); 288 } 289 check(0, Rect(0, 0, 300, 200)); 290 check(1, Rect(0, 200, 300, 200)); 291 292 check(2, Rect(300, 0, 171, 233)); 293 check(3, Rect(471, 0, 129, 233)); 294 295 check(4, Rect(300, 233, 120, 166)); 296 check(5, Rect(420, 233, 120, 166)); 297 check(6, Rect(540, 233, 60, 166)); 298 299 res.findFor(1, 1).visit!( 300 (Node n) {}, 301 (typeof(null)) {assert(false);} 302 ); 303 res.findFor(-1, -1).visit!( 304 (Node n) {assert(false);}, 305 (typeof(null)) {} 306 ); 307 }