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 }