UNPKG

@visactor/vgrammar-hierarchy

Version:

Layout of hierarchical data for VGrammar

121 lines (115 loc) 6.91 kB
import { isNil, isArray, isFunction, range, isBoolean, polarToCartesian } from "@visactor/vutils"; import dice from "../treemap/dice"; import { field, toPercent } from "@visactor/vgrammar-util"; import { calculateNodeValue } from "../utils"; const keyMap = { x0: "startAngle", x1: "endAngle", y0: "innerRadius", y1: "outerRadius" }; export class SunburstLayout { constructor(options) { this._layoutNode = parent => { if (parent.maxDepth = this._maxDepth, parent.children) { const ir = this._parsedInnerRadius[parent.depth + 1], or = this._parsedOutterRadius[parent.depth + 1]; dice(parent, parent.startAngle, Math.min(ir, or), parent.endAngle, Math.max(ir, or), keyMap); const labelOption = isArray(this.options.label) ? this.options.label[parent.depth + 1] : this.options.label; parent.children.forEach((child => { if (child.x = this._parsedCenter[0], child.y = this._parsedCenter[1], labelOption) return this._layoutLabel(child, isBoolean(labelOption) ? { align: "center", rotate: "radial" } : labelOption); })); } }, this.options = options ? Object.assign({}, SunburstLayout.defaultOpionts, options) : Object.assign({}, SunburstLayout.defaultOpionts); const keyOption = this.options.nodeKey, keyFunc = isFunction(keyOption) ? keyOption : keyOption ? field(keyOption) : null; this._getNodeKey = keyFunc, this._maxDepth = -1; } _parseRadius(viewBox, maxDepth) { const cx = viewBox.x0 + toPercent(this.options.center[0], viewBox.width), cy = viewBox.y0 + toPercent(this.options.center[1], viewBox.height), maxRadius = Math.min(viewBox.width / 2, viewBox.height / 2), innerRadius = this.options.innerRadius, outerRadius = this.options.outerRadius, isInnerArray = isArray(innerRadius), parsedInnerRadius = isInnerArray ? innerRadius.map((entry => toPercent(entry, maxRadius))) : toPercent(innerRadius, maxRadius), isOuterArray = isArray(outerRadius), gapRadius = this.options.gapRadius, parsedOuterRadius = isOuterArray ? outerRadius.map((entry => toPercent(entry, maxRadius))) : toPercent(outerRadius, maxRadius), rangeArr = range(0, maxDepth + 1); if (isInnerArray) this._parsedInnerRadius = rangeArr.map(((entry, index) => { const ir = parsedInnerRadius[index]; return isNil(ir) ? maxRadius : ir; })), this._parsedOutterRadius = rangeArr.map(((entry, index) => { var _a, _b; return isOuterArray ? null !== (_a = parsedOuterRadius[index]) && void 0 !== _a ? _a : maxRadius : index < maxDepth ? this._parsedInnerRadius[index + 1] - (isArray(gapRadius) ? null !== (_b = gapRadius[index]) && void 0 !== _b ? _b : 0 : gapRadius) : parsedOuterRadius; })); else if (isOuterArray) this._parsedOutterRadius = rangeArr.map(((entry, index) => isNil(parsedOuterRadius[index]) ? maxRadius : parsedOuterRadius[index])), this._parsedInnerRadius = rangeArr.map(((entry, index) => { var _a; return 0 === index ? parsedInnerRadius : this._parsedOutterRadius[index - 1] - (isArray(gapRadius) ? null !== (_a = gapRadius[index]) && void 0 !== _a ? _a : 0 : gapRadius); })); else { const ir = toPercent(innerRadius, maxRadius), step = (parsedOuterRadius - ir) / (maxDepth + 1); this._parsedInnerRadius = rangeArr.map(((entry, index) => ir + index * step)), this._parsedOutterRadius = rangeArr.map(((entry, index) => { var _a; return this._parsedInnerRadius[index] + step - (isArray(gapRadius) ? null !== (_a = gapRadius[index]) && void 0 !== _a ? _a : 0 : gapRadius); })); } this._parsedCenter = [ cx, cy ], this._maxRadius = maxRadius; } layout(data, config) { const viewBox = "width" in config ? { x0: 0, x1: config.width, y0: 0, y1: config.height, width: config.width, height: config.height } : { x0: Math.min(config.x0, config.x1), x1: Math.max(config.x0, config.x1), y0: Math.min(config.y0, config.y1), y1: Math.max(config.y0, config.y1), width: Math.abs(config.x1 - config.x0), height: Math.abs(config.y1 - config.y0) }; if (!data || !data.length) return []; const nodes = [], res = calculateNodeValue(data, nodes, 0, -1, null, this._getNodeKey); return this._parseRadius(viewBox, res.maxDepth), this._maxDepth = res.maxDepth, this._layout(nodes, { flattenIndex: -1, maxDepth: -1, key: "-1", depth: -1, index: -1, value: res.sum, datum: null, children: nodes, startAngle: this.options.startAngle, endAngle: this.options.endAngle }), nodes; } _layout(nodes, parent) { this._layoutNode(parent), nodes.forEach((node => { var _a; (null === (_a = null == node ? void 0 : node.children) || void 0 === _a ? void 0 : _a.length) ? this._layout(node.children, node) : this._layoutNode(node); })); } _layoutLabel(child, labelOption) { var _a; const angle = (child.startAngle + child.endAngle) / 2, r = ("start" === labelOption.align ? child.innerRadius : "end" === labelOption.align ? child.outerRadius : (child.innerRadius + child.outerRadius) / 2) + (null !== (_a = labelOption.offset) && void 0 !== _a ? _a : 0), pos = polarToCartesian({ x: this._parsedCenter[0], y: this._parsedCenter[1] }, r, angle); if (child.label = { x: pos.x, y: pos.y, textBaseline: "middle" }, "tangential" === labelOption.rotate) child.label.angle = angle - Math.PI / 2, child.label.textAlign = "center", child.label.maxLineWidth = Math.abs(child.endAngle - child.startAngle) * r; else { const uniformAngle = angle % (2 * Math.PI), formatAngle = uniformAngle < 0 ? uniformAngle + 2 * Math.PI : uniformAngle; formatAngle > Math.PI / 2 && formatAngle < 1.5 * Math.PI ? (child.label.angle = formatAngle + Math.PI, child.label.textAlign = "start" === labelOption.align ? "end" : "end" === labelOption.align ? "start" : "center") : (child.label.angle = formatAngle, child.label.textAlign = labelOption.align), child.label.maxLineWidth = child.isLeaf ? void 0 : Math.abs(child.outerRadius - child.innerRadius); } } } SunburstLayout.defaultOpionts = { startAngle: Math.PI / 2, endAngle: -3 * Math.PI / 2, center: [ "50%", "50%" ], gapRadius: 0, innerRadius: 0, outerRadius: "70%" }; //# sourceMappingURL=layout.js.map