config redirect

This commit is contained in:
Nuno Coração
2023-01-29 22:30:24 +00:00
parent 17557c7d73
commit 5fb4bd8083
9905 changed files with 1258996 additions and 36355 deletions
+17
View File
@@ -0,0 +1,17 @@
module.exports = {
root: true,
env: {
node: true,
jest: true,
},
extends: "eslint:recommended",
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off'
},
parserOptions: {
parser: 'babel-eslint',
ecmaVersion: 6,
sourceType: 'module'
}
}
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019 Michael Wong
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+123
View File
@@ -0,0 +1,123 @@
# non-layered-tidy-tree-layout
Draw non-layered tidy trees in linear time.
> This a JavaScript port from the project [cwi-swat/non-layered-tidy-trees](https://github.com/cwi-swat/non-layered-tidy-trees), which is written in Java. The algorithm used in that project is from the paper by _A.J. van der Ploeg_, [Drawing Non-layered Tidy Trees in Linear Time](http://oai.cwi.nl/oai/asset/21856/21856B.pdf). There is another JavaScript port from that project [d3-flextree](https://github.com/Klortho/d3-flextree), which depends on _d3-hierarchy_. This project is dependency free.
## Getting started
### Installation
```
npm install non-layered-tidy-tree-layout
```
Or
```
yarn add non-layered-tidy-tree-layout
```
There's also a built verison: `dist/non-layered-tidy-tree-layout.js` for use with browser `<script>` tag, or as a Javascript module.
### Usage
```js
import { BoundingBox, Layout } from 'non-layered-tidy-tree-layout'
// BoundingBox(gap, bottomPadding)
const bb = new BoundingBox(10, 20)
const layout = new Layout(bb)
const treeData = {
id: 0,
width: 40,
height: 40,
children: [
{
id: 1,
width: 40,
height: 40,
children: [{ id: 6, width: 400, height: 40 }]
},
{ id: 2, width: 40, height: 40 },
{ id: 3, width: 40, height: 40 },
{ id: 4, width: 40, height: 40 },
{ id: 5, width: 40, height: 80 }
]
}
const { result, boundingBox } = layout.layout(treeData)
// result:
// {
// id: 0,
// x: 300,
// y: 0,
// width: 40,
// height: 40,
// children: [
// {
// id: 1,
// x: 185,
// y: 60,
// width: 40,
// height: 40,
// children: [
// { id: 6, x: 5, y: 120, width: 400, height: 40 }
// ]
// },
// { id: 2, x: 242.5, y: 60, width: 40, height: 40 },
// { id: 3, x: 300, y: 60, width: 40, height: 40 },
// { id: 4, x: 357.5, y: 60, width: 40, height: 40 },
// { id: 5, x: 415, y: 60, width: 40, height: 80 }
// ]
// }
//
// boundingBox:
// {
// left: 5,
// right: 455,
// top: 0,
// bottom: 160
// }
```
The method `Layout.layout` modifies `treeData` inplace. It returns an object like `{ result: treeData, boundingBox: {left: num, right: num, top: num, bottom: num} }`. `result` is the same object `treeData` with calculated coordinates, `boundingBox` are the coordinates for the whole tree:
![](./screenshots/1.png)
The red dashed lines are the bounding boxes for each node. `Layout.layout()` produces coordinates to draw nodes, which are the grey boxes with black border.
The library also provides a class `Tree` and a method `layout`.
```js
/**
* Constructor for Tree.
* @param {number} width - width of bounding box
* @param {number} height - height of bounding box
* @param {number} y - veritcal coordinate of bounding box
* @param {array} children - a list of Tree instances
*/
new Tree(width, height, y, children)
/**
* Calculate x, y coordindates and assign them to tree.
* @param {Object} tree - a Tree object
*/
layout(tree)
```
In case your data structure are not the same as provided by the example above, you can refer to `src/helpers.js` to implement a `Layout` class that converts your data to a `Tree`, then call `layout` to calculate the coordinates for drawing.
## License
[MIT](./LICENSE)
## Changelog
### [2.0.1]
- Fixed bounding box calculation in `Layout.getSize` and `Layout.assignLayout` and `Layout.layout`
### [2.0.0]
- Added `Layout.layout`
- Removed `Layout.layoutTreeData`
### [1.0.0]
- Added `Layout`, `BoundingBox`, `layout`, `Tree`
+3
View File
@@ -0,0 +1,3 @@
module.exports = {
presets: [['@babel/preset-env', { targets: { node: 'current' } }]]
}
File diff suppressed because one or more lines are too long
+31
View File
@@ -0,0 +1,31 @@
{
"name": "non-layered-tidy-tree-layout",
"version": "2.0.2",
"description": "Draw non-layered tidy trees in linear time",
"main": "dist/non-layered-tidy-tree-layout.js",
"module": "src/index.js",
"repository": "https://github.com/stetrevor/non-layered-tidy-tree-layout.git",
"author": "Michael Wong",
"license": "MIT",
"devDependencies": {
"@babel/core": "^7.6.3",
"@babel/preset-env": "^7.6.3",
"@webpack-cli/init": "^0.2.2",
"babel-eslint": "^10.0.3",
"babel-jest": "^24.9.0",
"babel-loader": "^8.0.6",
"babel-plugin-syntax-dynamic-import": "^6.18.0",
"eslint": "^6.5.1",
"eslint-config-recommended": "^4.0.0",
"eslint-loader": "^3.0.2",
"html-webpack-plugin": "^3.2.0",
"jest": "^24.9.0",
"webpack": "^4.41.0",
"webpack-cli": "^3.3.9",
"webpack-dev-server": "^3.8.2"
},
"scripts": {
"build": "webpack",
"test": "jest test"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

+208
View File
@@ -0,0 +1,208 @@
class Tree {
constructor(width, height, y, children) {
this.w = width
this.h = height
this.y = y
this.c = children
this.cs = children.length
this.x = 0
this.prelim = 0
this.mod = 0
this.shift = 0
this.change = 0
this.tl = null // Left thread
this.tr = null // Right thread
this.el = null // extreme left nodes
this.er = null // extreme right nodes
//sum of modifiers at the extreme nodes
this.msel = 0
this.mser = 0
}
}
function setExtremes(tree) {
if (tree.cs === 0) {
tree.el = tree
tree.er = tree
tree.msel = tree.mser = 0
} else {
tree.el = tree.c[0].el
tree.msel = tree.c[0].msel
tree.er = tree.c[tree.cs - 1].er
tree.mser = tree.c[tree.cs - 1].mser
}
}
function bottom(tree) {
return tree.y + tree.h
}
/* A linked list of the indexes of left siblings and their lowest vertical coordinate.
*/
class IYL {
constructor(lowY, index, next) {
this.lowY = lowY
this.index = index
this.next = next
}
}
function updateIYL(minY, i, ih) {
// Remove siblings that are hidden by the new subtree.
while (ih !== null && minY >= ih.lowY) {
// Prepend the new subtree
ih = ih.next
}
return new IYL(minY, i, ih)
}
function distributeExtra(tree, i, si, distance) {
// Are there intermediate children?
if (si !== i - 1) {
const nr = i - si
tree.c[si + 1].shift += distance / nr
tree.c[i].shift -= distance / nr
tree.c[i].change -= distance - distance / nr
}
}
function moveSubtree(tree, i, si, distance) {
// Move subtree by changing mod.
tree.c[i].mod += distance
tree.c[i].msel += distance
tree.c[i].mser += distance
distributeExtra(tree, i, si, distance)
}
function nextLeftContour(tree) {
return tree.cs === 0 ? tree.tl : tree.c[0]
}
function nextRightContour(tree) {
return tree.cs === 0 ? tree.tr : tree.c[tree.cs - 1]
}
function setLeftThread(tree, i, cl, modsumcl) {
const li = tree.c[0].el
li.tl = cl
// Change mod so that the sum of modifier after following thread is correct.
const diff = (modsumcl - cl.mod) - tree.c[0].msel
li.mod += diff
// Change preliminary x coordinate so that the node does not move.
li.prelim -= diff
// Update extreme node and its sum of modifiers.
tree.c[0].el = tree.c[i].el
tree.c[0].msel = tree.c[i].msel
}
// Symmetrical to setLeftThread
function setRightThread(tree, i, sr, modsumsr) {
const ri = tree.c[i].er
ri.tr = sr
const diff = (modsumsr - sr.mod) - tree.c[i].mser
ri.mod += diff
ri.prelim -= diff
tree.c[i].er = tree.c[i - 1].er
tree.c[i].mser = tree.c[i - 1].mser
}
function seperate(tree, i, ih) {
// Right contour node of left siblings and its sum of modifiers.
let sr = tree.c[i - 1]
let mssr = sr.mod
// Left contour node of right siblings and its sum of modifiers.
let cl = tree.c[i]
let mscl = cl.mod
while (sr !== null && cl !== null) {
if (bottom(sr) > ih.lowY) {
ih = ih.next
}
// How far to the left of the right side of sr is the left side of cl.
const distance = mssr + sr.prelim + sr.w - (mscl + cl.prelim)
if (distance > 0) {
mscl += distance
moveSubtree(tree, i, ih.index, distance)
}
const sy = bottom(sr)
const cy = bottom(cl)
if (sy <= cy) {
sr = nextRightContour(sr)
if (sr !== null) {
mssr += sr.mod
}
}
if (sy >= cy) {
cl = nextLeftContour(cl)
if (cl !== null) {
mscl += cl.mod
}
}
}
// Set threads and update extreme nodes.
// In the first case, the current subtree must be taller than the left siblings.
if (sr === null && cl !== null) {
setLeftThread(tree, i, cl, mscl)
} else if (sr !== null && cl === null) {
setRightThread(tree, i, sr, mssr)
}
}
function positionRoot(tree) {
// Position root between children, taking into account their mod.
tree.prelim =
(tree.c[0].prelim +
tree.c[0].mod +
tree.c[tree.cs - 1].mod +
tree.c[tree.cs - 1].prelim +
tree.c[tree.cs - 1].w) /
2 -
tree.w / 2
}
function firstWalk(tree) {
if (tree.cs === 0) {
setExtremes(tree)
return
}
firstWalk(tree.c[0])
let ih = updateIYL(bottom(tree.c[0].el), 0, null)
for (let i = 1; i < tree.cs; i++) {
firstWalk(tree.c[i])
const minY = bottom(tree.c[i].er)
seperate(tree, i, ih)
ih = updateIYL(minY, i, ih)
}
positionRoot(tree)
setExtremes(tree)
}
function addChildSpacing(tree) {
let d = 0
let modsumdelta = 0
for (let i = 0; i < tree.cs; i++) {
d += tree.c[i].shift
modsumdelta += d + tree.c[i].change
tree.c[i].mod += modsumdelta
}
}
function secondWalk(tree, modsum) {
modsum += tree.mod
// Set absolute (no-relative) horizontal coordinates.
tree.x = tree.prelim + modsum
addChildSpacing(tree)
for (let i = 0; i < tree.cs; i++) {
secondWalk(tree.c[i], modsum)
}
}
function layout(tree) {
firstWalk(tree)
secondWalk(tree, 0)
}
export { Tree, layout }
+128
View File
@@ -0,0 +1,128 @@
import { layout, Tree } from './algorithm'
class BoundingBox {
/**
* @param {number} gap - the gap between sibling nodes
* @param {number} bottomPadding - the height reserved for connection drawing
*/
constructor(gap, bottomPadding) {
this.gap = gap
this.bottomPadding = bottomPadding
}
addBoundingBox(width, height) {
return { width: width + this.gap, height: height + this.bottomPadding }
}
/**
* Return the coordinate without the bounding box for a node
*/
removeBoundingBox(x, y) {
return { x: x + this.gap / 2, y }
}
}
class Layout {
constructor(boundingBox) {
this.bb = boundingBox
}
/**
* Layout treeData.
* Return modified treeData and the bounding box encompassing all the nodes.
*
* See getSize() for more explanation.
*/
layout(treeData) {
const tree = this.convert(treeData)
layout(tree)
const { boundingBox, result } = this.assignLayout(tree, treeData)
return { result, boundingBox }
}
/**
* Returns Tree to layout, with bounding boxes added to each node.
*/
convert(treeData, y = 0) {
if (treeData === null) return null
const { width, height } = this.bb.addBoundingBox(
treeData.width,
treeData.height
)
let children = []
if (treeData.children && treeData.children.length) {
for (let i = 0; i < treeData.children.length; i++) {
children[i] = this.convert(treeData.children[i], y + height)
}
}
return new Tree(width, height, y, children)
}
/**
* Assign layout tree x, y coordinates back to treeData,
* with bounding boxes removed.
*/
assignCoordinates(tree, treeData) {
const { x, y } = this.bb.removeBoundingBox(tree.x, tree.y)
treeData.x = x
treeData.y = y
for (let i = 0; i < tree.c.length; i++) {
this.assignCoordinates(tree.c[i], treeData.children[i])
}
}
/**
* Return the bounding box that encompasses all the nodes.
* The result has a structure of
* { left: number, right: number, top: number, bottom: nubmer}.
* This is not the same bounding box concept as the `BoundingBox` class
* used to construct `Layout` class.
*/
getSize(treeData, box = null) {
const { x, y, width, height } = treeData
if (box === null) {
box = { left: x, right: x + width, top: y, bottom: y + height }
}
box.left = Math.min(box.left, x)
box.right = Math.max(box.right, x + width)
box.top = Math.min(box.top, y)
box.bottom = Math.max(box.bottom, y + height)
if (treeData.children) {
for (const child of treeData.children) {
this.getSize(child, box)
}
}
return box
}
/**
* This function does assignCoordinates and getSize in one pass.
*/
assignLayout(tree, treeData, box = null) {
const { x, y } = this.bb.removeBoundingBox(tree.x, tree.y)
treeData.x = x
treeData.y = y
const { width, height } = treeData
if (box === null) {
box = { left: x, right: x + width, top: y, bottom: y + height }
}
box.left = Math.min(box.left, x)
box.right = Math.max(box.right, x + width)
box.top = Math.min(box.top, y)
box.bottom = Math.max(box.bottom, y + height)
for (let i = 0; i < tree.c.length; i++) {
this.assignLayout(tree.c[i], treeData.children[i], box)
}
return { result: treeData, boundingBox: box }
}
}
export { Layout, BoundingBox }
+4
View File
@@ -0,0 +1,4 @@
import { layout, Tree } from './algorithm'
import { BoundingBox, Layout } from './helpers'
export { layout, Tree, BoundingBox, Layout }
+100
View File
@@ -0,0 +1,100 @@
import { BoundingBox, Layout } from '../src/helpers'
test('bounding box', () => {
const bb = new BoundingBox(10, 20)
expect(bb.addBoundingBox(100, 200)).toEqual(
expect.objectContaining({
width: 110,
height: 220
})
)
expect(bb.removeBoundingBox(10, 120)).toEqual(
expect.objectContaining({
x: 15,
y: 120
})
)
})
test('Layout class', () => {
const data = {
width: 10,
height: 10,
children: [
{
width: 10,
height: 10,
children: [{ width: 150, height: 10, children: [] }]
},
{ width: 10, height: 10, children: [] },
{ width: 10, height: 10, children: [] },
{ width: 10, height: 10, children: [] },
{ width: 10, height: 20, children: [] }
]
}
const bb = new BoundingBox(10, 10)
const layout = new Layout(bb)
const { boundingBox } = layout.layout(data)
expect(data).toEqual(expect.objectContaining({ x: 120, y: 0 }))
expect(data.children[0]).toEqual(expect.objectContaining({ x: 75, y: 20 }))
expect(data.children[1]).toEqual(expect.objectContaining({ x: 97.5, y: 20 }))
expect(data.children[2]).toEqual(expect.objectContaining({ x: 120, y: 20 }))
expect(data.children[3]).toEqual(expect.objectContaining({ x: 142.5, y: 20 }))
expect(data.children[4]).toEqual(expect.objectContaining({ x: 165, y: 20 }))
expect(data.children[0].children[0]).toEqual(
expect.objectContaining({ x: 5, y: 40 })
)
expect(boundingBox).toEqual(
expect.objectContaining({ left: 5, right: 175, top: 0, bottom: 50 })
)
})
test('Big root, small child', () => {
const t = {
id: 0,
width: 100,
height: 50,
children: [{ id: 1, width: 50, height: 50 }]
}
const l = new Layout(new BoundingBox(0, 0))
const { result, boundingBox } = l.layout(t)
expect(result).toEqual(expect.objectContaining({ x: -25, y: 0 }))
expect(result.children[0]).toEqual(expect.objectContaining({ x: 0, y: 50 }))
expect(boundingBox).toEqual(
expect.objectContaining({ left: -25, right: 75, top: 0, bottom: 100 })
)
})
describe('Layout.getSize', () => {
test('big root, small child', () => {
const t = {
id: 0,
width: 100,
height: 50,
children: [{ id: 1, width: 50, height: 50 }]
}
const l = new Layout(new BoundingBox(0, 0))
l.layout(t)
const bb = l.getSize(t)
expect(bb).toEqual(
expect.objectContaining({ left: -25, right: 75, top: 0, bottom: 100 })
)
})
test('small root, big child', () => {
const t = {
id: 0,
width: 50,
height: 50,
children: [{ id: 1, width: 100, height: 50 }]
}
const l = new Layout(new BoundingBox(20, 20))
l.layout(t)
const bb = l.getSize(t)
expect(bb).toEqual(
expect.objectContaining({ left: 10, right: 110, top: 0, bottom: 120 })
)
})
})
+25
View File
@@ -0,0 +1,25 @@
import { Tree, layout } from '../src/algorithm'
export default {
convert(treeNode) {
if (treeNode === null) return null
let children = []
for (let i = 0; i < treeNode.children.length; i++) {
children[i] = this.convert(treeNode.children[i])
}
return new Tree(treeNode.width, treeNode.height, treeNode.y, children)
},
convertBack(converted, root) {
root.x = converted.x
for (let i = 0; i < converted.c.length; i++) {
this.convertBack(converted.c[i], root.children[i])
}
},
runOnConverted(root) {
layout(root)
}
}
+17
View File
@@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Test module in &gt;script&lt; tag</title>
</head>
<body>
<script src="../dist/non-layered-tidy-tree-layout.js"></script>
<script>
console.log(nonLayeredTidyTreeLayout);
console.log(nonLayeredTidyTreeLayout.Tree);
console.log(nonLayeredTidyTreeLayout.layout);
</script>
</body>
</html>
+5
View File
@@ -0,0 +1,5 @@
test('nonLayeredTidyTreeLayout exists', () => {
const { layout, Tree } = require('../dist/non-layered-tidy-tree-layout')
expect(layout).toBeTruthy()
expect(Tree).toBeTruthy()
})
+63
View File
@@ -0,0 +1,63 @@
import { layout, Tree } from '../src/algorithm'
import TreeNode from './tree-node'
import Marshall from './marshall'
test('layout tree with one node', () => {
const tree = new Tree(10, 5, 0, [])
layout(tree)
expect(tree).toEqual(expect.objectContaining({ x: 0, y: 0 }))
})
test('layout tree with 2 nodes', () => {
const child = new TreeNode(20, 10)
const root = new TreeNode(10, 4)
root.addChild(child)
const tree = Marshall.convert(root)
layout(tree)
Marshall.convertBack(tree, root)
expect(root).toEqual(expect.objectContaining({ x: 5, y: 0 }))
expect(child).toEqual(expect.objectContaining({ x: 0, y: 4 }))
})
test('layout tree with 3 nodes', () => {
const c1 = new TreeNode(10, 30)
const c2 = new TreeNode(20, 10)
const root = new TreeNode(40, 10)
root.addChild(c1)
root.addChild(c2)
const tree = Marshall.convert(root)
layout(tree)
Marshall.convertBack(tree, root)
expect(root).toEqual(expect.objectContaining({ x: -5, y: 0 }))
expect(c1).toEqual(expect.objectContaining({ x: 0, y: 10 }))
expect(c2).toEqual(expect.objectContaining({ x: 10, y: 10 }))
})
test('reflection of the tree is the mirror image of the original tree', () => {
const n1 = new TreeNode(10, 10)
const n2 = new TreeNode(10, 10)
const n3 = new TreeNode(10, 10)
const n4 = new TreeNode(10, 10)
const n5 = new TreeNode(10, 10)
const n6 = new TreeNode(10, 20)
const n7 = new TreeNode(150, 10)
n1.addChild(n2)
n1.addChild(n3)
n1.addChild(n4)
n1.addChild(n5)
n1.addChild(n6)
n2.addChild(n7)
const tree = Marshall.convert(n1)
layout(tree)
Marshall.convertBack(tree, n1)
expect(n1).toEqual(expect.objectContaining({ x: 110, y: 0 }))
expect(n2).toEqual(expect.objectContaining({ x: 70, y: 10 }))
expect(n3).toEqual(expect.objectContaining({ x: 90, y: 10 }))
expect(n4).toEqual(expect.objectContaining({ x: 110, y: 10 }))
expect(n5).toEqual(expect.objectContaining({ x: 130, y: 10 }))
expect(n6).toEqual(expect.objectContaining({ x: 150, y: 10 }))
expect(n7).toEqual(expect.objectContaining({ x: 0, y: 20 }))
})
+24
View File
@@ -0,0 +1,24 @@
export default class TreeNode {
constructor(width, height) {
this.width = width
this.height = height
this.x = 0
this.y = 0
this.children = []
}
addChild(child) {
child.y = this.y + this.height
this.children.push(child)
}
randExpand(tree) {
tree.y += this.height
const i = Math.floor(Math.random() * (this.children.length + 1))
if (i === this.children.length) {
this.children.push(tree)
} else {
this.children[i].randExpand(tree)
}
}
}
+34
View File
@@ -0,0 +1,34 @@
const path = require('path')
module.exports = {
mode: 'production',
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'non-layered-tidy-tree-layout.js',
library: 'nonLayeredTidyTreeLayout',
libraryTarget: 'umd'
},
module: {
rules: [
{
enforce: 'pre',
test: /\.js$/,
exclude: /node_modules/,
loader: 'eslint-loader'
},
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
}
]
}
}