前言
这一篇文章是继vite-bundle-analyzer
优化后的一次尝试。因为vite-bundle-analyzer
使用了@foamsearch/tree
尽管这个库十分优秀但是他不提供TreeShake
导致 我无法尽可能的压缩体积,因此我需要寻找一个其他的替代方案。本文会从Squarify
算法从0到1实现一个Treemap
。
为什么是Squarify
以D3的hierarchy
为例。常见的矩形树图本质上是通过对数组的分层形成一个具有层次的结构以便能直观的比较同级和子级占比。这里会分别实现这几种布局算法注意这和d3-hierarchy
不一致。
TreemapBinary
根据描述就是递归的将指定节点划为近似平衡的二叉树,对于宽矩形进行水平划分,对于高矩形进行垂直划分。
function function layoutTreemap(node: Node, x: number, y: number, w: number, h: number): LayoutNode
layoutTreemap(node: Node
node: Node, x: number
x: number, y: number
y: number, w: number
w: number, h: number
h: number): LayoutNode {
const const layout: [number, number, number, number]
layout: [number, number, number, number] = [x: number
x, y: number
y, w: number
w, h: number
h]
let let children: LayoutNode[]
children: LayoutNode[] = []
if (node: Node
node.Node.children: Node[]
children && node: Node
node.Node.children: Node[]
children.Array<Node>.length: number
Gets or sets the length of the array. This is a number one higher than the highest index in the array.length > 0) {
const const totalValue: number
totalValue = node: Node
node.Node.children: Node[]
children.Array<Node>.reduce<number>(callbackfn: (previousValue: number, currentValue: Node, currentIndex: number, array: Node[]) => number, initialValue: number): number (+2 overloads)
Calls the specified callback function for all the elements in an array. The return value of the callback function is the accumulated result, and is provided as an argument in the next call to the callback function.reduce(
(sum: number
sum, child: Node
child) => sum: number
sum + child: Node
child.Node.value: number
value,
0
)
let let accumulatedValue: number
accumulatedValue = 0
let let splitIndex: number
splitIndex = 0
// Find the split index
for (; let splitIndex: number
splitIndex < node: Node
node.Node.children: Node[]
children.Array<Node>.length: number
Gets or sets the length of the array. This is a number one higher than the highest index in the array.length; let splitIndex: number
splitIndex++) {
if (let accumulatedValue: number
accumulatedValue + node: Node
node.Node.children: Node[]
children[let splitIndex: number
splitIndex].Node.value: number
value > const totalValue: number
totalValue / 2) {
break
}
let accumulatedValue: number
accumulatedValue += node: Node
node.Node.children: Node[]
children[let splitIndex: number
splitIndex].Node.value: number
value
}
// Split the area and layout the children
if (w: number
w > h: number
h) {
const [const x1: number
x1, const w1: number
w1] = [x: number
x, w: number
w * let accumulatedValue: number
accumulatedValue / const totalValue: number
totalValue]
const [const x2: number
x2, const w2: number
w2] = [const x1: number
x1 + const w1: number
w1, w: number
w - const w1: number
w1]
let children: LayoutNode[]
children = [
...node: Node
node.Node.children: Node[]
children.Array<Node>.slice(start?: number, end?: number): Node[]
Returns a copy of a section of an array.
For both start and end, a negative index can be used to indicate an offset from the end of the array.
For example, -2 refers to the second to last element of the array.slice(0, let splitIndex: number
splitIndex).Array<Node>.map<LayoutNode>(callbackfn: (value: Node, index: number, array: Node[]) => LayoutNode, thisArg?: any): LayoutNode[]
Calls a defined callback function on each element of an array, and returns an array that contains the results.map((child: Node
child) => function layoutTreemap(node: Node, x: number, y: number, w: number, h: number): LayoutNode
layoutTreemap(child: Node
child, const x1: number
x1, y: number
y, const w1: number
w1, h: number
h)),
...node: Node
node.Node.children: Node[]
children.Array<Node>.slice(start?: number, end?: number): Node[]
Returns a copy of a section of an array.
For both start and end, a negative index can be used to indicate an offset from the end of the array.
For example, -2 refers to the second to last element of the array.slice(let splitIndex: number
splitIndex).Array<Node>.map<LayoutNode>(callbackfn: (value: Node, index: number, array: Node[]) => LayoutNode, thisArg?: any): LayoutNode[]
Calls a defined callback function on each element of an array, and returns an array that contains the results.map((child: Node
child) => function layoutTreemap(node: Node, x: number, y: number, w: number, h: number): LayoutNode
layoutTreemap(child: Node
child, const x2: number
x2, y: number
y, const w2: number
w2, h: number
h))
]
} else {
const [const y1: number
y1, const h1: number
h1] = [y: number
y, h: number
h * let accumulatedValue: number
accumulatedValue / const totalValue: number
totalValue]
const [const y2: number
y2, const h2: number
h2] = [const y1: number
y1 + const h1: number
h1, h: number
h - const h1: number
h1]
let children: LayoutNode[]
children = [
...node: Node
node.Node.children: Node[]
children.Array<Node>.slice(start?: number, end?: number): Node[]
Returns a copy of a section of an array.
For both start and end, a negative index can be used to indicate an offset from the end of the array.
For example, -2 refers to the second to last element of the array.slice(0, let splitIndex: number
splitIndex).Array<Node>.map<LayoutNode>(callbackfn: (value: Node, index: number, array: Node[]) => LayoutNode, thisArg?: any): LayoutNode[]
Calls a defined callback function on each element of an array, and returns an array that contains the results.map((child: Node
child) => function layoutTreemap(node: Node, x: number, y: number, w: number, h: number): LayoutNode
layoutTreemap(child: Node
child, x: number
x, const y1: number
y1, w: number
w, const h1: number
h1)),
...node: Node
node.Node.children: Node[]
children.Array<Node>.slice(start?: number, end?: number): Node[]
Returns a copy of a section of an array.
For both start and end, a negative index can be used to indicate an offset from the end of the array.
For example, -2 refers to the second to last element of the array.slice(let splitIndex: number
splitIndex).Array<Node>.map<LayoutNode>(callbackfn: (value: Node, index: number, array: Node[]) => LayoutNode, thisArg?: any): LayoutNode[]
Calls a defined callback function on each element of an array, and returns an array that contains the results.map((child: Node
child) => function layoutTreemap(node: Node, x: number, y: number, w: number, h: number): LayoutNode
layoutTreemap(child: Node
child, x: number
x, const y2: number
y2, w: number
w, const h2: number
h2))
]
}
}
return { LayoutNode.node: Node
node, LayoutNode.layout: [number, number, number, number]
layout, LayoutNode.children: LayoutNode[]
children }
}
TreemapDice/Slice/Both
根据d3-hierarchy
文档可以得知Treemap也有水平或者垂直以及混合他们的布局方式。这里提供了一个简易的算法实现。
export function function layoutTreemap(data: Node[], x: number, y: number, w: number, h: number, mode: LayoutMode): LayoutNode[]
layoutTreemap(data: Node[]
data: Node[], x: number
x: number, y: number
y: number, w: number
w: number, h: number
h: number, mode: LayoutMode
mode: type LayoutMode = "slice" | "dice" | "auto"
LayoutMode) {
const const result: LayoutNode[]
result: LayoutNode[] = []
if (!data: Node[]
data) { return const result: LayoutNode[]
result }
const const recursion: (start: number, x: number, y: number, w: number, h: number) => void
recursion = (start: number
start: number, x: number
x: number, y: number
y: number, w: number
w: number, h: number
h: number) => {
while (start: number
start < data: Node[]
data.Array<Node>.length: number
Gets or sets the length of the array. This is a number one higher than the highest index in the array.length) {
const const total: number
total = data: Node[]
data.Array<Node>.reduce<number>(callbackfn: (previousValue: number, currentValue: Node, currentIndex: number, array: Node[]) => number, initialValue: number): number (+2 overloads)
Calls the specified callback function for all the elements in an array. The return value of the callback function is the accumulated result, and is provided as an argument in the next call to the callback function.reduce((acc: number
acc, cur: Node
cur) => acc: number
acc += cur: Node
cur.Node.value: number
value, 0)
const const node: Node
node = data: Node[]
data[start: number
start]
const const sizeToArea: number
sizeToArea = const node: Node
node.Node.value: number
value / const total: number
total
const const vertical: [number, number, number, number]
vertical = [x: number
x, y: number
y, w: number
w, h: number
h * const sizeToArea: number
sizeToArea] as [number, number, number, number]
const const horizontal: [number, number, number, number]
horizontal = [x: number
x, y: number
y, w: number
w * const sizeToArea: number
sizeToArea, h: number
h] as [number, number, number, number]
const const preferHorizontal: boolean
preferHorizontal = !!(const node: Node
node.Node.children: Node[]
children && const node: Node
node.Node.children: Node[]
children.Array<Node>.length: number
Gets or sets the length of the array. This is a number one higher than the highest index in the array.length & 1)
const const direction: "horizontal" | "vertical"
direction = mode: LayoutMode
mode === 'auto'
? const preferHorizontal: boolean
preferHorizontal ? 'horizontal' : 'vertical'
: mode: "slice" | "dice"
mode === 'slice'
? 'vertical'
: 'horizontal'
const const layout: [number, number, number, number]
layout = const direction: "horizontal" | "vertical"
direction === 'vertical' ? const vertical: [number, number, number, number]
vertical : const horizontal: [number, number, number, number]
horizontal
const result: LayoutNode[]
result.Array<LayoutNode>.push(...items: LayoutNode[]): number
Appends new elements to the end of an array, and returns the new length of the array.push({
LayoutNode.node: Node
node,
LayoutNode.layout: [number, number, number, number]
layout,
LayoutNode.children: LayoutNode[]
children: const node: Node
node.Node.children: Node[]
children
? function layoutTreemap(data: Node[], x: number, y: number, w: number, h: number, mode: LayoutMode): LayoutNode[]
layoutTreemap(const node: Node
node.Node.children: Node[]
children, ...const layout: [number, number, number, number]
layout, mode: LayoutMode
mode)
: []
})
if (const direction: "horizontal" | "vertical"
direction === 'horizontal') {
x: number
x += const layout: [number, number, number, number]
layout[2]
} else {
y: number
y += const layout: [number, number, number, number]
layout[3]
}
start: number
start++
}
}
const recursion: (start: number, x: number, y: number, w: number, h: number) => void
recursion(0, x: number
x, y: number
y, w: number
w, h: number
h)
return const result: LayoutNode[]
result
}
TreemapSquarify
根据 Mark Bruls、Kees Huizing、Jarke J. van Wijk 算法实现分割 使生成的矩形尽量接近正方形,拥有更佳的平均长宽比。
function function layoutTreemap(data: Node[], x: number, y: number, w: number, h: number): LayoutNode[]
layoutTreemap(data: Node[]
data: Node[], x: number
x: number, y: number
y: number, w: number
w: number, h: number
h: number) {
const const result: LayoutNode[]
result: LayoutNode[] = []
const const worst: (start: number, end: number, shortSide: number, totalSizes: number, sizeToArea: number) => number
worst = (start: number
start: number, end: number
end: number, shortSide: number
shortSide: number, totalSizes: number
totalSizes: number, sizeToArea: number
sizeToArea: number) => {
const const first: Node
first = data: Node[]
data[start: number
start]
const const last: Node
last = data: Node[]
data[end: number
end]
const const maxArea: number
maxArea = const first: Node
first.Node.value: number
value * sizeToArea: number
sizeToArea
const const minArea: number
minArea = const last: Node
last.Node.value: number
value * sizeToArea: number
sizeToArea
return var Math: Math
An intrinsic object that provides basic mathematics functionality and constants.Math.Math.max(...values: number[]): number
Returns the larger of a set of supplied numeric expressions.max(
((shortSide: number
shortSide ** 2) * const maxArea: number
maxArea) / (totalSizes: number
totalSizes ** 2),
(totalSizes: number
totalSizes * totalSizes: number
totalSizes) / ((shortSide: number
shortSide ** 2) * const minArea: number
minArea)
)
}
const const squarify: (start: number, x: number, y: number, w: number, h: number) => void
squarify = (start: number
start: number, x: number
x: number, y: number
y: number, w: number
w: number, h: number
h: number) => {
while (start: number
start < data: Node[]
data.Array<Node>.length: number
Gets or sets the length of the array. This is a number one higher than the highest index in the array.length) {
const const total: number
total = data: Node[]
data.Array<Node>.reduce<number>(callbackfn: (previousValue: number, currentValue: Node, currentIndex: number, array: Node[]) => number, initialValue: number): number (+2 overloads)
Calls the specified callback function for all the elements in an array. The return value of the callback function is the accumulated result, and is provided as an argument in the next call to the callback function.reduce((acc: number
acc, cur: Node
cur) => acc: number
acc += cur: Node
cur.Node.value: number
value, 0)
const const shortSide: number
shortSide = var Math: Math
An intrinsic object that provides basic mathematics functionality and constants.Math.Math.min(...values: number[]): number
Returns the smaller of a set of supplied numeric expressions.min(w: number
w, h: number
h)
const const sizeToArea: number
sizeToArea = (w: number
w * h: number
h) / const total: number
total
let let end: number
end = start: number
start
let let areaInRun: number
areaInRun = 0
let let oldWorst: number
oldWorst = 0
while (let end: number
end < data: Node[]
data.Array<Node>.length: number
Gets or sets the length of the array. This is a number one higher than the highest index in the array.length) {
const const area: number
area = data: Node[]
data[let end: number
end].Node.value: number
value * const sizeToArea: number
sizeToArea
const const newWorst: number
newWorst = const worst: (start: number, end: number, shortSide: number, totalSizes: number, sizeToArea: number) => number
worst(start: number
start, let end: number
end, const shortSide: number
shortSide, let areaInRun: number
areaInRun + const area: number
area, const sizeToArea: number
sizeToArea)
if (let end: number
end > start: number
start && let oldWorst: number
oldWorst < const newWorst: number
newWorst) { break }
let areaInRun: number
areaInRun += const area: number
area
let oldWorst: number
oldWorst = const newWorst: number
newWorst
let end: number
end++
}
const const split: number
split = var Math: Math
An intrinsic object that provides basic mathematics functionality and constants.Math.Math.round(x: number): number
Returns a supplied numeric expression rounded to the nearest integer.round(let areaInRun: number
areaInRun / const shortSide: number
shortSide)
let let areaInLayout: number
areaInLayout = 0
for (let let i: number
i = start: number
start; let i: number
i < let end: number
end; let i: number
i++) {
const const node: Node
node = data: Node[]
data[let i: number
i]
const const area: number
area = const node: Node
node.Node.value: number
value * const sizeToArea: number
sizeToArea
const const lower: number
lower = var Math: Math
An intrinsic object that provides basic mathematics functionality and constants.Math.Math.round(x: number): number
Returns a supplied numeric expression rounded to the nearest integer.round(const shortSide: number
shortSide * let areaInLayout: number
areaInLayout / let areaInRun: number
areaInRun)
const const upper: number
upper = var Math: Math
An intrinsic object that provides basic mathematics functionality and constants.Math.Math.round(x: number): number
Returns a supplied numeric expression rounded to the nearest integer.round(const shortSide: number
shortSide * (let areaInLayout: number
areaInLayout + const area: number
area) / let areaInRun: number
areaInRun)
const const layout: [number, number, number, number]
layout: [number, number, number, number] = w: number
w >= h: number
h
? [x: number
x, y: number
y + const lower: number
lower, const split: number
split, const upper: number
upper - const lower: number
lower]
: [x: number
x + const lower: number
lower, y: number
y, const upper: number
upper - const lower: number
lower, const split: number
split]
const result: LayoutNode[]
result.Array<LayoutNode>.push(...items: LayoutNode[]): number
Appends new elements to the end of an array, and returns the new length of the array.push({
LayoutNode.node: Node
node,
LayoutNode.layout: [number, number, number, number]
layout,
LayoutNode.children: LayoutNode[]
children: const node: Node
node.Node.children: Node[]
children ? function layoutTreemap(data: Node[], x: number, y: number, w: number, h: number): LayoutNode[]
layoutTreemap(const node: Node
node.Node.children: Node[]
children, ...const layout: [number, number, number, number]
layout) : []
})
let areaInLayout: number
areaInLayout += const area: number
area
}
start: number
start = let end: number
end
if (w: number
w >= h: number
h) {
x: number
x += const split: number
split
w: number
w -= const split: number
split
} else {
y: number
y += const split: number
split
h: number
h -= const split: number
split
}
}
}
const squarify: (start: number, x: number, y: number, w: number, h: number) => void
squarify(0, x: number
x, y: number
y, w: number
w, h: number
h)
return const result: LayoutNode[]
result
}
总结
平均长宽比是指生成的矩形长宽的比值,越佳的平均长宽比,矩形越接近正方形,用户观感的体验也越好。节点有序性是指输入数据权重值发生变化时,树图节点位置的变化程度。 有序的节点,树图的稳定性也更加优秀。
Algorithm | 平均长宽比 | 节点有序性 | 稳定性 |
---|---|---|---|
treemapBinary | 良好 | 部分有序 | 一般 |
treemapSlice | 很差 | 有序 | 优秀 |
treemapDice | 很差 | 有序 | 优秀 |
treemapSquarify | 优秀 | 部分有序 | 一般 |