docs: 📚️ add dag demo (#1515)
This commit is contained in:
@ -0,0 +1,453 @@
|
||||
import React from 'react'
|
||||
import { Graph, Node, Path, Cell } from '@antv/x6'
|
||||
import '@antv/x6-react-shape'
|
||||
import '../index.less'
|
||||
import './index.less'
|
||||
interface NodeStatus {
|
||||
id: string
|
||||
status: 'default' | 'success' | 'failed' | 'running'
|
||||
label?: string
|
||||
}
|
||||
|
||||
const image = {
|
||||
logo: 'https://gw.alipayobjects.com/mdn/rms_43231b/afts/img/A*evDjT5vjkX0AAAAAAAAAAAAAARQnAQ',
|
||||
success:
|
||||
'https://gw.alipayobjects.com/mdn/rms_43231b/afts/img/A*6l60T6h8TTQAAAAAAAAAAAAAARQnAQ',
|
||||
failed:
|
||||
'https://gw.alipayobjects.com/mdn/rms_43231b/afts/img/A*SEISQ6My-HoAAAAAAAAAAAAAARQnAQ',
|
||||
running:
|
||||
'https://gw.alipayobjects.com/mdn/rms_43231b/afts/img/A*t8fURKfgSOgAAAAAAAAAAAAAARQnAQ',
|
||||
}
|
||||
export class AlgoNode extends React.Component<{ node?: Node }> {
|
||||
shouldComponentUpdate() {
|
||||
const { node } = this.props
|
||||
if (node) {
|
||||
if (node.hasChanged('data')) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
render() {
|
||||
const { node } = this.props
|
||||
const data = node?.getData() as NodeStatus
|
||||
const { label, status = 'default' } = data
|
||||
|
||||
return (
|
||||
<div className={`node ${status}`}>
|
||||
<img src={image.logo} />
|
||||
<span className="label">{label}</span>
|
||||
<span className="status">
|
||||
{status === 'success' && <img src={image.success} />}
|
||||
{status === 'failed' && <img src={image.failed} />}
|
||||
{status === 'running' && <img src={image.running} />}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Graph.registerNode(
|
||||
'dag-node',
|
||||
{
|
||||
inherit: 'react-shape',
|
||||
width: 180,
|
||||
height: 36,
|
||||
component: <AlgoNode />,
|
||||
ports: {
|
||||
groups: {
|
||||
top: {
|
||||
position: 'top',
|
||||
attrs: {
|
||||
circle: {
|
||||
r: 4,
|
||||
magnet: true,
|
||||
stroke: '#C2C8D5',
|
||||
strokeWidth: 1,
|
||||
fill: '#fff',
|
||||
},
|
||||
},
|
||||
},
|
||||
bottom: {
|
||||
position: 'bottom',
|
||||
attrs: {
|
||||
circle: {
|
||||
r: 4,
|
||||
magnet: true,
|
||||
stroke: '#C2C8D5',
|
||||
strokeWidth: 1,
|
||||
fill: '#fff',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
true,
|
||||
)
|
||||
|
||||
Graph.registerEdge(
|
||||
'dag-edge',
|
||||
{
|
||||
inherit: 'edge',
|
||||
attrs: {
|
||||
line: {
|
||||
stroke: '#C2C8D5',
|
||||
strokeWidth: 1,
|
||||
targetMarker: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
true,
|
||||
)
|
||||
|
||||
Graph.registerConnector(
|
||||
'algo-connector',
|
||||
(s, e) => {
|
||||
const offset = 4
|
||||
const deltaY = Math.abs(e.y - s.y)
|
||||
const control = Math.floor((deltaY / 3) * 2)
|
||||
|
||||
const v1 = { x: s.x, y: s.y + offset + control }
|
||||
const v2 = { x: e.x, y: e.y - offset - control }
|
||||
|
||||
return Path.normalize(
|
||||
`M ${s.x} ${s.y}
|
||||
L ${s.x} ${s.y + offset}
|
||||
C ${v1.x} ${v1.y} ${v2.x} ${v2.y} ${e.x} ${e.y - offset}
|
||||
L ${e.x} ${e.y}
|
||||
`,
|
||||
)
|
||||
},
|
||||
true,
|
||||
)
|
||||
|
||||
const data = [
|
||||
{
|
||||
id: '1',
|
||||
shape: 'dag-node',
|
||||
x: 290,
|
||||
y: 110,
|
||||
data: {
|
||||
label: '读数据',
|
||||
status: 'success',
|
||||
},
|
||||
ports: [
|
||||
{
|
||||
id: '1-1',
|
||||
group: 'bottom',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
shape: 'dag-node',
|
||||
x: 290,
|
||||
y: 225,
|
||||
data: {
|
||||
label: '读数据',
|
||||
status: 'success',
|
||||
},
|
||||
ports: [
|
||||
{
|
||||
id: '2-1',
|
||||
group: 'top',
|
||||
},
|
||||
{
|
||||
id: '2-2',
|
||||
group: 'bottom',
|
||||
},
|
||||
{
|
||||
id: '2-3',
|
||||
group: 'bottom',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
shape: 'dag-node',
|
||||
x: 170,
|
||||
y: 350,
|
||||
data: {
|
||||
label: '读数据',
|
||||
status: 'success',
|
||||
},
|
||||
ports: [
|
||||
{
|
||||
id: '3-1',
|
||||
group: 'top',
|
||||
},
|
||||
{
|
||||
id: '3-2',
|
||||
group: 'bottom',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
shape: 'dag-node',
|
||||
x: 450,
|
||||
y: 350,
|
||||
data: {
|
||||
label: '读数据',
|
||||
status: 'success',
|
||||
},
|
||||
ports: [
|
||||
{
|
||||
id: '4-1',
|
||||
group: 'top',
|
||||
},
|
||||
{
|
||||
id: '4-2',
|
||||
group: 'bottom',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
shape: 'dag-edge',
|
||||
source: {
|
||||
cell: '1',
|
||||
port: '1-1',
|
||||
},
|
||||
target: {
|
||||
cell: '2',
|
||||
port: '2-1',
|
||||
},
|
||||
zIndex: 0,
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
shape: 'dag-edge',
|
||||
source: {
|
||||
cell: '2',
|
||||
port: '2-2',
|
||||
},
|
||||
target: {
|
||||
cell: '3',
|
||||
port: '3-1',
|
||||
},
|
||||
zIndex: 0,
|
||||
},
|
||||
{
|
||||
id: '7',
|
||||
shape: 'dag-edge',
|
||||
source: {
|
||||
cell: '2',
|
||||
port: '2-3',
|
||||
},
|
||||
target: {
|
||||
cell: '4',
|
||||
port: '4-1',
|
||||
},
|
||||
zIndex: 0,
|
||||
},
|
||||
]
|
||||
|
||||
const nodeStatusList = [
|
||||
[
|
||||
{
|
||||
id: '1',
|
||||
status: 'running',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
status: 'default',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
status: 'default',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
status: 'default',
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
id: '1',
|
||||
status: 'success',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
status: 'running',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
status: 'default',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
status: 'default',
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
id: '1',
|
||||
status: 'success',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
status: 'success',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
status: 'running',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
status: 'running',
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
id: '1',
|
||||
status: 'success',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
status: 'success',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
status: 'success',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
status: 'failed',
|
||||
},
|
||||
],
|
||||
]
|
||||
export default class Example extends React.Component {
|
||||
private container: HTMLDivElement
|
||||
|
||||
componentDidMount() {
|
||||
const graph: Graph = new Graph({
|
||||
container: this.container,
|
||||
width: 800,
|
||||
height: 600,
|
||||
panning: {
|
||||
enabled: true,
|
||||
eventTypes: ['leftMouseDown', 'mouseWheel'],
|
||||
},
|
||||
mousewheel: {
|
||||
enabled: true,
|
||||
modifiers: 'ctrl',
|
||||
factor: 1.1,
|
||||
maxScale: 1.5,
|
||||
minScale: 0.5,
|
||||
},
|
||||
highlighting: {
|
||||
magnetAdsorbed: {
|
||||
name: 'stroke',
|
||||
args: {
|
||||
attrs: {
|
||||
fill: '#fff',
|
||||
stroke: '#31d0c6',
|
||||
strokeWidth: 4,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
connecting: {
|
||||
snap: true,
|
||||
allowBlank: false,
|
||||
allowLoop: false,
|
||||
highlight: true,
|
||||
connector: 'algo-connector',
|
||||
connectionPoint: 'anchor',
|
||||
anchor: 'center',
|
||||
validateMagnet({ magnet }) {
|
||||
return magnet.getAttribute('port-group') !== 'top'
|
||||
},
|
||||
createEdge() {
|
||||
return graph.createEdge({
|
||||
shape: 'dag-edge',
|
||||
attrs: {
|
||||
line: {
|
||||
strokeDasharray: '5 5',
|
||||
},
|
||||
},
|
||||
zIndex: -1,
|
||||
})
|
||||
},
|
||||
},
|
||||
selecting: {
|
||||
enabled: true,
|
||||
multiple: true,
|
||||
rubberEdge: true,
|
||||
rubberNode: true,
|
||||
modifiers: 'shift',
|
||||
rubberband: true,
|
||||
},
|
||||
})
|
||||
|
||||
graph.on('edge:connected', ({ edge }) => {
|
||||
edge.attr({
|
||||
line: {
|
||||
strokeDasharray: '',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
graph.on('node:change:data', ({ node }) => {
|
||||
const edges = graph.getIncomingEdges(node)
|
||||
const { status } = node.getData() as NodeStatus
|
||||
edges?.forEach((edge) => {
|
||||
if (status === 'running') {
|
||||
edge.attr('line/strokeDasharray', 5)
|
||||
edge.attr('line/style/animation', 'running-line 30s infinite linear')
|
||||
} else {
|
||||
edge.attr('line/strokeDasharray', '')
|
||||
edge.attr('line/style/animation', '')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// 初始化节点/边
|
||||
const init = (data: Cell.Metadata[]) => {
|
||||
const cells: Cell[] = []
|
||||
data.forEach((item) => {
|
||||
if (item.shape === 'dag-node') {
|
||||
cells.push(graph.createNode(item))
|
||||
} else {
|
||||
cells.push(graph.createEdge(item))
|
||||
}
|
||||
})
|
||||
graph.resetCells(cells)
|
||||
}
|
||||
|
||||
// 显示节点状态
|
||||
const showNodeStatus = async (statusList: NodeStatus[][]) => {
|
||||
const status = statusList.shift()
|
||||
status?.forEach((item) => {
|
||||
const { id, status } = item
|
||||
const node = graph.getCellById(id)
|
||||
const data = node.getData() as NodeStatus
|
||||
node.setData({
|
||||
...data,
|
||||
status: status,
|
||||
})
|
||||
})
|
||||
setTimeout(() => {
|
||||
showNodeStatus(statusList)
|
||||
}, 3000)
|
||||
}
|
||||
|
||||
init(data)
|
||||
showNodeStatus(nodeStatusList)
|
||||
}
|
||||
|
||||
refContainer = (container: HTMLDivElement) => {
|
||||
this.container = container
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="x6-graph-wrap">
|
||||
<div ref={this.refContainer} className="x6-graph" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
84
examples/x6-example-features/src/pages/case/index.less
Normal file
84
examples/x6-example-features/src/pages/case/index.less
Normal file
@ -0,0 +1,84 @@
|
||||
.node {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #fff;
|
||||
border: 1px solid #c2c8d5;
|
||||
border-left: 4px solid #1890ff;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 5px 1px rgba(0, 0, 0, 0.06);
|
||||
img {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
flex-shrink: 0;
|
||||
margin-left: 8px;
|
||||
}
|
||||
.label {
|
||||
display: inline-block;
|
||||
flex-shrink: 0;
|
||||
width: 104px;
|
||||
margin-left: 8px;
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
}
|
||||
.status {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
&.success {
|
||||
border-left: 4px solid #52c41a;
|
||||
}
|
||||
&.failed {
|
||||
border-left: 4px solid #ff4d4f;
|
||||
}
|
||||
&.running .status img {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
}
|
||||
|
||||
.x6-node-selected {
|
||||
.node {
|
||||
border-color: #1890ff;
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 0 0 4px #d4e8fe;
|
||||
}
|
||||
.node.success {
|
||||
border-color: #52c41a;
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 0 0 4px #ccecc0;
|
||||
}
|
||||
.node.failed {
|
||||
border-color: #ff4d4f;
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 0 0 4px #fedcdc;
|
||||
}
|
||||
}
|
||||
|
||||
.x6-edge:hover {
|
||||
path:nth-child(2) {
|
||||
stroke: #1890ff;
|
||||
stroke-width: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.x6-edge-selected {
|
||||
path:nth-child(2) {
|
||||
stroke: #1890ff;
|
||||
stroke-width: 1.5px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes running-line {
|
||||
to {
|
||||
stroke-dashoffset: -1000;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
121
sites/x6-sites/examples/data/dag.json
Normal file
121
sites/x6-sites/examples/data/dag.json
Normal file
@ -0,0 +1,121 @@
|
||||
[
|
||||
{
|
||||
"id": "1",
|
||||
"shape": "dag-node",
|
||||
"x": 290,
|
||||
"y": 110,
|
||||
"data": {
|
||||
"label": "读数据",
|
||||
"status": "success"
|
||||
},
|
||||
"ports": [
|
||||
{
|
||||
"id": "1-1",
|
||||
"group": "bottom"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"shape": "dag-node",
|
||||
"x": 290,
|
||||
"y": 225,
|
||||
"data": {
|
||||
"label": "逻辑回归",
|
||||
"status": "success"
|
||||
},
|
||||
"ports": [
|
||||
{
|
||||
"id": "2-1",
|
||||
"group": "top"
|
||||
},
|
||||
{
|
||||
"id": "2-2",
|
||||
"group": "bottom"
|
||||
},
|
||||
{
|
||||
"id": "2-3",
|
||||
"group": "bottom"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "3",
|
||||
"shape": "dag-node",
|
||||
"x": 170,
|
||||
"y": 350,
|
||||
"data": {
|
||||
"label": "模型预测",
|
||||
"status": "success"
|
||||
},
|
||||
"ports": [
|
||||
{
|
||||
"id": "3-1",
|
||||
"group": "top"
|
||||
},
|
||||
{
|
||||
"id": "3-2",
|
||||
"group": "bottom"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "4",
|
||||
"shape": "dag-node",
|
||||
"x": 450,
|
||||
"y": 350,
|
||||
"data": {
|
||||
"label": "读取参数",
|
||||
"status": "success"
|
||||
},
|
||||
"ports": [
|
||||
{
|
||||
"id": "4-1",
|
||||
"group": "top"
|
||||
},
|
||||
{
|
||||
"id": "4-2",
|
||||
"group": "bottom"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "5",
|
||||
"shape": "dag-edge",
|
||||
"source": {
|
||||
"cell": "1",
|
||||
"port": "1-1"
|
||||
},
|
||||
"target": {
|
||||
"cell": "2",
|
||||
"port": "2-1"
|
||||
},
|
||||
"zIndex": 0
|
||||
},
|
||||
{
|
||||
"id": "6",
|
||||
"shape": "dag-edge",
|
||||
"source": {
|
||||
"cell": "2",
|
||||
"port": "2-2"
|
||||
},
|
||||
"target": {
|
||||
"cell": "3",
|
||||
"port": "3-1"
|
||||
},
|
||||
"zIndex": 0
|
||||
},
|
||||
{
|
||||
"id": "7",
|
||||
"shape": "dag-edge",
|
||||
"source": {
|
||||
"cell": "2",
|
||||
"port": "2-3"
|
||||
},
|
||||
"target": {
|
||||
"cell": "4",
|
||||
"port": "4-1"
|
||||
},
|
||||
"zIndex": 0
|
||||
}
|
||||
]
|
@ -1,396 +0,0 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import insertCss from 'insert-css'
|
||||
import { Tooltip } from 'antd'
|
||||
import { Graph, Node, Platform, Dom } from '@antv/x6'
|
||||
|
||||
// https://codesandbox.io/s/x6-pai-edge-nq3hl
|
||||
|
||||
// 定义节点
|
||||
Graph.registerNode(
|
||||
'algo-node',
|
||||
{
|
||||
inherit: 'rect',
|
||||
attrs: {
|
||||
body: {
|
||||
strokeWidth: 1,
|
||||
stroke: '#108ee9',
|
||||
fill: '#fff',
|
||||
rx: 15,
|
||||
ry: 15,
|
||||
},
|
||||
},
|
||||
portMarkup: [
|
||||
{
|
||||
tagName: 'foreignObject',
|
||||
selector: 'fo',
|
||||
attrs: {
|
||||
width: 10,
|
||||
height: 10,
|
||||
x: -5,
|
||||
y: -5,
|
||||
magnet: 'true',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
ns: Dom.ns.xhtml,
|
||||
tagName: 'body',
|
||||
selector: 'foBody',
|
||||
attrs: {
|
||||
xmlns: Dom.ns.xhtml,
|
||||
},
|
||||
style: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
tagName: 'div',
|
||||
selector: 'content',
|
||||
style: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
true,
|
||||
)
|
||||
|
||||
// 定义边
|
||||
Graph.registerConnector(
|
||||
'algo-edge',
|
||||
(source, target) => {
|
||||
const offset = 4
|
||||
const control = 80
|
||||
const v1 = { x: source.x, y: source.y + offset + control }
|
||||
const v2 = { x: target.x, y: target.y - offset - control }
|
||||
|
||||
return `M ${source.x} ${source.y}
|
||||
L ${source.x} ${source.y + offset}
|
||||
C ${v1.x} ${v1.y} ${v2.x} ${v2.y} ${target.x} ${target.y - offset}
|
||||
L ${target.x} ${target.y}
|
||||
`
|
||||
},
|
||||
true,
|
||||
)
|
||||
|
||||
// 初始化画布
|
||||
const graph = new Graph({
|
||||
grid: true,
|
||||
container: document.getElementById('container'),
|
||||
onPortRendered(args) {
|
||||
// console.log(args)
|
||||
const port = args.port
|
||||
const contentSelectors = args.contentSelectors
|
||||
const container = contentSelectors && contentSelectors.content
|
||||
if (container) {
|
||||
ReactDOM.render(
|
||||
<Tooltip title="port">
|
||||
<div className={`my-port${port.connected ? ' connected' : ''}`} />
|
||||
</Tooltip>,
|
||||
container,
|
||||
)
|
||||
}
|
||||
},
|
||||
highlighting: {
|
||||
nodeAvailable: {
|
||||
name: 'className',
|
||||
args: {
|
||||
className: 'available',
|
||||
},
|
||||
},
|
||||
magnetAvailable: {
|
||||
name: 'className',
|
||||
args: {
|
||||
className: 'available',
|
||||
},
|
||||
},
|
||||
magnetAdsorbed: {
|
||||
name: 'className',
|
||||
args: {
|
||||
className: 'adsorbed',
|
||||
},
|
||||
},
|
||||
},
|
||||
connecting: {
|
||||
snap: true,
|
||||
allowBlank: false,
|
||||
allowLoop: false,
|
||||
highlight: true,
|
||||
sourceAnchor: {
|
||||
name: 'bottom',
|
||||
args: {
|
||||
dx: Platform.IS_SAFARI ? 5 : 0,
|
||||
},
|
||||
},
|
||||
targetAnchor: {
|
||||
name: 'center',
|
||||
args: {
|
||||
dx: Platform.IS_SAFARI ? 5 : 0,
|
||||
},
|
||||
},
|
||||
connectionPoint: 'anchor',
|
||||
connector: 'algo-edge',
|
||||
createEdge() {
|
||||
return graph.createEdge({
|
||||
attrs: {
|
||||
line: {
|
||||
strokeDasharray: '5 5',
|
||||
stroke: '#808080',
|
||||
strokeWidth: 1,
|
||||
targetMarker: {
|
||||
name: 'block',
|
||||
args: {
|
||||
size: '6',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
},
|
||||
validateMagnet({ magnet }) {
|
||||
return magnet.getAttribute('port-group') !== 'in'
|
||||
},
|
||||
validateConnection({ sourceView, targetView, sourceMagnet, targetMagnet }) {
|
||||
// 只能从输出链接桩创建连接
|
||||
if (!sourceMagnet || sourceMagnet.getAttribute('port-group') === 'in') {
|
||||
return false
|
||||
}
|
||||
|
||||
// 只能连接到输入链接桩
|
||||
if (!targetMagnet || targetMagnet.getAttribute('port-group') !== 'in') {
|
||||
return false
|
||||
}
|
||||
|
||||
// 判断目标链接桩是否可连接
|
||||
const portId = targetMagnet.getAttribute('port')!
|
||||
const node = targetView.cell as Node
|
||||
const port = node.getPort(portId)
|
||||
if (port && port.connected) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
graph.on('edge:connected', (args) => {
|
||||
const edge = args.edge
|
||||
const node = args.currentCell as Node
|
||||
const elem = args.currentMagnet as HTMLElement
|
||||
const portId = elem.getAttribute('port') as string
|
||||
|
||||
// 触发 port 重新渲染
|
||||
node.setPortProp(portId, 'connected', true)
|
||||
|
||||
// 更新连线样式
|
||||
edge.attr({
|
||||
line: {
|
||||
strokeDasharray: '',
|
||||
targetMarker: '',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
graph.addNode({
|
||||
x: 380,
|
||||
y: 180,
|
||||
width: 160,
|
||||
height: 30,
|
||||
shape: 'algo-node',
|
||||
label: '归一化',
|
||||
ports: {
|
||||
items: [
|
||||
{ group: 'in', id: 'in1' },
|
||||
{ group: 'in', id: 'in2' },
|
||||
{ group: 'out', id: 'out1' },
|
||||
{ group: 'out', id: 'out2' },
|
||||
],
|
||||
groups: {
|
||||
in: {
|
||||
position: { name: 'top' },
|
||||
zIndex: 1,
|
||||
},
|
||||
out: {
|
||||
position: { name: 'bottom' },
|
||||
zIndex: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const source = graph.addNode({
|
||||
x: 200,
|
||||
y: 50,
|
||||
width: 160,
|
||||
height: 30,
|
||||
shape: 'algo-node',
|
||||
label: 'SQL',
|
||||
ports: {
|
||||
items: [
|
||||
{ group: 'in', id: 'in1' },
|
||||
{ group: 'in', id: 'in2' },
|
||||
{ group: 'out', id: 'out1' },
|
||||
],
|
||||
groups: {
|
||||
in: {
|
||||
position: { name: 'top' },
|
||||
zIndex: 1,
|
||||
},
|
||||
out: {
|
||||
position: { name: 'bottom' },
|
||||
zIndex: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const target = graph.addNode({
|
||||
x: 120,
|
||||
y: 260,
|
||||
width: 160,
|
||||
height: 30,
|
||||
shape: 'algo-node',
|
||||
label: '序列化',
|
||||
ports: {
|
||||
items: [
|
||||
{ group: 'in', id: 'in1', connected: true },
|
||||
{ group: 'in', id: 'in2' },
|
||||
{ group: 'out', id: 'out1' },
|
||||
],
|
||||
groups: {
|
||||
in: {
|
||||
position: { name: 'top' },
|
||||
zIndex: 1,
|
||||
},
|
||||
out: {
|
||||
position: { name: 'bottom' },
|
||||
zIndex: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
graph.addEdge({
|
||||
source: { cell: source, port: 'out1' },
|
||||
target: { cell: target, port: 'in1' },
|
||||
attrs: {
|
||||
line: {
|
||||
stroke: '#808080',
|
||||
strokeWidth: 1,
|
||||
targetMarker: '',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// 引入样式
|
||||
insertCss(`
|
||||
.x6-node [magnet="true"] {
|
||||
cursor: crosshair;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.x6-node [magnet="true"]:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.x6-node [magnet="true"][port-group="in"] {
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
.my-port {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 1px solid #808080;
|
||||
border-radius: 100%;
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
.my-port.connected {
|
||||
width: 0;
|
||||
height: 0;
|
||||
margin-top: 5px;
|
||||
margin-left: 1px;
|
||||
border-width: 5px 4px 0;
|
||||
border-style: solid;
|
||||
border-color: #808080 transparent transparent;
|
||||
border-radius: 0;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.x6-port-body.available {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.x6-port-body.available body {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.x6-port-body.available body > div::before {
|
||||
content: " ";
|
||||
float: left;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-top: -5px;
|
||||
margin-left: -5px;
|
||||
border-radius: 50%;
|
||||
background-color: rgba(57, 202, 116, 0.6);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.x6-port-body.available body > div::after {
|
||||
content: " ";
|
||||
float: left;
|
||||
clear: both;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
margin-top: -15px;
|
||||
border-radius: 50%;
|
||||
background-color: #fff;
|
||||
border: 1px solid #39ca74;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.x6-port-body.adsorbed {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.x6-port-body.adsorbed body {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.x6-port-body.adsorbed body > div::before {
|
||||
content: " ";
|
||||
float: left;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
margin-top: -9px;
|
||||
margin-left: -9px;
|
||||
border-radius: 50%;
|
||||
background-color: rgba(57, 202, 116, 0.6);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.x6-port-body.adsorbed body > div::after {
|
||||
content: " ";
|
||||
float: left;
|
||||
clear: both;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
margin-top: -19px;
|
||||
border-radius: 50%;
|
||||
background-color: #fff;
|
||||
border: 1px solid #39ca74;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
`)
|
397
sites/x6-sites/examples/showcase/practices/demo/dag.tsx
Normal file
397
sites/x6-sites/examples/showcase/practices/demo/dag.tsx
Normal file
@ -0,0 +1,397 @@
|
||||
import React from 'react'
|
||||
import { Graph, Node, Path, Cell } from '@antv/x6'
|
||||
import insertCss from 'insert-css'
|
||||
import '@antv/x6-react-shape'
|
||||
|
||||
interface NodeStatus {
|
||||
id: string
|
||||
status: 'default' | 'success' | 'failed' | 'running'
|
||||
label?: string
|
||||
}
|
||||
|
||||
const image = {
|
||||
logo: 'https://gw.alipayobjects.com/mdn/rms_43231b/afts/img/A*evDjT5vjkX0AAAAAAAAAAAAAARQnAQ',
|
||||
success:
|
||||
'https://gw.alipayobjects.com/mdn/rms_43231b/afts/img/A*6l60T6h8TTQAAAAAAAAAAAAAARQnAQ',
|
||||
failed:
|
||||
'https://gw.alipayobjects.com/mdn/rms_43231b/afts/img/A*SEISQ6My-HoAAAAAAAAAAAAAARQnAQ',
|
||||
running:
|
||||
'https://gw.alipayobjects.com/mdn/rms_43231b/afts/img/A*t8fURKfgSOgAAAAAAAAAAAAAARQnAQ',
|
||||
}
|
||||
|
||||
export class AlgoNode extends React.Component<{ node?: Node }> {
|
||||
shouldComponentUpdate() {
|
||||
const { node } = this.props
|
||||
if (node) {
|
||||
if (node.hasChanged('data')) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
render() {
|
||||
const { node } = this.props
|
||||
const data = node?.getData() as NodeStatus
|
||||
const { label, status = 'default' } = data
|
||||
|
||||
return (
|
||||
<div className={`node ${status}`}>
|
||||
<img src={image.logo} />
|
||||
<span className="label">{label}</span>
|
||||
<span className="status">
|
||||
{status === 'success' && <img src={image.success} />}
|
||||
{status === 'failed' && <img src={image.failed} />}
|
||||
{status === 'running' && <img src={image.running} />}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Graph.registerNode(
|
||||
'dag-node',
|
||||
{
|
||||
inherit: 'react-shape',
|
||||
width: 180,
|
||||
height: 36,
|
||||
component: <AlgoNode />,
|
||||
ports: {
|
||||
groups: {
|
||||
top: {
|
||||
position: 'top',
|
||||
attrs: {
|
||||
circle: {
|
||||
r: 4,
|
||||
magnet: true,
|
||||
stroke: '#C2C8D5',
|
||||
strokeWidth: 1,
|
||||
fill: '#fff',
|
||||
},
|
||||
},
|
||||
},
|
||||
bottom: {
|
||||
position: 'bottom',
|
||||
attrs: {
|
||||
circle: {
|
||||
r: 4,
|
||||
magnet: true,
|
||||
stroke: '#C2C8D5',
|
||||
strokeWidth: 1,
|
||||
fill: '#fff',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
true,
|
||||
)
|
||||
|
||||
Graph.registerEdge(
|
||||
'dag-edge',
|
||||
{
|
||||
inherit: 'edge',
|
||||
attrs: {
|
||||
line: {
|
||||
stroke: '#C2C8D5',
|
||||
strokeWidth: 1,
|
||||
targetMarker: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
true,
|
||||
)
|
||||
|
||||
Graph.registerConnector(
|
||||
'algo-connector',
|
||||
(s, e) => {
|
||||
const offset = 4
|
||||
const deltaY = Math.abs(e.y - s.y)
|
||||
const control = Math.floor((deltaY / 3) * 2)
|
||||
|
||||
const v1 = { x: s.x, y: s.y + offset + control }
|
||||
const v2 = { x: e.x, y: e.y - offset - control }
|
||||
|
||||
return Path.normalize(
|
||||
`M ${s.x} ${s.y}
|
||||
L ${s.x} ${s.y + offset}
|
||||
C ${v1.x} ${v1.y} ${v2.x} ${v2.y} ${e.x} ${e.y - offset}
|
||||
L ${e.x} ${e.y}
|
||||
`,
|
||||
)
|
||||
},
|
||||
true,
|
||||
)
|
||||
|
||||
const nodeStatusList = [
|
||||
[
|
||||
{
|
||||
id: '1',
|
||||
status: 'running',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
status: 'default',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
status: 'default',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
status: 'default',
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
id: '1',
|
||||
status: 'success',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
status: 'running',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
status: 'default',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
status: 'default',
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
id: '1',
|
||||
status: 'success',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
status: 'success',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
status: 'running',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
status: 'running',
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
id: '1',
|
||||
status: 'success',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
status: 'success',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
status: 'success',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
status: 'failed',
|
||||
},
|
||||
],
|
||||
]
|
||||
|
||||
const graph: Graph = new Graph({
|
||||
container: document.getElementById('container')!,
|
||||
width: 800,
|
||||
height: 600,
|
||||
panning: {
|
||||
enabled: true,
|
||||
eventTypes: ['leftMouseDown', 'mouseWheel'],
|
||||
},
|
||||
mousewheel: {
|
||||
enabled: true,
|
||||
modifiers: 'ctrl',
|
||||
factor: 1.1,
|
||||
maxScale: 1.5,
|
||||
minScale: 0.5,
|
||||
},
|
||||
highlighting: {
|
||||
magnetAdsorbed: {
|
||||
name: 'stroke',
|
||||
args: {
|
||||
attrs: {
|
||||
fill: '#fff',
|
||||
stroke: '#31d0c6',
|
||||
strokeWidth: 4,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
connecting: {
|
||||
snap: true,
|
||||
allowBlank: false,
|
||||
allowLoop: false,
|
||||
highlight: true,
|
||||
connector: 'algo-connector',
|
||||
connectionPoint: 'anchor',
|
||||
anchor: 'center',
|
||||
validateMagnet({ magnet }) {
|
||||
return magnet.getAttribute('port-group') !== 'top'
|
||||
},
|
||||
createEdge() {
|
||||
return graph.createEdge({
|
||||
shape: 'dag-edge',
|
||||
attrs: {
|
||||
line: {
|
||||
strokeDasharray: '5 5',
|
||||
},
|
||||
},
|
||||
zIndex: -1,
|
||||
})
|
||||
},
|
||||
},
|
||||
selecting: {
|
||||
enabled: true,
|
||||
multiple: true,
|
||||
rubberEdge: true,
|
||||
rubberNode: true,
|
||||
modifiers: 'shift',
|
||||
rubberband: true,
|
||||
},
|
||||
})
|
||||
|
||||
graph.on('edge:connected', ({ edge }) => {
|
||||
edge.attr({
|
||||
line: {
|
||||
strokeDasharray: '',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
graph.on('node:change:data', ({ node }) => {
|
||||
const edges = graph.getIncomingEdges(node)
|
||||
const { status } = node.getData() as NodeStatus
|
||||
edges?.forEach((edge) => {
|
||||
if (status === 'running') {
|
||||
edge.attr('line/strokeDasharray', 5)
|
||||
edge.attr('line/style/animation', 'running-line 30s infinite linear')
|
||||
} else {
|
||||
edge.attr('line/strokeDasharray', '')
|
||||
edge.attr('line/style/animation', '')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// 初始化节点/边
|
||||
const init = (data: Cell.Metadata[]) => {
|
||||
const cells: Cell[] = []
|
||||
data.forEach((item) => {
|
||||
if (item.shape === 'dag-node') {
|
||||
cells.push(graph.createNode(item))
|
||||
} else {
|
||||
cells.push(graph.createEdge(item))
|
||||
}
|
||||
})
|
||||
graph.resetCells(cells)
|
||||
}
|
||||
|
||||
// 显示节点状态
|
||||
const showNodeStatus = async (statusList: NodeStatus[][]) => {
|
||||
const status = statusList.shift()
|
||||
status?.forEach((item) => {
|
||||
const { id, status } = item
|
||||
const node = graph.getCellById(id)
|
||||
const data = node.getData() as NodeStatus
|
||||
node.setData({
|
||||
...data,
|
||||
status: status,
|
||||
})
|
||||
})
|
||||
setTimeout(() => {
|
||||
showNodeStatus(statusList)
|
||||
}, 3000)
|
||||
}
|
||||
|
||||
fetch('../data/dag.json')
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
init(data)
|
||||
showNodeStatus(nodeStatusList)
|
||||
})
|
||||
|
||||
insertCss(`
|
||||
.node {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #fff;
|
||||
border: 1px solid #c2c8d5;
|
||||
border-left: 4px solid #1890ff;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 5px 1px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
.node img {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
flex-shrink: 0;
|
||||
margin-left: 8px;
|
||||
}
|
||||
.node .label {
|
||||
display: inline-block;
|
||||
flex-shrink: 0;
|
||||
width: 104px;
|
||||
margin-left: 8px;
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
}
|
||||
.node .status {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.node.success {
|
||||
border-left: 4px solid #52c41a;
|
||||
}
|
||||
.node.failed {
|
||||
border-left: 4px solid #ff4d4f;
|
||||
}
|
||||
.node.running .status img {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
.x6-node-selected .node {
|
||||
border-color: #1890ff;
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 0 0 4px #d4e8fe;
|
||||
}
|
||||
.x6-node-selected .node.success {
|
||||
border-color: #52c41a;
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 0 0 4px #ccecc0;
|
||||
}
|
||||
.x6-node-selected .node.failed {
|
||||
border-color: #ff4d4f;
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 0 0 4px #fedcdc;
|
||||
}
|
||||
.x6-edge:hover path:nth-child(2){
|
||||
stroke: #1890ff;
|
||||
stroke-width: 1px;
|
||||
}
|
||||
|
||||
.x6-edge-selected path:nth-child(2){
|
||||
stroke: #1890ff;
|
||||
stroke-width: 1.5px !important;
|
||||
}
|
||||
|
||||
@keyframes running-line {
|
||||
to {
|
||||
stroke-dashoffset: -1000;
|
||||
}
|
||||
}
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
`)
|
@ -9,12 +9,12 @@
|
||||
"screenshot": "https://gw.alipayobjects.com/mdn/rms_43231b/afts/img/A*JSr-RbwCgmcAAAAAAAAAAAAAARQnAQ"
|
||||
},
|
||||
{
|
||||
"filename": "algo-flow.tsx",
|
||||
"filename": "dag.tsx",
|
||||
"title": {
|
||||
"zh": "人工智能建模流程",
|
||||
"en": "Flow for AI Model"
|
||||
},
|
||||
"screenshot": "https://gw.alipayobjects.com/mdn/rms_43231b/afts/img/A*3A0IR44lo60AAAAAAAAAAAAAARQnAQ"
|
||||
"screenshot": "https://gw.alipayobjects.com/mdn/rms_43231b/afts/img/A*fcCAS5w_4lkAAAAAAAAAAAAAARQnAQ"
|
||||
},
|
||||
{
|
||||
"filename": "er.ts",
|
||||
|
Reference in New Issue
Block a user