You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
279 lines
10 KiB
279 lines
10 KiB
import { __awaiter } from "tslib";
|
|
import { isNumber, isString } from '@antv/util';
|
|
import { cloneFormatData, formatNumberFn, formatSizeFn } from './util';
|
|
import { handleSingleNodeGraph } from './util/common';
|
|
import { parseSize } from './util/size';
|
|
const DEFAULTS_LAYOUT_OPTIONS = {
|
|
begin: [0, 0],
|
|
preventOverlap: true,
|
|
preventOverlapPadding: 10,
|
|
condense: false,
|
|
rows: undefined,
|
|
cols: undefined,
|
|
position: undefined,
|
|
sortBy: 'degree',
|
|
nodeSize: 30,
|
|
width: 300,
|
|
height: 300,
|
|
};
|
|
/**
|
|
* <zh/> 网格布局
|
|
*
|
|
* <en/> Grid layout
|
|
*/
|
|
export class GridLayout {
|
|
constructor(options = {}) {
|
|
this.options = options;
|
|
this.id = 'grid';
|
|
this.options = Object.assign(Object.assign({}, DEFAULTS_LAYOUT_OPTIONS), options);
|
|
}
|
|
/**
|
|
* Return the positions of nodes and edges(if needed).
|
|
*/
|
|
execute(graph, options) {
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
return this.genericGridLayout(false, graph, options);
|
|
});
|
|
}
|
|
/**
|
|
* To directly assign the positions to the nodes.
|
|
*/
|
|
assign(graph, options) {
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
yield this.genericGridLayout(true, graph, options);
|
|
});
|
|
}
|
|
genericGridLayout(assign, graph, options) {
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
const mergedOptions = Object.assign(Object.assign({}, this.options), options);
|
|
const { begin = [0, 0], condense, preventOverlapPadding, preventOverlap, rows: propsRows, cols: propsCols, nodeSpacing: paramNodeSpacing, nodeSize: paramNodeSize, width: propsWidth, height: propsHeight, position, } = mergedOptions;
|
|
let { sortBy } = mergedOptions;
|
|
const nodes = graph.getAllNodes();
|
|
const edges = graph.getAllEdges();
|
|
const n = nodes === null || nodes === void 0 ? void 0 : nodes.length;
|
|
// Need no layout if there is no node.
|
|
if (!n || n === 1) {
|
|
return handleSingleNodeGraph(graph, assign, begin);
|
|
}
|
|
const layoutNodes = nodes.map((node) => cloneFormatData(node));
|
|
if (
|
|
// `id` should be reserved keyword
|
|
sortBy !== 'id' &&
|
|
(!isString(sortBy) || layoutNodes[0].data[sortBy] === undefined)) {
|
|
sortBy = 'degree';
|
|
}
|
|
if (sortBy === 'degree') {
|
|
layoutNodes.sort((n1, n2) => graph.getDegree(n2.id, 'both') - graph.getDegree(n1.id, 'both'));
|
|
}
|
|
else if (sortBy === 'id') {
|
|
// sort nodes by ID
|
|
layoutNodes.sort((n1, n2) => {
|
|
if (isNumber(n2.id) && isNumber(n1.id)) {
|
|
return n2.id - n1.id;
|
|
}
|
|
return `${n1.id}`.localeCompare(`${n2.id}`);
|
|
});
|
|
}
|
|
else {
|
|
// sort nodes by value
|
|
layoutNodes.sort((n1, n2) => n2.data[sortBy] - n1.data[sortBy]);
|
|
}
|
|
const width = !propsWidth && typeof window !== 'undefined'
|
|
? window.innerWidth
|
|
: propsWidth;
|
|
const height = !propsHeight && typeof window !== 'undefined'
|
|
? window.innerHeight
|
|
: propsHeight;
|
|
const cells = n;
|
|
const rcs = { rows: propsRows, cols: propsCols };
|
|
// if rows or columns were set in self, use those values
|
|
if (propsRows != null && propsCols != null) {
|
|
rcs.rows = propsRows;
|
|
rcs.cols = propsCols;
|
|
}
|
|
else if (propsRows != null && propsCols == null) {
|
|
rcs.rows = propsRows;
|
|
rcs.cols = Math.ceil(cells / rcs.rows);
|
|
}
|
|
else if (propsRows == null && propsCols != null) {
|
|
rcs.cols = propsCols;
|
|
rcs.rows = Math.ceil(cells / rcs.cols);
|
|
}
|
|
else {
|
|
// otherwise use the automatic values and adjust accordingly // otherwise use the automatic values and adjust accordingly
|
|
// width/height * splits^2 = cells where splits is number of times to split width
|
|
const splits = Math.sqrt((cells * height) / width);
|
|
rcs.rows = Math.round(splits);
|
|
rcs.cols = Math.round((width / height) * splits);
|
|
}
|
|
rcs.rows = Math.max(rcs.rows, 1);
|
|
rcs.cols = Math.max(rcs.cols, 1);
|
|
if (rcs.cols * rcs.rows > cells) {
|
|
// otherwise use the automatic values and adjust accordingly
|
|
// if rounding was up, see if we can reduce rows or columns
|
|
const sm = small(rcs);
|
|
const lg = large(rcs);
|
|
// reducing the small side takes away the most cells, so try it first
|
|
if ((sm - 1) * lg >= cells) {
|
|
small(rcs, sm - 1);
|
|
}
|
|
else if ((lg - 1) * sm >= cells) {
|
|
large(rcs, lg - 1);
|
|
}
|
|
}
|
|
else {
|
|
// if rounding was too low, add rows or columns
|
|
while (rcs.cols * rcs.rows < cells) {
|
|
const sm = small(rcs);
|
|
const lg = large(rcs);
|
|
// try to add to larger side first (adds less in multiplication)
|
|
if ((lg + 1) * sm >= cells) {
|
|
large(rcs, lg + 1);
|
|
}
|
|
else {
|
|
small(rcs, sm + 1);
|
|
}
|
|
}
|
|
}
|
|
let cellWidth = condense ? 0 : width / rcs.cols;
|
|
let cellHeight = condense ? 0 : height / rcs.rows;
|
|
if (preventOverlap || paramNodeSpacing) {
|
|
const nodeSpacing = formatNumberFn(10, paramNodeSpacing);
|
|
const nodeSize = formatSizeFn(30, paramNodeSize, false);
|
|
layoutNodes.forEach((node) => {
|
|
if (!node.data.x || !node.data.y) {
|
|
// for bb
|
|
node.data.x = 0;
|
|
node.data.y = 0;
|
|
}
|
|
const oNode = graph.getNode(node.id);
|
|
const [nodeW, nodeH] = parseSize(nodeSize(oNode) || 30);
|
|
const p = nodeSpacing !== undefined ? nodeSpacing(node) : preventOverlapPadding;
|
|
const w = nodeW + p;
|
|
const h = nodeH + p;
|
|
cellWidth = Math.max(cellWidth, w);
|
|
cellHeight = Math.max(cellHeight, h);
|
|
});
|
|
}
|
|
const cellUsed = {}; // e.g. 'c-0-2' => true
|
|
// to keep track of current cell position
|
|
const rc = { row: 0, col: 0 };
|
|
// get a cache of all the manual positions
|
|
const id2manPos = {};
|
|
for (let i = 0; i < layoutNodes.length; i++) {
|
|
const node = layoutNodes[i];
|
|
let rcPos;
|
|
if (position) {
|
|
// TODO: not sure the api name
|
|
rcPos = position(graph.getNode(node.id));
|
|
}
|
|
if (rcPos && (rcPos.row !== undefined || rcPos.col !== undefined)) {
|
|
// must have at least row or col def'd
|
|
const pos = {
|
|
row: rcPos.row,
|
|
col: rcPos.col,
|
|
};
|
|
if (pos.col === undefined) {
|
|
// find unused col
|
|
pos.col = 0;
|
|
while (used(cellUsed, pos)) {
|
|
pos.col++;
|
|
}
|
|
}
|
|
else if (pos.row === undefined) {
|
|
// find unused row
|
|
pos.row = 0;
|
|
while (used(cellUsed, pos)) {
|
|
pos.row++;
|
|
}
|
|
}
|
|
id2manPos[node.id] = pos;
|
|
use(cellUsed, pos);
|
|
}
|
|
getPos(node, begin, cellWidth, cellHeight, id2manPos, rcs, rc, cellUsed);
|
|
}
|
|
const result = {
|
|
nodes: layoutNodes,
|
|
edges,
|
|
};
|
|
if (assign) {
|
|
layoutNodes.forEach((node) => {
|
|
graph.mergeNodeData(node.id, {
|
|
x: node.data.x,
|
|
y: node.data.y,
|
|
});
|
|
});
|
|
}
|
|
return result;
|
|
});
|
|
}
|
|
}
|
|
const small = (rcs, val) => {
|
|
let res;
|
|
const rows = rcs.rows || 5;
|
|
const cols = rcs.cols || 5;
|
|
if (val == null) {
|
|
res = Math.min(rows, cols);
|
|
}
|
|
else {
|
|
const min = Math.min(rows, cols);
|
|
if (min === rcs.rows) {
|
|
rcs.rows = val;
|
|
}
|
|
else {
|
|
rcs.cols = val;
|
|
}
|
|
}
|
|
return res;
|
|
};
|
|
const large = (rcs, val) => {
|
|
let result;
|
|
const usedRows = rcs.rows || 5;
|
|
const usedCols = rcs.cols || 5;
|
|
if (val == null) {
|
|
result = Math.max(usedRows, usedCols);
|
|
}
|
|
else {
|
|
const max = Math.max(usedRows, usedCols);
|
|
if (max === rcs.rows) {
|
|
rcs.rows = val;
|
|
}
|
|
else {
|
|
rcs.cols = val;
|
|
}
|
|
}
|
|
return result;
|
|
};
|
|
const used = (cellUsed, rc) => cellUsed[`c-${rc.row}-${rc.col}`] || false;
|
|
const use = (cellUsed, rc) => (cellUsed[`c-${rc.row}-${rc.col}`] = true);
|
|
const moveToNextCell = (rcs, rc) => {
|
|
const cols = rcs.cols || 5;
|
|
rc.col++;
|
|
if (rc.col >= cols) {
|
|
rc.col = 0;
|
|
rc.row++;
|
|
}
|
|
};
|
|
const getPos = (node, begin, cellWidth, cellHeight, id2manPos, rcs, rc, cellUsed) => {
|
|
let x;
|
|
let y;
|
|
// see if we have a manual position set
|
|
const rcPos = id2manPos[node.id];
|
|
if (rcPos) {
|
|
x = rcPos.col * cellWidth + cellWidth / 2 + begin[0];
|
|
y = rcPos.row * cellHeight + cellHeight / 2 + begin[1];
|
|
}
|
|
else {
|
|
// otherwise set automatically
|
|
while (used(cellUsed, rc)) {
|
|
moveToNextCell(rcs, rc);
|
|
}
|
|
x = rc.col * cellWidth + cellWidth / 2 + begin[0];
|
|
y = rc.row * cellHeight + cellHeight / 2 + begin[1];
|
|
use(cellUsed, rc);
|
|
moveToNextCell(rcs, rc);
|
|
}
|
|
node.data.x = x;
|
|
node.data.y = y;
|
|
};
|
|
//# sourceMappingURL=grid.js.map
|