OLD | NEW |
| (Empty) |
1 /* | |
2 * Copyright 2015 Google Inc. All rights reserved. | |
3 * | |
4 * Use of this source code is governed by a BSD-style | |
5 * license that can be found in the LICENSE file or at | |
6 * https://developers.google.com/open-source/licenses/bsd | |
7 */ | |
8 part of charted.layout; | |
9 | |
10 /// PaddingFunction takes a node and generates the padding for the particular | |
11 /// node | |
12 typedef List PaddingFunction(TreeMapNode node); | |
13 | |
14 /** | |
15 * Utility layout class which recursively subdivides area into rectangles which | |
16 * can be used to quickly visualize the size of any node in the tree. | |
17 */ | |
18 class TreeMapLayout extends HierarchyLayout { | |
19 /// Rectangular subdivision; squareness controlled via the target ratio. | |
20 static const TREEMAP_LAYOUT_SQUARIFY = 0; | |
21 | |
22 /// Horizontal subdivision. | |
23 static const TREEMAP_LAYOUT_SLICE= 1; | |
24 | |
25 /// Vertical subdivision. | |
26 static const TREEMAP_LAYOUT_DICE = 2; | |
27 | |
28 /// Alternating between horizontal and vertical subdivision. | |
29 static const TREEMAP_LAYOUT_SLICE_DICE = 3; | |
30 | |
31 static const _DEFAULT_PADDING = const [0, 0, 0, 0]; | |
32 | |
33 /// A sticky treemap layout will preserve the relative arrangement of nodes | |
34 /// across transitions. (not yet implemented) | |
35 bool _sticky = false; | |
36 | |
37 /// The available layout size to the specified two-element array of numbers | |
38 /// representing width and height. | |
39 List size = [1, 1]; | |
40 | |
41 /// The mode to layout the Treemap. | |
42 int mode = TREEMAP_LAYOUT_SQUARIFY; | |
43 | |
44 /// The ration to scale the Treemap. | |
45 num ratio = .5 * (1 + math.sqrt(5)); | |
46 | |
47 /// The paddingFunction for each node, defaults to return [0, 0, 0, 0]. | |
48 PaddingFunction paddingFunction = (node) => _DEFAULT_PADDING; | |
49 | |
50 /// TODO(midoringo): Implement sticky related feature. | |
51 get sticky => _sticky; | |
52 set sticky (bool sticky) { | |
53 _sticky = sticky; | |
54 } | |
55 | |
56 // TODO (midoringo): handle the sticky case. | |
57 @override | |
58 List<TreeMapNode> layout(List rows, int parentColumn, int labelColumn, | |
59 int valueColumn) { | |
60 var nodes = super.layout(rows, parentColumn, labelColumn, valueColumn); | |
61 var root = nodes[0]; | |
62 root.x = 0; | |
63 root.y = 0; | |
64 root.dx = size.first; | |
65 root.dy = size.last; | |
66 _scale([root], root.dx * root.dy / root.value); | |
67 _squarify(root); | |
68 return nodes; | |
69 } | |
70 | |
71 @override | |
72 TreeMapNode createNode(label, value, depth) { | |
73 return new TreeMapNode() | |
74 ..label = label | |
75 ..value = value | |
76 ..depth = depth; | |
77 } | |
78 | |
79 void _position(List<TreeMapNode> nodes, num length, MutableRect rect, | |
80 bool flush, num area) { | |
81 var x = rect.x; | |
82 var y = rect.y; | |
83 var v = length > 0 ? (area / length).round() : 0; | |
84 if (length == rect.width) { | |
85 if (flush || (v > rect.height)) v = rect.height; | |
86 for (var node in nodes) { | |
87 node.x = x; | |
88 node.y = y; | |
89 node.dy = v; | |
90 x += node.dx = math.min(rect.x + rect.width - x, v > 0 ? | |
91 (node.area / v).round() : 0); | |
92 } | |
93 nodes.last.sticky = true; | |
94 nodes.last.dx += rect.x + rect.width - x; | |
95 rect.y += v; | |
96 rect.height -= v; | |
97 } else { | |
98 if (flush || (v > rect.width)) v = rect.width; | |
99 for (var node in nodes) { | |
100 node.x = x; | |
101 node.y = y; | |
102 node.dx = v; | |
103 y += node.dy = math.min(rect.y + rect.height - y, v > 0 ? | |
104 (node.area / v).round() : 0); | |
105 } | |
106 nodes.last.sticky = false; | |
107 nodes.last.dy += rect.y + rect.height - y; | |
108 rect.x += v; | |
109 rect.width -= v; | |
110 } | |
111 } | |
112 | |
113 /// Applies padding between each nodes. | |
114 MutableRect _treeMapPad(TreeMapNode node, padding) { | |
115 var x = node.x + padding[3]; | |
116 var y = node.y + padding.first; | |
117 var dx = node.dx - padding[1] - padding[3]; | |
118 var dy = node.dy - padding.first - padding[2]; | |
119 if (dx < 0) { | |
120 x += dx / 2; | |
121 dx = 0; | |
122 } | |
123 if (dy < 0) { | |
124 y += dy / 2; | |
125 dy = 0; | |
126 } | |
127 return new MutableRect(x, y, dx, dy); | |
128 } | |
129 | |
130 /// Scales the node base on it's value and the layout area. | |
131 void _scale(List<TreeMapNode> children, var factor) { | |
132 var area; | |
133 for (var child in children) { | |
134 area = child.value * (factor < 0 ? 0 : factor); | |
135 child.area = area <= 0 ? 0 : area; | |
136 } | |
137 } | |
138 | |
139 /// Computes the most amount of area needed to layout the list of nodes. | |
140 num _worst(List<TreeMapNode> nodes, num length, num pArea) { | |
141 var area; | |
142 var rmax = 0; | |
143 var rmin = double.INFINITY; | |
144 for (var node in nodes) { | |
145 area = node.area; | |
146 if (area <= 0) continue; | |
147 if (area < rmin) rmin = area; | |
148 if (area > rmax) rmax = area; | |
149 } | |
150 pArea *= pArea; | |
151 length *= length; | |
152 return (pArea > 0) ? math.max(length * rmax * ratio / pArea, | |
153 pArea / (length * rmin * ratio)) : double.INFINITY; | |
154 } | |
155 | |
156 /// Recursively compute each nodes (and its children nodes) position and size | |
157 /// base on the node's property and layout mode. | |
158 void _squarify(TreeMapNode node) { | |
159 var children = node.children; | |
160 if (children.isNotEmpty) { | |
161 var rect = _treeMapPad(node, paddingFunction(node)); | |
162 List<TreeMapNode> nodes = []; | |
163 var area = 0; | |
164 var remaining = new List.from(children); | |
165 var score, n, | |
166 best = double.INFINITY, | |
167 length = (mode == TREEMAP_LAYOUT_SLICE) ? rect.width : | |
168 (mode == TREEMAP_LAYOUT_DICE) ? rect.height : | |
169 (mode == TREEMAP_LAYOUT_SLICE_DICE) ? (node.depth & 1 == 1) ? | |
170 rect.height : rect.width : math.min(rect.width, rect.height); | |
171 _scale(remaining, rect.width * rect.height / node.value); | |
172 while ((n = remaining.length) > 0) { | |
173 var child = remaining[n - 1]; | |
174 nodes.add(child); | |
175 area += child.area; | |
176 score = _worst(nodes, length, area); | |
177 if (mode != TREEMAP_LAYOUT_SQUARIFY || score <= best) { | |
178 remaining.removeLast(); | |
179 best = score; | |
180 } else { | |
181 area -= nodes.removeLast().area; | |
182 _position(nodes, length, rect, false, area); | |
183 length = math.min(rect.width, rect.height); | |
184 nodes.clear(); | |
185 area = 0; | |
186 best = double.INFINITY; | |
187 } | |
188 } | |
189 if (nodes.isNotEmpty) { | |
190 _position(nodes, length, rect, true, area); | |
191 nodes.clear(); | |
192 area = 0; | |
193 } | |
194 children.forEach(_squarify); | |
195 } | |
196 } | |
197 } | |
198 | |
199 class TreeMapNode extends HierarchyNode { | |
200 /// The minimum x-coordinate of the node position. | |
201 num x = 0; | |
202 | |
203 /// The minimum y-coordinate of the node position. | |
204 num y = 0; | |
205 | |
206 /// The x-extent of the node position. | |
207 num dx = 0; | |
208 | |
209 /// The y-extent of the node position. | |
210 num dy = 0; | |
211 | |
212 /// The area the node should take up. | |
213 num area = 0; | |
214 | |
215 /// Attribute for the last node in the row, only used for sticky layout. | |
216 bool sticky = false; | |
217 } | |
OLD | NEW |