| 1 | import {optional} from "./accessors.js";
|
|---|
| 2 | import {Node, computeHeight} from "./hierarchy/index.js";
|
|---|
| 3 |
|
|---|
| 4 | var preroot = {depth: -1},
|
|---|
| 5 | ambiguous = {},
|
|---|
| 6 | imputed = {};
|
|---|
| 7 |
|
|---|
| 8 | function defaultId(d) {
|
|---|
| 9 | return d.id;
|
|---|
| 10 | }
|
|---|
| 11 |
|
|---|
| 12 | function defaultParentId(d) {
|
|---|
| 13 | return d.parentId;
|
|---|
| 14 | }
|
|---|
| 15 |
|
|---|
| 16 | export default function() {
|
|---|
| 17 | var id = defaultId,
|
|---|
| 18 | parentId = defaultParentId,
|
|---|
| 19 | path;
|
|---|
| 20 |
|
|---|
| 21 | function stratify(data) {
|
|---|
| 22 | var nodes = Array.from(data),
|
|---|
| 23 | currentId = id,
|
|---|
| 24 | currentParentId = parentId,
|
|---|
| 25 | n,
|
|---|
| 26 | d,
|
|---|
| 27 | i,
|
|---|
| 28 | root,
|
|---|
| 29 | parent,
|
|---|
| 30 | node,
|
|---|
| 31 | nodeId,
|
|---|
| 32 | nodeKey,
|
|---|
| 33 | nodeByKey = new Map;
|
|---|
| 34 |
|
|---|
| 35 | if (path != null) {
|
|---|
| 36 | const I = nodes.map((d, i) => normalize(path(d, i, data)));
|
|---|
| 37 | const P = I.map(parentof);
|
|---|
| 38 | const S = new Set(I).add("");
|
|---|
| 39 | for (const i of P) {
|
|---|
| 40 | if (!S.has(i)) {
|
|---|
| 41 | S.add(i);
|
|---|
| 42 | I.push(i);
|
|---|
| 43 | P.push(parentof(i));
|
|---|
| 44 | nodes.push(imputed);
|
|---|
| 45 | }
|
|---|
| 46 | }
|
|---|
| 47 | currentId = (_, i) => I[i];
|
|---|
| 48 | currentParentId = (_, i) => P[i];
|
|---|
| 49 | }
|
|---|
| 50 |
|
|---|
| 51 | for (i = 0, n = nodes.length; i < n; ++i) {
|
|---|
| 52 | d = nodes[i], node = nodes[i] = new Node(d);
|
|---|
| 53 | if ((nodeId = currentId(d, i, data)) != null && (nodeId += "")) {
|
|---|
| 54 | nodeKey = node.id = nodeId;
|
|---|
| 55 | nodeByKey.set(nodeKey, nodeByKey.has(nodeKey) ? ambiguous : node);
|
|---|
| 56 | }
|
|---|
| 57 | if ((nodeId = currentParentId(d, i, data)) != null && (nodeId += "")) {
|
|---|
| 58 | node.parent = nodeId;
|
|---|
| 59 | }
|
|---|
| 60 | }
|
|---|
| 61 |
|
|---|
| 62 | for (i = 0; i < n; ++i) {
|
|---|
| 63 | node = nodes[i];
|
|---|
| 64 | if (nodeId = node.parent) {
|
|---|
| 65 | parent = nodeByKey.get(nodeId);
|
|---|
| 66 | if (!parent) throw new Error("missing: " + nodeId);
|
|---|
| 67 | if (parent === ambiguous) throw new Error("ambiguous: " + nodeId);
|
|---|
| 68 | if (parent.children) parent.children.push(node);
|
|---|
| 69 | else parent.children = [node];
|
|---|
| 70 | node.parent = parent;
|
|---|
| 71 | } else {
|
|---|
| 72 | if (root) throw new Error("multiple roots");
|
|---|
| 73 | root = node;
|
|---|
| 74 | }
|
|---|
| 75 | }
|
|---|
| 76 |
|
|---|
| 77 | if (!root) throw new Error("no root");
|
|---|
| 78 |
|
|---|
| 79 | // When imputing internal nodes, only introduce roots if needed.
|
|---|
| 80 | // Then replace the imputed marker data with null.
|
|---|
| 81 | if (path != null) {
|
|---|
| 82 | while (root.data === imputed && root.children.length === 1) {
|
|---|
| 83 | root = root.children[0], --n;
|
|---|
| 84 | }
|
|---|
| 85 | for (let i = nodes.length - 1; i >= 0; --i) {
|
|---|
| 86 | node = nodes[i];
|
|---|
| 87 | if (node.data !== imputed) break;
|
|---|
| 88 | node.data = null;
|
|---|
| 89 | }
|
|---|
| 90 | }
|
|---|
| 91 |
|
|---|
| 92 | root.parent = preroot;
|
|---|
| 93 | root.eachBefore(function(node) { node.depth = node.parent.depth + 1; --n; }).eachBefore(computeHeight);
|
|---|
| 94 | root.parent = null;
|
|---|
| 95 | if (n > 0) throw new Error("cycle");
|
|---|
| 96 |
|
|---|
| 97 | return root;
|
|---|
| 98 | }
|
|---|
| 99 |
|
|---|
| 100 | stratify.id = function(x) {
|
|---|
| 101 | return arguments.length ? (id = optional(x), stratify) : id;
|
|---|
| 102 | };
|
|---|
| 103 |
|
|---|
| 104 | stratify.parentId = function(x) {
|
|---|
| 105 | return arguments.length ? (parentId = optional(x), stratify) : parentId;
|
|---|
| 106 | };
|
|---|
| 107 |
|
|---|
| 108 | stratify.path = function(x) {
|
|---|
| 109 | return arguments.length ? (path = optional(x), stratify) : path;
|
|---|
| 110 | };
|
|---|
| 111 |
|
|---|
| 112 | return stratify;
|
|---|
| 113 | }
|
|---|
| 114 |
|
|---|
| 115 | // To normalize a path, we coerce to a string, strip the trailing slash if any
|
|---|
| 116 | // (as long as the trailing slash is not immediately preceded by another slash),
|
|---|
| 117 | // and add leading slash if missing.
|
|---|
| 118 | function normalize(path) {
|
|---|
| 119 | path = `${path}`;
|
|---|
| 120 | let i = path.length;
|
|---|
| 121 | if (slash(path, i - 1) && !slash(path, i - 2)) path = path.slice(0, -1);
|
|---|
| 122 | return path[0] === "/" ? path : `/${path}`;
|
|---|
| 123 | }
|
|---|
| 124 |
|
|---|
| 125 | // Walk backwards to find the first slash that is not the leading slash, e.g.:
|
|---|
| 126 | // "/foo/bar" ⇥ "/foo", "/foo" ⇥ "/", "/" ↦ "". (The root is special-cased
|
|---|
| 127 | // because the id of the root must be a truthy value.)
|
|---|
| 128 | function parentof(path) {
|
|---|
| 129 | let i = path.length;
|
|---|
| 130 | if (i < 2) return "";
|
|---|
| 131 | while (--i > 1) if (slash(path, i)) break;
|
|---|
| 132 | return path.slice(0, i);
|
|---|
| 133 | }
|
|---|
| 134 |
|
|---|
| 135 | // Slashes can be escaped; to determine whether a slash is a path delimiter, we
|
|---|
| 136 | // count the number of preceding backslashes escaping the forward slash: an odd
|
|---|
| 137 | // number indicates an escaped forward slash.
|
|---|
| 138 | function slash(path, i) {
|
|---|
| 139 | if (path[i] === "/") {
|
|---|
| 140 | let k = 0;
|
|---|
| 141 | while (i > 0 && path[--i] === "\\") ++k;
|
|---|
| 142 | if ((k & 1) === 0) return true;
|
|---|
| 143 | }
|
|---|
| 144 | return false;
|
|---|
| 145 | }
|
|---|