UNPKG

vega-hierarchy

Version:

Hierarchical layout transforms for Vega dataflows.

562 lines (540 loc) 14 kB
import { Transform, isTuple, ingest, tupleid, stableCompare } from 'vega-dataflow'; import { inherits, error, array, one, truthy, hasOwnProperty } from 'vega-util'; import { hierarchy, pack, partition, stratify, cluster, tree, treemap, treemapResquarify, treemapSquarify, treemapSliceDice, treemapSlice, treemapDice, treemapBinary } from 'd3-hierarchy'; // Build lookup table mapping tuple keys to tree node instances function lookup (tree, key, filter) { const map = {}; tree.each(node => { const t = node.data; if (filter(t)) map[key(t)] = node; }); tree.lookup = map; return tree; } /** * Nest tuples into a tree structure, grouped by key values. * @constructor * @param {object} params - The parameters for this operator. * @param {Array<function(object): *>} params.keys - The key fields to nest by, in order. * @param {boolean} [params.generate=false] - A boolean flag indicating if * non-leaf nodes generated by this transform should be included in the * output. The default (false) includes only the input data (leaf nodes) * in the data stream. */ function Nest(params) { Transform.call(this, null, params); } Nest.Definition = { 'type': 'Nest', 'metadata': { 'treesource': true, 'changes': true }, 'params': [{ 'name': 'keys', 'type': 'field', 'array': true }, { 'name': 'generate', 'type': 'boolean' }] }; const children = n => n.values; inherits(Nest, Transform, { transform(_, pulse) { if (!pulse.source) { error('Nest transform requires an upstream data source.'); } var gen = _.generate, mod = _.modified(), out = pulse.clone(), tree = this.value; if (!tree || mod || pulse.changed()) { // collect nodes to remove if (tree) { tree.each(node => { if (node.children && isTuple(node.data)) { out.rem.push(node.data); } }); } // generate new tree structure this.value = tree = hierarchy({ values: array(_.keys).reduce((n, k) => { n.key(k); return n; }, nest()).entries(out.source) }, children); // collect nodes to add if (gen) { tree.each(node => { if (node.children) { node = ingest(node.data); out.add.push(node); out.source.push(node); } }); } // build lookup table lookup(tree, tupleid, tupleid); } out.source.root = tree; return out; } }); function nest() { const keys = [], nest = { entries: array => entries(apply(array, 0), 0), key: d => (keys.push(d), nest) }; function apply(array, depth) { if (depth >= keys.length) { return array; } const n = array.length, key = keys[depth++], valuesByKey = {}, result = {}; let i = -1, keyValue, value, values; while (++i < n) { keyValue = key(value = array[i]) + ''; if (values = valuesByKey[keyValue]) { values.push(value); } else { valuesByKey[keyValue] = [value]; } } for (keyValue in valuesByKey) { result[keyValue] = apply(valuesByKey[keyValue], depth); } return result; } function entries(map, depth) { if (++depth > keys.length) return map; const array = []; for (const key in map) { array.push({ key, values: entries(map[key], depth) }); } return array; } return nest; } /** * Abstract class for tree layout. * @constructor * @param {object} params - The parameters for this operator. */ function HierarchyLayout(params) { Transform.call(this, null, params); } const defaultSeparation = (a, b) => a.parent === b.parent ? 1 : 2; inherits(HierarchyLayout, Transform, { transform(_, pulse) { if (!pulse.source || !pulse.source.root) { error(this.constructor.name + ' transform requires a backing tree data source.'); } const layout = this.layout(_.method), fields = this.fields, root = pulse.source.root, as = _.as || fields; if (_.field) root.sum(_.field);else root.count(); if (_.sort) root.sort(stableCompare(_.sort, d => d.data)); setParams(layout, this.params, _); if (layout.separation) { layout.separation(_.separation !== false ? defaultSeparation : one); } try { this.value = layout(root); } catch (err) { error(err); } root.each(node => setFields(node, fields, as)); return pulse.reflow(_.modified()).modifies(as).modifies('leaf'); } }); function setParams(layout, params, _) { for (let p, i = 0, n = params.length; i < n; ++i) { p = params[i]; if (p in _) layout[p](_[p]); } } function setFields(node, fields, as) { const t = node.data, n = fields.length - 1; for (let i = 0; i < n; ++i) { t[as[i]] = node[fields[i]]; } t[as[n]] = node.children ? node.children.length : 0; } const Output$3 = ['x', 'y', 'r', 'depth', 'children']; /** * Packed circle tree layout. * @constructor * @param {object} params - The parameters for this operator. * @param {function(object): *} params.field - The value field to size nodes. */ function Pack(params) { HierarchyLayout.call(this, params); } Pack.Definition = { 'type': 'Pack', 'metadata': { 'tree': true, 'modifies': true }, 'params': [{ 'name': 'field', 'type': 'field' }, { 'name': 'sort', 'type': 'compare' }, { 'name': 'padding', 'type': 'number', 'default': 0 }, { 'name': 'radius', 'type': 'field', 'default': null }, { 'name': 'size', 'type': 'number', 'array': true, 'length': 2 }, { 'name': 'as', 'type': 'string', 'array': true, 'length': Output$3.length, 'default': Output$3 }] }; inherits(Pack, HierarchyLayout, { layout: pack, params: ['radius', 'size', 'padding'], fields: Output$3 }); const Output$2 = ['x0', 'y0', 'x1', 'y1', 'depth', 'children']; /** * Partition tree layout. * @constructor * @param {object} params - The parameters for this operator. * @param {function(object): *} params.field - The value field to size nodes. */ function Partition(params) { HierarchyLayout.call(this, params); } Partition.Definition = { 'type': 'Partition', 'metadata': { 'tree': true, 'modifies': true }, 'params': [{ 'name': 'field', 'type': 'field' }, { 'name': 'sort', 'type': 'compare' }, { 'name': 'padding', 'type': 'number', 'default': 0 }, { 'name': 'round', 'type': 'boolean', 'default': false }, { 'name': 'size', 'type': 'number', 'array': true, 'length': 2 }, { 'name': 'as', 'type': 'string', 'array': true, 'length': Output$2.length, 'default': Output$2 }] }; inherits(Partition, HierarchyLayout, { layout: partition, params: ['size', 'round', 'padding'], fields: Output$2 }); /** * Stratify a collection of tuples into a tree structure based on * id and parent id fields. * @constructor * @param {object} params - The parameters for this operator. * @param {function(object): *} params.key - Unique key field for each tuple. * @param {function(object): *} params.parentKey - Field with key for parent tuple. */ function Stratify(params) { Transform.call(this, null, params); } Stratify.Definition = { 'type': 'Stratify', 'metadata': { 'treesource': true }, 'params': [{ 'name': 'key', 'type': 'field', 'required': true }, { 'name': 'parentKey', 'type': 'field', 'required': true }] }; inherits(Stratify, Transform, { transform(_, pulse) { if (!pulse.source) { error('Stratify transform requires an upstream data source.'); } let tree = this.value; const mod = _.modified(), out = pulse.fork(pulse.ALL).materialize(pulse.SOURCE), run = !tree || mod || pulse.changed(pulse.ADD_REM) || pulse.modified(_.key.fields) || pulse.modified(_.parentKey.fields); // prevent upstream source pollution out.source = out.source.slice(); if (run) { tree = out.source.length ? lookup(stratify().id(_.key).parentId(_.parentKey)(out.source), _.key, truthy) : lookup(stratify()([{}]), _.key, _.key); } out.source.root = this.value = tree; return out; } }); const Layouts = { tidy: tree, cluster: cluster }; const Output$1 = ['x', 'y', 'depth', 'children']; /** * Tree layout. Depending on the method parameter, performs either * Reingold-Tilford 'tidy' layout or dendrogram 'cluster' layout. * @constructor * @param {object} params - The parameters for this operator. */ function Tree(params) { HierarchyLayout.call(this, params); } Tree.Definition = { 'type': 'Tree', 'metadata': { 'tree': true, 'modifies': true }, 'params': [{ 'name': 'field', 'type': 'field' }, { 'name': 'sort', 'type': 'compare' }, { 'name': 'method', 'type': 'enum', 'default': 'tidy', 'values': ['tidy', 'cluster'] }, { 'name': 'size', 'type': 'number', 'array': true, 'length': 2 }, { 'name': 'nodeSize', 'type': 'number', 'array': true, 'length': 2 }, { 'name': 'separation', 'type': 'boolean', 'default': true }, { 'name': 'as', 'type': 'string', 'array': true, 'length': Output$1.length, 'default': Output$1 }] }; inherits(Tree, HierarchyLayout, { /** * Tree layout generator. Supports both 'tidy' and 'cluster' layouts. */ layout(method) { const m = method || 'tidy'; if (hasOwnProperty(Layouts, m)) return Layouts[m]();else error('Unrecognized Tree layout method: ' + m); }, params: ['size', 'nodeSize'], fields: Output$1 }); /** * Generate tuples representing links between tree nodes. * The resulting tuples will contain 'source' and 'target' fields, * which point to parent and child node tuples, respectively. * @constructor * @param {object} params - The parameters for this operator. */ function TreeLinks(params) { Transform.call(this, [], params); } TreeLinks.Definition = { 'type': 'TreeLinks', 'metadata': { 'tree': true, 'generates': true, 'changes': true }, 'params': [] }; inherits(TreeLinks, Transform, { transform(_, pulse) { const links = this.value, tree = pulse.source && pulse.source.root, out = pulse.fork(pulse.NO_SOURCE), lut = {}; if (!tree) error('TreeLinks transform requires a tree data source.'); if (pulse.changed(pulse.ADD_REM)) { // remove previous links out.rem = links; // build lookup table of valid tuples pulse.visit(pulse.SOURCE, t => lut[tupleid(t)] = 1); // generate links for all edges incident on valid tuples tree.each(node => { const t = node.data, p = node.parent && node.parent.data; if (p && lut[tupleid(t)] && lut[tupleid(p)]) { out.add.push(ingest({ source: p, target: t })); } }); this.value = out.add; } else if (pulse.changed(pulse.MOD)) { // build lookup table of modified tuples pulse.visit(pulse.MOD, t => lut[tupleid(t)] = 1); // gather links incident on modified tuples links.forEach(link => { if (lut[tupleid(link.source)] || lut[tupleid(link.target)]) { out.mod.push(link); } }); } return out; } }); const Tiles = { binary: treemapBinary, dice: treemapDice, slice: treemapSlice, slicedice: treemapSliceDice, squarify: treemapSquarify, resquarify: treemapResquarify }; const Output = ['x0', 'y0', 'x1', 'y1', 'depth', 'children']; /** * Treemap layout. * @constructor * @param {object} params - The parameters for this operator. * @param {function(object): *} params.field - The value field to size nodes. */ function Treemap(params) { HierarchyLayout.call(this, params); } Treemap.Definition = { 'type': 'Treemap', 'metadata': { 'tree': true, 'modifies': true }, 'params': [{ 'name': 'field', 'type': 'field' }, { 'name': 'sort', 'type': 'compare' }, { 'name': 'method', 'type': 'enum', 'default': 'squarify', 'values': ['squarify', 'resquarify', 'binary', 'dice', 'slice', 'slicedice'] }, { 'name': 'padding', 'type': 'number', 'default': 0 }, { 'name': 'paddingInner', 'type': 'number', 'default': 0 }, { 'name': 'paddingOuter', 'type': 'number', 'default': 0 }, { 'name': 'paddingTop', 'type': 'number', 'default': 0 }, { 'name': 'paddingRight', 'type': 'number', 'default': 0 }, { 'name': 'paddingBottom', 'type': 'number', 'default': 0 }, { 'name': 'paddingLeft', 'type': 'number', 'default': 0 }, { 'name': 'ratio', 'type': 'number', 'default': 1.618033988749895 }, { 'name': 'round', 'type': 'boolean', 'default': false }, { 'name': 'size', 'type': 'number', 'array': true, 'length': 2 }, { 'name': 'as', 'type': 'string', 'array': true, 'length': Output.length, 'default': Output }] }; inherits(Treemap, HierarchyLayout, { /** * Treemap layout generator. Adds 'method' and 'ratio' parameters * to configure the underlying tile method. */ layout() { const x = treemap(); x.ratio = _ => { const t = x.tile(); if (t.ratio) x.tile(t.ratio(_)); }; x.method = _ => { if (hasOwnProperty(Tiles, _)) x.tile(Tiles[_]);else error('Unrecognized Treemap layout method: ' + _); }; return x; }, params: ['method', 'ratio', 'size', 'round', 'padding', 'paddingInner', 'paddingOuter', 'paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft'], fields: Output }); export { Nest as nest, Pack as pack, Partition as partition, Stratify as stratify, Tree as tree, TreeLinks as treelinks, Treemap as treemap }; //# sourceMappingURL=vega-hierarchy.js.map