Git Product home page Git Product logo

layout's Introduction

@antv/layout

Build Status Coverage Status npm Version npm Download npm License

This is a collection of basic layout algorithms. We provide the following packages to support different runtime environments:

Online benchmarks: https://antv.vision/layout/index.html

Development

We use Vite to start a dev server:

$ pnpm dev

Test

$ pnpm test

Publish

Using Changesets with pnpm: https://pnpm.io/next/using-changesets

The generated markdown files in the .changeset directory should be committed to the repository.

pnpm changeset

This will bump the versions of the packages previously specified with pnpm changeset (and any dependents of those) and update the changelog files.

pnpm changeset version

Commit the changes. This command will publish all packages that have bumped versions not yet present in the registry.

pnpm publish -r

If you want to publish versions for test:

pnpm changeset pre enter alpha   # 发布 alpha 版本
pnpm changeset pre enter beta    # 发布 beta 版本
pnpm changeset pre enter rc      # 发布 rc 版本

layout's People

Contributors

aarebecca avatar andreasnett avatar baizn avatar brickmaker avatar bubkoo avatar hustcc avatar i-artist avatar lxfu1 avatar mxz96102 avatar newbyvector avatar serializedowen avatar webhao avatar wenyanqi avatar whf0403 avatar xiaoiver avatar yanyan-wang avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

layout's Issues

多次迭代的布局在 Worker 中运行是否还需要触发 tick 事件?

d3.force 提供了 tick 方法,配合 stop 可以完成同步方式下的 static force layout
https://github.com/d3/d3-force#simulation_tick

const simulation = d3.forceSimulation(nodes)
// 省略其他参数
  .stop();

for () {
  simulation.tick();
}

// 一次性计算完成后渲染

文档中也建议这种方式适合放在 WebWorker 中运行,同时也不会触发 tick 事件,同步计算完成后进行渲染等操作。

如果把 Worker 中的首次布局计算看作服务端渲染,整个过程有点像 React SSR 中的 hydrate

  • Worker 中执行 static force layout,返回结果给主线程。类似返回服务端渲染结果 HTML 片段
  • 主线程接收到首次布局结果,需要更新图模型中的数据(如果后续需要调用 assign 的话),便于 drag 时继续执行使用。类似给 HTML 添加交互

在 Supervisor 中使用

配合 Supervisor 使用时,不关心 tick,只关心全部迭代完成后的最终结果:

const graph = new Graph();
const force = new D3ForceLayout();

const supervisor = new Supervisor(graph, force, { iterations: 1000 });
supervisor.on('layoutend', (positions) => {
  // 进行首次渲染,并绑定节点拖拽事件
  createNodesEdges(positions);
  // 更新图模型中的数据
  graph.mergeNodeData();
});
supervisor.start();

当对单个节点进行拖拽交互时:

function moveAt(target, canvasX, canvasY) {
  // 更新图模型当前节点的数据
  graph.mergeNodeData(positions.nodes[i].id, {
    x: canvasX - shiftX,
    y: canvasY - shiftY,
  });

  // 停止当前布局
  force.stop();
  // 开始新的布局计算
  force.assign(graph, {
    center: [200, 200], // The center of the graph by default
    preventOverlap: true,
    nodeSize: 20,
    onTick: (positions) => {
      updateNodesEdges(positions);
    },
  });
}

在 Worker 中执行 static layout:

const graph = new Graph();
const layout = new D3ForceLayout();
layout.stop();
layout.tick(1000);
// 通知主进程计算完成

之前在 layout 中的使用方式:https://github.com/antvis/layout/blob/master/src/layout/force/force.ts#L232-L248

请问有没有更详细的文档

请问一下我这边在哪里找到相关layout算法使用文档。我看了关闭issues里面的文档 并不是很全面
比如forceLayout布局返回是什么呢?如何使用呢?
感谢!

WebWorker 方案讨论

问题背景

在 graphpology 中只有 noverlap / force / forceatlas2 支持在 WebWorker 中运行。
但我们希望所有布局算法都可以,无论是否是 iterative,至少可以做到不阻塞主线程运行。

此外我们希望在工程、使用体验上作出以下改进:

  • 算法主体部分不出现 WebWorker 相关代码,保持纯粹
  • 提供易用的 WebWorker 相关控制逻辑,例如启动、停止等
  • 开发调试友好,不需要发线上 UMD

设计思路

提供一个统一的监视器(这个名字也是从 graphology 中看来的),主线程和 WebWorker 线程协作过程如下:

  • 主线程 创建 Supervisor,接收一个图模型和算法作为参数
  • 主线程 创建 WebWorker 并与之通信。在 payload 中带上当前执行的算法、参数,图节点/边数据(转换成 ArrayBuffer,通过 Transferables 共享控制权),通过 postMessage 向 WebWorker 发送消息
  • WebWorker 接收到事件上携带的 payload 后,同步创建一个对应的算法并执行,返回计算结果(可以是 Transferables)
  • 主线程 接收计算结果,触发对应生命周期事件(例如 tick layoutend 等)

API

创建一个“监视器”,接收一个图模型和算法作为参数:

const graph = new Graph();
const layout = new CircularLayout();

const supervisor = new Supervisor(graph, layout, { auto: true });

流程控制:

supervisor.start();
supervisor.stop();
supervisor.kill();

事件。既然是一个异步的计算过程,就需要通知主线程当前的计算状态,例如单次迭代完成、全部计算完成等:

supervisor.on('tick', (positions) => {
});
supervisor.on('layoutend', (positions) => {
});

实现

之前创建 Worker 是在 G6 代码中完成的,会存在有一些局限性:
https://github.com/antvis/G6/blob/master/packages/pc/src/layout/worker/work.ts#L5

  • 如果想单独使用 @antv/layout,需要重复实现一遍 WebWorker 相关的逻辑
  • 目前在 Worker 代码中使用 importScripts 直接引用线上的 layout UMD 版本,无网络环境下就无法使用了,也不便于调试(需要把新版本发到线上)

目前使用 Webpack + workerize-loader 的方案,比照最初设想的 template 替换可以说是好处多多:

  • 可以将代码内联到 WebWorker 中
  • 走 Webpack 编译,因此可以使用正常的高级语法,开发调试时也可以被 watch
  • workerize-loader 封装了 Worker 的创建与通信代码,还可以通过 @naoak/workerize-transferable 使用 Transferables
import worker from "workerize-loader?inline!./worker";

主线程和 WebWorker 通信时,需要告知所需的信息,便于在 Worker 中同步创建 Graph 和 Layout 算法并执行,目前携带的数据如下:

  • layout 包含算法名称、参数。Worker 据此可以同步创建对应的 Layout 对象
  • nodes/edges 目前是直接把 Graph 上的原始节点边数据带过去,是这么获取的 nodes: this.graph.getAllNodes()。但考虑到 Transferables,需要将数据转换成线性存储,例如:const arraybufferWithNodesEdges = graphToByteArrays(this.graph); // Float32Array
export interface Payload {
  layout: {
    id: string;
    options: any;
  };
  nodes: Node<any>[];
  edges: Edge<any>[];
}

目前的实现:

目前存在的问题

体积问题

相比之前运行时通过 importScripts 的方式加载 Layout UMD,目前由于是内联的,体积会变大,可以近似认为是原来的两倍。

自定义布局

目前 Worker 代码是构建时产生的,所以运行时的自定义布局即使可以“注册”,也无法添加到已经生成好的 Worker 代码里,也就无法使用 Supervisor 的功能了。

Transferables

我看到 graphology 的三个 Worker 实现中都使用了这个特性,简单来说就是主线程和 Worker 共享同一块线性内存的控制权:
https://github.com/graphology/graphology/blob/master/src/layout-forceatlas2/webworker.tpl.js#L29-L34

NODES = new Float32Array(data.nodes);

self.postMessage(
  {
    nodes: NODES.buffer
  },
  [NODES.buffer]
);

在我们目前的方案中要使用这个特性也很简单,借助 @naoak/workerize-transferable 就可以完成,例如在 Worker 侧的代码如下:

import { setupTransferableMethodsOnWorker } from "@naoak/workerize-transferable";
setupTransferableMethodsOnWorker({
  // The name of function which use some transferables.
  calculateLayout: {
    // Specify an instance of the function
    fn: calculateLayout,
    // Pick a transferable object from the result which is an instance of Float32Array
    pickTransferablesFromResult: (result) => [result[1].buffer],
  },
});

剩下的问题就是如何设计线性内存的结构,合理存储节点和边的数据。
在 graphology 中不同的算法有不同的结构,例如 forceatlas2:
https://github.com/graphology/graphology/blob/master/src/layout-forceatlas2/helpers.js#L117

var NodeMatrix = new Float32Array(order * PPN);
var EdgeMatrix = new Float32Array(size * PPE);

graph.forEachNode(function (node, attr) {
  // Node index
  index[node] = j;

  // Populating byte array
  NodeMatrix[j] = attr.x;
  NodeMatrix[j + 1] = attr.y;
  NodeMatrix[j + 2] = 0; // dx
  NodeMatrix[j + 3] = 0; // dy
  NodeMatrix[j + 4] = 0; // old_dx
  NodeMatrix[j + 5] = 0; // old_dy
  NodeMatrix[j + 6] = 1; // mass
  NodeMatrix[j + 7] = 1; // convergence
  NodeMatrix[j + 8] = attr.size || 1;
  NodeMatrix[j + 9] = attr.fixed ? 1 : 0;
  j += PPN;
});

因此每个 Layout 实现需要实现自己的 graphToByteArray 方法。

ESM

目前用 Webpack 生成的是 UMD + 类型文件,那 ESM 应该咋生成呢?如果是用 tsc 的话,肯定不认识这样带 webpack loader 的语法:

import worker from "workerize-loader?inline!./worker";

截屏2023-01-06 上午10 10 34

Edge类型问题

当前使用的版本:0.1.19

我是在x6(版本:1.28.1)中使用到这个库的。

x6中定义一条边的source和target允许使用 TerminalCellData ,这样可以定义连接桩。

  interface TerminalCellData extends SetCellTerminalArgs {
      cell: string;
      port?: string;
  }

而layout库里定义一条边,source和target只能为string

export interface Edge {
    source: string;
    target: string;
}

期望两边类型能够统一。

ps: 我暂时是通过断言绕过去类型问题,运行时不报错。

dagre布局

ranksep 和 nodesep 的单位不是 px
nodesepFunc 和 ranksepFunc 设置的 间距并不是根据不同节点而不同,而是层间距都为最大值

Need a way to create a Layout from the port of a node

It is clear that the model object can specify nodes and edges, and when the layout is created from it, it works perfectly.
But is there a way to specify ports within the nodes - and then also specify edges that connects separate ports.

Can this use-case be solved?

dist 文件依赖 lodash 导致报错

服务端用 vite 打包后包含了 dist 目录下的内容,开发环境没问题

image

排查发现非 commonjs 打包直接用了 window._ 来获取 lodash,求兼容~

image

所用版本:v0.1.22,文件目录:node_modules/@antv/layout/dist/layout.min.js

FruchtermanLayout 需要加一个引力系数

现在引力系数是由画布面积和点数量决定的,而且和斥力系数公用一个。。。导致没法分开调节
https://github.com/antvis/layout/blob/master/src/layout/fruchterman.ts#L195

建议 加一个引力系数 attractive 在这里*common
https://github.com/antvis/layout/blob/master/src/layout/fruchterman.ts#L195
或者在
https://github.com/antvis/layout/blob/master/src/layout/fruchterman.ts#L267
这里加个 node.x *= scaleKey,整体缩放,让画布里的点尺寸可以适应
(gpu的也希望同步加些调节参数)

GridLayout init model error: nodeSize is not a function or its return value is not iterable

问题描述

在使用 ANTV X6 集成 Layout ,使用 GridLayout 初始化模型数据时会出现异常:nodeSize is not a function or its return value is not iterable,尝试 Copy 官方源码实验也会出现同样的异常。

尝试使用 Dagre、Circle 的布局模式没有出现异常

版本依赖

场景复现

尝试将自定义的 er-rect 换成标准的 rect 依然报错

const graph = new Graph({
    container: document.getElementById('diagram-container'),
    grid: true,
    panning: true,
    snapline: true,
    selecting: {
        enabled: true,
        showNodeSelectionBox: true,
    },
    connecting: {
        snap: true,
        createEdge() {
            return graph.createEdge({
                shape: 'er-edge',
                strokeDasharray: 5,
                zIndex: 1
            })
        }
    },
})

const model = {
    nodes: [
        {
            "id":"departments",
            "shape":"er-rect",
            "size":{
                "width":224,
                "height":40
            },
            "width":224,
            "height":40,
            "label":"departments"
        }
    ],
    edges: []
}
const gridLayout = new GridLayout({
    type: 'grid',
})
const newModel = gridLayout.layout(model)   // throw error: nodeSize is not a function or its return value is not iterable
console.log(newModel)
graph.fromJSON(newModel)

异常日志

grid.js?8257:170 Uncaught (in promise) TypeError: nodeSize is not a function or its return value is not iterable
    at eval (grid.js?8257:170:1)
    at Array.forEach (<anonymous>)
    at GridLayout.execute (grid.js?8257:164:1)
    at GridLayout.layout (base.js?08f0:16:1)
    at Proxy.rendendarByAutoLayout (Diagram.vue?076d:179:1)
    at Proxy.init (Diagram.vue?076d:73:1)
    at Proxy.mounted (Diagram.vue?076d:67:1)
    at callWithErrorHandling (runtime-core.esm-bundler.js?d2dd:6737:1)
    at callWithAsyncErrorHandling (runtime-core.esm-bundler.js?d2dd:6746:1)
    at Array.hook.__weh.hook.__weh (runtime-core.esm-bundler.js?d2dd:1970:1)

报错代码片段

if (preventOverlap || paramNodeSpacing) {
    const nodeSpacing = getFuncByUnknownType(10, paramNodeSpacing);
    const nodeSize = getFuncByUnknownType(30, paramNodeSize, false);
    layoutNodes.forEach((node) => {
        if (!node.x || !node.y) {
            // for bb
            node.x = 0;
            node.y = 0;
        }
// ------------------------------------------------>
        const [nodew = 30, nodeh = 30] = nodeSize(node);  // nodeSize is not a function or its return value is not iterable
// <------------------------------------------------
        const p = nodeSpacing !== undefined ? nodeSpacing(node) : preventOverlapPadding;
        const w = nodew + p;
        const h = nodeh + p;
        self.cellWidth = Math.max(self.cellWidth, w);
        self.cellHeight = Math.max(self.cellHeight, h);
    });
}

依赖@antv/layout,使用装饰器,会导致界面404

"@antv/layout"依赖的库 "@antv/g-webgpu": "0.5.5",存在不,会导致使用装饰器的页面挂掉,请帮助尽快修复下
bug修复地址:rbuckton/reflect-metadata#107

exporter("deleteMetadata", deleteMetadata);
    function DecorateConstructor(decorators, target) {
        for (var i = decorators.length - 1; i >= 0; --i) {
            var decorator = decorators[i];
            var decorated = decorator(target);
            if (!IsUndefined(decorated) && !IsNull(decorated)) {
                if (!IsConstructor(decorated))
                    throw new TypeError();
                target = decorated;
            }
        }
        return target;
    }

gridLayout.layout(model)无法解析model;

使用graph.toJSON导出数据后, 用该数据作为model, 将报错。
可能是节点上有连接桩的话不支持布局?
model数据结构
{"nodes":[{"position":{"x":210,"y":210},"size":{"width":40,"height":40},"attrs":{"text":{"fontSize":12,"fill":"#fff","text":"开始"}},"shape":"custom-start","data":{"content":""},"ports":{"groups":{"out":{"position":"right","label":{"position":"bottom"},"attrs":{"circle":{"r":6,"magnet":true,"stroke":"#1d92ff","strokeWidth":1,"fill":"#fff","style":{"visibility":"hidden"}}}}},"items":[{"group":"out","id":"54296695-fcc4-4416-a97f-2a38844b2e1d"}]},"id":"9c9675d9-3db9-4439-b89b-532397042d46","zIndex":1},{"position":{"x":367,"y":213},"size":{"width":70,"height":34},"attrs":{"text":{"fill":"white","text":"提问"}},"shape":"custom-question","data":{"content":""},"ports":{"groups":{"in":{"position":"left","label":{"position":"top"},"attrs":{"circle":{"r":6,"refX":-4,"magnet":true,"stroke":"#1d92ff","strokeWidth":1,"fill":"#fff","style":{"visibility":"hidden"}}}},"out":{"position":"right","label":{"position":"bottom"},"attrs":{"circle":{"r":6,"refX":4,"magnet":true,"stroke":"#1d92ff","strokeWidth":1,"fill":"#fff","style":{"visibility":"hidden"}}}}},"items":[{"group":"in","id":"15622c39-3528-4abd-9c8f-efc12e1aa162"},{"group":"out","id":"c794c5f5-528b-4e42-942e-1fd833989e6a"}]},"id":"8364a6e7-f777-4381-820d-85c58af4ee92","zIndex":2},{"position":{"x":582,"y":213},"size":{"width":70,"height":34},"attrs":{"text":{"fill":"white","text":"引导"}},"shape":"custom-guide","data":{"content":""},"ports":{"groups":{"in":{"zIndex":999,"position":"left","label":{"position":"top"},"attrs":{"circle":{"r":8,"magnet":true,"stroke":"#1d92ff","strokeWidth":1,"fill":"#fff","style":{"visibility":"hidden"}}}},"out":{"position":"right","label":{"position":"bottom"},"attrs":{"circle":{"r":8,"magnet":true,"stroke":"#1d92ff","strokeWidth":1,"fill":"#fff","style":{"visibility":"hidden"}}}}},"items":[{"group":"in","id":"33b014b9-ef11-433e-a3dc-e7844cb9d404"},{"group":"out","id":"7c67f94a-fd9b-4433-93b4-32207b257367"}]},"id":"a17ffe75-00a6-4a4c-9c90-5adf44a18011","zIndex":3},{"position":{"x":790,"y":80},"size":{"width":70,"height":34},"attrs":{"text":{"fill":"white","text":"回答"}},"shape":"custom-answer","data":{"content":""},"ports":{"groups":{"in":{"position":"left","label":{"position":"top"},"attrs":{"circle":{"r":6,"magnet":true,"stroke":"#1d92ff","strokeWidth":1,"fill":"#fff","style":{"visibility":"hidden"}}}},"out":{"position":"right","label":{"position":"bottom"},"attrs":{"circle":{"r":6,"magnet":true,"stroke":"#1d92ff","strokeWidth":1,"fill":"#fff","style":{"visibility":"hidden"}}}}},"items":[{"group":"in","id":"b9ad93b0-cb2f-4e3e-8b54-ac0f18bff2d4"},{"group":"out","id":"c11955c4-c8fb-4b6d-b6db-b7b97fd05e4d"}]},"id":"a0ffca88-29d8-4718-a982-70f06aa1b091","zIndex":4},{"position":{"x":790,"y":370},"size":{"width":70,"height":34},"attrs":{"text":{"fill":"white","text":"回答"}},"shape":"custom-answer","data":{"content":""},"ports":{"groups":{"in":{"position":"left","label":{"position":"top"},"attrs":{"circle":{"r":6,"magnet":true,"stroke":"#1d92ff","strokeWidth":1,"fill":"#fff","style":{"visibility":"hidden"}}}},"out":{"position":"right","label":{"position":"bottom"},"attrs":{"circle":{"r":6,"magnet":true,"stroke":"#1d92ff","strokeWidth":1,"fill":"#fff","style":{"visibility":"hidden"}}}}},"items":[{"group":"in","id":"b9ad93b0-cb2f-4e3e-8b54-ac0f18bff2d4"},{"group":"out","id":"c11955c4-c8fb-4b6d-b6db-b7b97fd05e4d"}]},"id":"06338735-68f4-41aa-80c4-dcb8c1604414","zIndex":5},{"position":{"x":980,"y":210},"size":{"width":40,"height":40},"attrs":{"text":{"fontSize":12,"fill":"white","text":"结束"}},"shape":"custom-end","data":{"content":""},"ports":{"groups":{"in":{"position":"left","label":{"position":"bottom"},"attrs":{"circle":{"r":6,"magnet":true,"stroke":"#1d92ff","strokeWidth":1,"fill":"#fff","style":{"visibility":"hidden"}}}}},"items":[{"group":"in","id":"13f3b0b5-6849-4c49-9f60-fabf1bd2fccb"}]},"id":"a904b5f2-041f-46e9-8007-adc7e837fd5d","zIndex":6}],"edges":[{"shape":"edge","attrs":{"line":{"stroke":"#a0a0a0","targetMarker":{"name":"classic","size":7}}},"id":"2d8d8213-0e85-4974-b490-62cbd8bef795","zIndex":7,"data":{"content":""},"labels":[""],"source":{"cell":"9c9675d9-3db9-4439-b89b-532397042d46","port":"54296695-fcc4-4416-a97f-2a38844b2e1d"},"target":{"cell":"8364a6e7-f777-4381-820d-85c58af4ee92","port":"15622c39-3528-4abd-9c8f-efc12e1aa162"}},{"shape":"edge","attrs":{"line":{"stroke":"#a0a0a0","targetMarker":{"name":"classic","size":7}}},"id":"de8df879-feb1-458e-9b91-f03850e32bdc","zIndex":8,"data":{"content":""},"labels":[""],"source":{"cell":"a0ffca88-29d8-4718-a982-70f06aa1b091","port":"c11955c4-c8fb-4b6d-b6db-b7b97fd05e4d"},"target":{"cell":"a904b5f2-041f-46e9-8007-adc7e837fd5d","port":"13f3b0b5-6849-4c49-9f60-fabf1bd2fccb"}},{"shape":"edge","attrs":{"line":{"stroke":"#a0a0a0","targetMarker":{"name":"classic","size":7}}},"id":"4bb6471e-9c41-4b81-8766-81558af52448","zIndex":9,"data":{"content":""},"labels":[""],"source":{"cell":"06338735-68f4-41aa-80c4-dcb8c1604414","port":"c11955c4-c8fb-4b6d-b6db-b7b97fd05e4d"},"target":{"cell":"a904b5f2-041f-46e9-8007-adc7e837fd5d","port":"13f3b0b5-6849-4c49-9f60-fabf1bd2fccb"}},{"shape":"edge","attrs":{"line":{"stroke":"#a0a0a0","targetMarker":{"name":"classic","size":7}}},"id":"8a3505c7-7c5c-4f1b-94f7-e76261c82ac8","zIndex":10,"data":{"content":""},"labels":[""],"source":{"cell":"a17ffe75-00a6-4a4c-9c90-5adf44a18011","port":"7c67f94a-fd9b-4433-93b4-32207b257367"},"target":{"cell":"06338735-68f4-41aa-80c4-dcb8c1604414","port":"b9ad93b0-cb2f-4e3e-8b54-ac0f18bff2d4"}},{"shape":"edge","attrs":{"line":{"stroke":"#a0a0a0","targetMarker":{"name":"classic","size":7}}},"id":"6d6d6ead-2aed-45d6-879d-0ca0c4f71a14","zIndex":11,"data":{"content":""},"labels":[""],"source":{"cell":"a17ffe75-00a6-4a4c-9c90-5adf44a18011","port":"7c67f94a-fd9b-4433-93b4-32207b257367"},"target":{"cell":"a0ffca88-29d8-4718-a982-70f06aa1b091","port":"b9ad93b0-cb2f-4e3e-8b54-ac0f18bff2d4"}},{"shape":"edge","attrs":{"line":{"stroke":"#a0a0a0","targetMarker":{"name":"classic","size":7}}},"id":"21c00165-c757-4c1c-afb6-9de56ef9312c","zIndex":12,"data":{"content":""},"labels":[""],"source":{"cell":"8364a6e7-f777-4381-820d-85c58af4ee92","port":"c794c5f5-528b-4e42-942e-1fd833989e6a"},"target":{"cell":"a17ffe75-00a6-4a4c-9c90-5adf44a18011","port":"33b014b9-ef11-433e-a3dc-e7844cb9d404"}}]}

> vue.esm.js?a026:1897 Error: Not possible to find intersection inside of the rectangle

at Object.intersectRect (util.js?b50e:108)
at eval (layout.js?7a9c:271)
at arrayEach (_arrayEach.js?8057:15)
at Object.forEach (forEach.js?6cd4:38)
at assignNodeIntersects (layout.js?7a9c:258)
at eval (layout.js?7a9c:55)
at notime (util.js?b50e:237)
at runLayout (layout.js?7a9c:55)
at eval (layout.js?7a9c:25)
at notime (util.js?b50e:237)

v0.1.24 layout/src/layout/dagre/src/graphlib.ts

// @ts-ignore
import glib from '@dagrejs/graphlib';

let graphlib = glib;


if (!graphlib && typeof window !== "undefined") {
  graphlib = (window as any).graphlib;
}
// @ts-ignore
(Array as any).prototype.flat = function(count) {
  let c = count || 1;
  let len = this.length;
  let ret: any = [];
  if (this.length == 0) return this;
  while (c--) {
    let _arr = [];
    let flag = false;
    if (ret.length == 0) {
      flag = true;
      for (let i = 0; i < len; i++) {
        if (this[i] instanceof Array) {
          ret.push(...this[i]);
        } else {
          ret.push(this[i]);
        }
      }
    } else {
      for (let i = 0; i < ret.length; i++) {
        if (ret[i] instanceof Array) {
          flag = true;
          _arr.push(...ret[i]);
        } else {
          _arr.push(ret[i]);
        }
      }
      ret = _arr;
    }
    if (!flag && c == Infinity) {
      break;
    }
  }
  return ret;
};

export default graphlib;

这个垫片在原数组为空的多维数组,且入参为Infinity的时候,会导致js死循环
Example:
[[], [], []].flat(Infinity)

registerLayout对自定义布局不支持

目前最新layout中export的layouts是固定枚举的,导致在用registerLayout自定义布局后,无法创建自定义布局实例,提示"The layout method: '" + layoutType + "' does not exist! Please specify it first."
c45386040239410e6a551c3b7347563e
3829cac8bd5e03c7f8ab5f51d5d55260

之前版本中代码如下所示,可以成功创建自定义布局
9e675fa9e5c46db12ff13995cd11e2b1

这里是否能修改为原来的样子,或者自定义布局的使用方式是否变了, 应该怎么使用

工程改进建议

可以直接基于这个模板:https://github.com/antvis/template

  • npm 包含上 src ant-design/ant-design-charts#786
  • 去除 tslint,已经不维护了,改成 eslint,参考模板
  • 一些依赖好像并没有直接使用,比如 g-webgpu ,是否去除 #43
  • 如果定位是和 scale 一样,底层依赖,且可以社区使用,建议 readme 和文档参考 scale 补充详细
  • 这个代码的设计上是否可以去掉 register 和 unregister 的概念,全部保持是纯函数的代码片段,并设置 sideEffects: false

可以提供正交布局(Orthogonal layout)算法吗

效果可参考yFiles的正交布局算法:https://live.yworks.com/demos/layout/layoutstyles/
image

关键要求:

  1. 节点和节点之间的连线是直线或正交直角连线
  2. 连线尽量避免交叉,某些场景下可以使用正交自动布局实现连线0交叉,比如小数据量的ER图、关系图谱等
  3. 连线转折点不超过2个
  4. 主要应用在非大数据场景(X6)

尝试过ERLayout,发现效果并不好:
0. ERLayout的options如果width、height和最小nodeSize没有控制好,会导致GridLayout无法绘制足够的网格来摆放所有节点,layout算法没有做检查,控制台报错信息也不清晰,需要深入debug才能找到原因

  1. 节点位置是根据GridLayout实现,并且算出来的结果并不稳定,每次都有可能有差异
  2. 连线没有避免交叉,这样即使设置连线为正交也会交叉
  3. erlayout里面有一个mysqlWorkbench.ts,不知道是不是实现了MySQL Workbench ER图自动布局的算法,如果是有使用方法吗?

layoutClass is not a constructor

在 X6 里使用最新的 layout 0.1.9 版本报错。

repo

import { Layout } from "@antv/layout"; // umd模式下, const { Layout } = window.AntVLayout

const model = {
  nodes: [
    {
      id: "node1",
    },
    {
      id: "node2",
    },
  ],
  edges: [
    {
      source: "node1",
      target: "node2",
    },
  ],
};

const gridLayout = new Layout({
  type: "grid",
  width: 600,
  height: 400,
  // center: [300, 200],
  rows: 4,
  cols: 4,
});

const newModel = gridLayout.layout(model);

export default function () {
  return <div></div>;
}

ComboCombinedLayout returning different results than g6 layout comboCombined

I'm attempting to compute the layout on the server and want to gather the x, y coordinates of all the nodes/edge/combos but am receiving inconsistent results. Maybe I'm using the library incorrectly?

Code Sandbox: https://codesandbox.io/s/frosty-morning-jv3p68?file=/src/index.js:379-786

Normal comboCombined layout in g6 gives these results:
image

If I try to compute the layout beforehand with this layout library, I get these results:
image

dagre 抖动问题

在使用 darge 排布一个有向无环图的时候 碰到了瓶颈了。

  1. 新增插入节点之后会发生排布抖动,左右两列互跳。(如下图,在左插入新节点之后布局排列会跳到右侧)
  2. 分叉节点都是以底部对齐。(如下图)

E5A8F2DB-99F2-46C4-9DE9-D9DF4E0C5B63

077CE8A6-DDC1-41C6-94D3-F499C750381D

请改进一下dagre布局的算法

这里有一段样例代码,可以嵌入任何一个html的dvi中,我用的是1000px*900px的画幅,可以直接重现这个问题

最后的两个node的的分布明显不合理,两个edge出现了容易让人误解的相交,其实本可以不相交的。

`
import { Graph, Node, Edge } from "@antv/x6";
import { DagreLayout, Model as layoutModel } from "@antv/layout"; // the Model class in @antv/layout conflict the Model class in @antv/x6
const layoutData: layoutModel = {
"nodes": [
{
"id": "1",
"shape": "rect",
"width": 100,
"height": 40,
"attrs": {
"body": {
"fill": "#1E90FF",
"stroke": "#FF4500"
},
"label": {
"text": "start",
"fill": "#000000",
"fontSize": 13
}
},
"x": 35,
"y": 55,
"_order": 0
},
{
"id": "3",
"shape": "rect",
"width": 100,
"height": 40,
"attrs": {
"body": {
"fill": "#FFFF00",
"stroke": "#87CEFA"
},
"label": {
"text": "hr_review",
"fill": "#000000",
"fontSize": 13
}
},
"x": 140,
"y": 635,
"_order": 1
},
{
"id": "2",
"shape": "rect",
"width": 100,
"height": 40,
"attrs": {
"body": {
"fill": "#00FF00",
"stroke": "#FF4500"
},
"label": {
"text": "first_review",
"fill": "#000000",
"fontSize": 13
}
},
"x": 35,
"y": 200,
"_order": 0
},
{
"id": "4",
"shape": "rect",
"width": 100,
"height": 40,
"attrs": {
"body": {
"fill": "#00FF00",
"stroke": "#FF4500"
},
"label": {
"text": "engineer_manager_review",
"fill": "#000000",
"fontSize": 13
}
},
"x": 35,
"y": 490,
"_order": 0
},
{
"id": "5",
"shape": "rect",
"width": 100,
"height": 40,
"attrs": {
"body": {
"fill": "#FFFF00",
"stroke": "#FF4500"
},
"label": {
"text": "engineering_director_review",
"fill": "#000000",
"fontSize": 13
}
},
"x": 35,
"y": 635,
"_order": 0
},
{
"id": "6",
"shape": "rect",
"width": 100,
"height": 40,
"attrs": {
"body": {
"fill": "#FFFFFF",
"stroke": "#FF4500"
},
"label": {
"text": "sales_review",
"fill": "#000000",
"fontSize": 13
}
},
"x": 120,
"y": 345,
"_order": 1
},
{
"id": "7",
"shape": "rect",
"width": 100,
"height": 40,
"attrs": {
"body": {
"fill": "#FFFFFF",
"stroke": "#FF4500"
},
"label": {
"text": "finance_review",
"fill": "#000000",
"fontSize": 13
}
},
"x": 120,
"y": 490,
"_order": 1
}
],
"edges": [
{
"source": "1",
"target": "2",
"router": {
"name": "metro",
"args": {
"padding": 1,
"startDirections": [
"bottom"
],
"endDirections": [
"top"
]
}
},
"connector": {
"name": "rounded",
"args": {
"radius": 8
}
},
"anchor": "center",
"connectionPoint": "anchor",
"allowBlank": false,
"snap": {
"radius": 20
},
"attrs": {
"line": {
"stroke": "#1890ff",
"strokeDasharray": 10,
"targetMarker": "classic",
"style": {
"animation": "ant-line 30s infinite linear"
}
}
},
"shape": "edge"
},
{
"source": "2",
"target": "4",
"router": {
"name": "metro",
"args": {
"padding": 1,
"startDirections": [
"bottom"
],
"endDirections": [
"top"
]
}
},
"connector": {
"name": "rounded",
"args": {
"radius": 8
}
},
"anchor": "center",
"connectionPoint": "anchor",
"allowBlank": false,
"snap": {
"radius": 20
},
"attrs": {
"line": {
"stroke": "#1890ff",
"strokeDasharray": 10,
"targetMarker": "classic",
"style": {
"animation": "ant-line 30s infinite linear"
}
}
},
"shape": "edge"
},
{
"source": "2",
"target": "6",
"router": {
"name": "metro",
"args": {
"padding": 1,
"startDirections": [
"bottom"
],
"endDirections": [
"top"
]
}
},
"connector": {
"name": "rounded",
"args": {
"radius": 8
}
},
"anchor": "center",
"connectionPoint": "anchor",
"allowBlank": false,
"snap": {
"radius": 20
},
"attrs": {
"line": {
"stroke": "#1890ff",
"strokeDasharray": 10,
"targetMarker": "classic",
"style": {
"animation": "ant-line 30s infinite linear"
}
}
},
"shape": "edge"
},
{
"source": "4",
"target": "5",
"router": {
"name": "metro",
"args": {
"padding": 1,
"startDirections": [
"bottom"
],
"endDirections": [
"top"
]
}
},
"connector": {
"name": "rounded",
"args": {
"radius": 8
}
},
"anchor": "center",
"connectionPoint": "anchor",
"allowBlank": false,
"snap": {
"radius": 20
},
"attrs": {
"line": {
"stroke": "#1890ff",
"strokeDasharray": 10,
"targetMarker": "classic",
"style": {
"animation": "ant-line 30s infinite linear"
}
}
},
"shape": "edge"
},
{
"source": "4",
"target": "3",
"router": {
"name": "metro",
"args": {
"padding": 1,
"startDirections": [
"bottom"
],
"endDirections": [
"top"
]
}
},
"connector": {
"name": "rounded",
"args": {
"radius": 8
}
},
"anchor": "center",
"connectionPoint": "anchor",
"allowBlank": false,
"snap": {
"radius": 20
},
"attrs": {
"line": {
"stroke": "#1890ff",
"strokeDasharray": 10,
"targetMarker": "classic",
"style": {
"animation": "ant-line 30s infinite linear"
}
}
},
"shape": "edge"
},
{
"source": "6",
"target": "7",
"router": {
"name": "metro",
"args": {
"padding": 1,
"startDirections": [
"bottom"
],
"endDirections": [
"top"
]
}
},
"connector": {
"name": "rounded",
"args": {
"radius": 8
}
},
"anchor": "center",
"connectionPoint": "anchor",
"allowBlank": false,
"snap": {
"radius": 20
},
"attrs": {
"line": {
"stroke": "#1890ff",
"strokeDasharray": 10,
"targetMarker": "classic",
"style": {
"animation": "ant-line 30s infinite linear"
}
}
},
"shape": "edge"
},
{
"source": "7",
"target": "5",
"router": {
"name": "metro",
"args": {
"padding": 1,
"startDirections": [
"bottom"
],
"endDirections": [
"top"
]
}
},
"connector": {
"name": "rounded",
"args": {
"radius": 8
}
},
"anchor": "center",
"connectionPoint": "anchor",
"allowBlank": false,
"snap": {
"radius": 20
},
"attrs": {
"line": {
"stroke": "#1890ff",
"strokeDasharray": 10,
"targetMarker": "classic",
"style": {
"animation": "ant-line 30s infinite linear"
}
}
},
"shape": "edge"
}
]
}

const dagreLayout = new DagreLayout({
type: "dagre",
rankdir: "TB",
align: "UL",
ranksep: 35,
nodesep: 15,
});
const dagreModel = dagreLayout.layout(layoutData);
graph = new Graph({
container: document.getElementById('container')!,
panning: {
enabled: true,
eventTypes: ["leftMouseDown"],
},
});
graph.fromJSON(dagreModel);
`

GForce布局,GPU布局结果不可用

G6中使用GForce布局

同样的data和config,仅布局方式不同

CPU单线程布局:(gpuEnabled: false, workerEnabled: false)
cpu

GPU布局:(gpuEnabled: true, workerEnabled: false)
gpu
有几条边特别长,不可用

可以用这个仓库复现。 npm i然后npm run dev即可

The package version is inconsistent with the release note

The release note information in the Github repository seems to have stopped updating after version 0.1.3, but the current package version has been updated to 0.1.10, I can't directly know what changes have been made in these versions.

react中使用layout

react中使用layout时,属性无法扩展报错,是不支持react吗
报错信息:Cannot add property x, object is not extensible
报错代码: nodes[i].x = coord.x! + dBegin[0]; nodes[i].y = coord.y! + dBegin[1];

深度较大的数据初始渲染时,dfs递归导致内存溢出

使用g6时遇到了一个问题,如下demo

demo

image

demo中是2000条首尾相连的数据,布局模式使用Dagre

定位到 https://github.com/antvis/layout/blob/master/src/layout/dagre/src/order/init-order.ts 里面的方法dfs

const dfs = (v: string) => {
    if (visited.hasOwnProperty(v)) return;
    visited[v] = true;
    const node = g.node(v);
    if (!isNaN(node.rank as number)) {
      layers[node.rank as number].push(v);
    }
    g.successors(v)?.forEach((child) => dfs(child as any));
  };

此demo中图的深度较大,猜测是递归pending的dfs函数过多导致内存溢出。本地调试改为bfs后不再报错(但导致了初始化时图不居中)

const dfs = (v: string) => {
    let cur = [v];
    while(cur.length) {
      let temp: any[] = [];
      cur.forEach((item: any) => {
        if (visited.hasOwnProperty(item)) return;
        visited[item] = true;
        const node = g.node(item);
        if (!isNaN(node.rank as number)) {
          layers[node.rank as number].push(item);
        }
        temp = [...temp, ...(g.successors(v) || [])];
      });
      cur = temp;
    }
  };

image

不知是否有相关的配置可以解决这个问题,或者说一个不成熟的建议是提供给用户可选dfs或者bfs进行初始布局?

sourcemap 告警

0.1.x版本未输出src文件夹,导致sourcemap找不到src下的文件。升到0.3.x支持@antv/x6 的1.x版本吗

Dagre布局存在的缺陷以及解决方法

两节点双向指向时导致layoutLabel不存在
当A节点和B节点存在双向连接时,会存在第二条双向连接线报错情况
@AntV\layout\es\layout\dagre\src\layout.js内部的updateInputGraph方法中
1667554678284
此处是否需要做出判断进行修正处理
image

G6使用ForceAtlas2Layout布局时center参数不生效

1652752396(1)

const options:ForceAtlas2LayoutOptions = {
center: [600, 400],
kr: 10,
kg: 1,
preventOverlap: true,
type:'forceAtlas2'
}
const layout= new ForceAtlas2Layout(options)

  layout.init({
    nodes: data.nodes,
    edges: data.edges,
  })

  layout.execute()
  
  graph.data(data)

  graph.render()

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.