| 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 |