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 }