@visactor/vgrammar-hierarchy
Version:
Layout of hierarchical data for VGrammar
121 lines (115 loc) • 6.91 kB
JavaScript
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