UNPKG

@atomist/org-visualizer

Version:

Organization Visualizer using Atomist project scanning

297 lines 10.7 kB
"use strict"; /* * Copyright © 2019 Atomist, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); const automation_client_1 = require("@atomist/automation-client"); const _ = require("lodash"); const sunburst_1 = require("./sunburst"); function visit(t, visitor, depth = 0) { const keepGoing = visitor(t, depth); if (keepGoing && sunburst_1.isSunburstTree(t)) { (t.children || []).forEach(c => visit(c, visitor, depth + 1)); } } exports.visit = visit; function visitAsync(t, visitor, depth = 0) { return __awaiter(this, void 0, void 0, function* () { const r = yield visitor(t, depth); if (r && sunburst_1.isSunburstTree(t)) { yield Promise.all(t.children.map(c => visitAsync(c, visitor, depth + 1))); } }); } exports.visitAsync = visitAsync; /** * Suppress branches that meet a condition */ function killChildren(tr, shouldEliminate) { const t = _.cloneDeep(tr); visit(t, (l, depth) => { if (sunburst_1.isSunburstTree(l)) { l.children = l.children.filter(c => { if (sunburst_1.isSunburstTree(c)) { return true; } const kill = shouldEliminate(c, depth + 1); // logger.debug("Kill = %s for %s of depth %d", kill, c.name, depth); return !kill; }); return true; } return true; }); return t; } /** * Merge siblings into groups by the grouper */ function groupSiblings(tr, params) { const opts = Object.assign({ collapseUnderName: () => false }, params); const t = _.cloneDeep(tr); visit(t, l => { if (sunburst_1.isSunburstTree(l) && opts.parentSelector(l)) { const grouped = _.groupBy(l.children, opts.childClassifier); if (Object.values(grouped).every(a => a.length === 1)) { // Nothing needs merged return false; } l.children = []; for (const name of Object.keys(grouped)) { let children = _.flatten(grouped[name]); if (opts.collapseUnderName(name)) { children = _.flatten(children.map(childrenOf)); } const newLayer = { name, children, }; if (opts.groupLayerDecorator) { opts.groupLayerDecorator(newLayer); } l.children.push(newLayer); } return false; } return true; }); return t; } exports.groupSiblings = groupSiblings; /** * Trim the outer rim, replacing the next one with sized leaves */ function trimOuterRim(tr) { const t = _.cloneDeep(tr); visit(t, l => { if (sunburst_1.isSunburstTree(l) && !l.children.some(sunburst_1.isSunburstTree)) { l.size = l.children.length; delete (l.children); } return true; }); return t; } exports.trimOuterRim = trimOuterRim; function insertAt(arr, index, item) { arr.splice(index, 0, item); return arr; } /** * Introduce a new level splitting by by the given classifier for descendants */ function introduceClassificationLayer(pt, how) { const opts = Object.assign({ descendantFinder: descendants }, how); const tr = pt.tree; if (tr.children.length === 0) { return pt; } const circles = insertAt(pt.circles, opts.newLayerDepth, { meaning: opts.newLayerMeaning }); // Find descendants we're introduced in const descendantPicker = tree => opts.descendantFinder(tree).filter(n => !!opts.descendantClassifier(n)); const t = _.cloneDeep(tr); visit(t, (node, depth) => { if (depth === (opts.newLayerDepth - 1) && sunburst_1.isSunburstTree(node)) { // Split children const descendantsToClassifyBy = descendantPicker(node); automation_client_1.logger.info("Found %d leaves for %s", descendantsToClassifyBy.length, t.name); // Introduce a new node for each classification const distinctNames = _.uniq(descendantsToClassifyBy.map(d => opts.descendantClassifier(d))); const oldKids = node.children; node.children = []; for (const name of distinctNames.sort()) { const children = oldKids .filter(k => sunburst_1.isSunburstTree(k) && descendantPicker(k).some(leaf => opts.descendantClassifier(leaf) === name) || !sunburst_1.isSunburstTree(k) && opts.descendantClassifier(k) === name); if (children.length > 0) { // Need to take out the children that are trees but don't have a descendant under them const subTree = { name, children, }; // Kill the children that have a different descendant classification const prunedSubTree = killChildren(subTree, tt => { const classification = opts.descendantClassifier(tt); return !!classification && classification !== name; }); node.children.push(prunedSubTree); } } return false; } return true; }); const result = { tree: t, circles }; validatePlantedTree(result); return result; } exports.introduceClassificationLayer = introduceClassificationLayer; function pruneLeaves(tr, toPrune) { const copy = _.cloneDeep(tr); visit(copy, l => { if (sunburst_1.isSunburstTree(l) && !l.children.some(sunburst_1.isSunburstTree)) { l.children = l.children.filter(c => { const f = sunburst_1.isSunburstTree(c) || !toPrune(c); return f; }); return false; } return true; }); return copy; } exports.pruneLeaves = pruneLeaves; /** * Return all terminals under this level * @param {SunburstLevel} t * @return {SunburstLeaf[]} */ function leavesUnder(t) { const leaves = []; visit(t, l => { if (!sunburst_1.isSunburstTree(l)) { leaves.push(l); } return true; }); return leaves; } exports.leavesUnder = leavesUnder; function descendants(t) { const descs = []; visit(t, l => { descs.push(l); if (sunburst_1.isSunburstTree(l)) { descs.push(..._.flatten(l.children.map(descendants))); } return true; }); return _.uniq(descs); } exports.descendants = descendants; function childCount(l) { return sunburst_1.isSunburstTree(l) ? l.children.length : 0; } exports.childCount = childCount; function childrenOf(l) { return sunburst_1.isSunburstTree(l) ? l.children : []; } exports.childrenOf = childrenOf; /** * Merge these trees. They must have the same name. * Trees may be merged in memory because they may have many nodes, but don't have much data. * @return {SunburstTree} */ function mergeTrees(...trees) { if (trees.length === 1) { return trees[0]; } return trees.reduce(merge2Trees); } exports.mergeTrees = mergeTrees; function merge2Trees(t1, t2) { if (t1.name !== t2.name) { throw new Error(`Trees with different names cannot be merged. Had '${t1.name}' and '${t2.name}'`); } const mergedChildren = []; for (const child of [...t1.children, ...t2.children]) { const existing = mergedChildren.find(n => n.name === child.name); if (existing) { if (!sunburst_1.isSunburstTree(existing)) { if (sunburst_1.isSunburstTree(child)) { throw new Error(`Cannot add tree child to non-tree ${JSON.stringify(existing)}`); } // TODO what about label? existing.size += child.size; } else { const merged = mergeTrees(existing, child); const index = mergedChildren.indexOf(existing); mergedChildren[index] = merged; } } else { mergedChildren.push(child); } } const result = { name: t1.name, children: mergedChildren, }; // console.log(JSON.stringify(result)); return result; } function validatePlantedTree(pt) { checkNullChildrenInvariant(pt); checkDepthInvariant(pt); } exports.validatePlantedTree = validatePlantedTree; function checkDepthInvariant(pt) { let depth = 0; visit(pt.tree, (l, d) => { if (d > depth) { depth = d; } return true; }); // the tree counts depth from zero if ((depth + 1) !== pt.circles.length) { automation_client_1.logger.error("Data: " + JSON.stringify(pt, undefined, 2)); throw new Error(`Expected a depth of ${pt.circles.length} but saw a tree of depth ${depth + 1}`); } } function checkNullChildrenInvariant(pt) { const haveNullChildren = []; visit(pt.tree, (l, d) => { if (sunburst_1.isSunburstTree(l) && l.children === null) { haveNullChildren.push(l); } return true; }); // the tree counts depth from zero if (haveNullChildren.length > 0) { automation_client_1.logger.error("Tree: " + JSON.stringify(pt.tree, undefined, 2)); throw new Error(`${haveNullChildren.length} tree nodes have null children: ${JSON.stringify(haveNullChildren)}`); } } //# sourceMappingURL=treeUtils.js.map