Skip to content

Commit b776e60

Browse files
committed
feat: adds extension for solid-js
1 parent 9979fc0 commit b776e60

24 files changed

+1160
-0
lines changed

packages/g6-extension-solid/README.md

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
## SolidJS extension for G6
2+
3+
<img width="500" src="https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*rWSiT6dnwfcAAAAAAAAAAAAADmJ7AQ/original" />
4+
5+
This extension allows you to define G6 node by SolidJS component and JSX syntax with fine-grained reactivity.
6+
7+
## Features
8+
9+
- **Fine-grained reactivity**: Unlike React's virtual DOM, SolidJS uses signals for surgical DOM updates
10+
- **No re-renders**: Component attributes are reactive signals that update only the parts of the DOM that depend on them
11+
- **Familiar JSX syntax**: Write components using JSX just like React, but with SolidJS's reactive primitives
12+
13+
## Usage
14+
15+
1. Install
16+
17+
```bash
18+
npm install @antv/g6-extension-solid
19+
```
20+
21+
2. Import and Register
22+
23+
```js
24+
import { ExtensionCategory, register } from '@antv/g6';
25+
import { SolidNode } from '@antv/g6-extension-solid';
26+
27+
register(ExtensionCategory.NODE, 'solid', SolidNode);
28+
```
29+
30+
3. Define Node
31+
32+
SolidJS Node:
33+
34+
```jsx
35+
const SolidNode = (props) => {
36+
return <div>node: {props.id}</div>;
37+
};
38+
```
39+
40+
G Node with SolidJS:
41+
42+
```jsx
43+
import { Group, Rect, Text } from '@antv/g6-extension-solid';
44+
45+
const GNode = (props) => {
46+
return <Group>
47+
<Rect width={100} height={100}></Rect>
48+
<Text text={props.label || "node"} />
49+
</Group>
50+
};
51+
```
52+
53+
Reactive Node:
54+
55+
```jsx
56+
import { createSignal } from 'solid-js';
57+
58+
const ReactiveNode = (props) => {
59+
const [count, setCount] = createSignal(0);
60+
61+
return (
62+
<div onClick={() => setCount(count() + 1)}>
63+
Node {props.id}: {count()} clicks
64+
</div>
65+
);
66+
};
67+
```
68+
69+
4. Use
70+
71+
Use SolidNode:
72+
73+
```jsx
74+
const graph = new Graph({
75+
// ... other options
76+
node: {
77+
type: 'solid',
78+
style: {
79+
component: SolidNode,
80+
},
81+
},
82+
});
83+
```
84+
85+
Use GNode:
86+
87+
```jsx
88+
const graph = new Graph({
89+
// ... other options
90+
node: {
91+
type: 'solid',
92+
style: {
93+
component: GNode,
94+
},
95+
},
96+
});
97+
```
98+
99+
## Key Differences from React Extension
100+
101+
1. **Reactivity Model**: SolidJS uses signals instead of virtual DOM, providing automatic fine-grained updates
102+
2. **Performance**: No re-renders - only the specific DOM nodes that depend on changed signals are updated
103+
3. **Props Updates**: Node attributes are automatically reactive through signals, no manual re-rendering needed
104+
105+
## Q&A
106+
107+
1. Difference between SolidNode and GNode
108+
109+
SolidNode is a SolidJS component that renders to regular DOM, while GNode supports JSX syntax but can only use G tag nodes for SVG/Canvas rendering.
110+
111+
2. How does reactivity work?
112+
113+
The extension automatically creates signals for node attributes. When attributes change in G6, the signals update, and SolidJS reactively updates only the parts of the DOM that depend on those signals.
114+
115+
## Resources
116+
117+
- [SolidJS Documentation](https://www.solidjs.com/)
118+
- [G6 Custom Nodes](https://g6.antv.antgroup.com/examples/element/custom-node/)
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"rules": {
3+
"no-console": "off"
4+
}
5+
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
{
2+
"nodes": [
3+
{
4+
"id": "50251337",
5+
"x": 50,
6+
"y": 68,
7+
"isTeamA": "1",
8+
"player_id": "50251337",
9+
"player_shirtnumber": "19",
10+
"player_enName": "Justin Kluivert",
11+
"player_name": "尤斯廷-克鲁伊维特"
12+
},
13+
{
14+
"id": "50436685",
15+
"x": 25,
16+
"y": 68,
17+
"isTeamA": "1",
18+
"player_id": "50436685",
19+
"player_shirtnumber": "24",
20+
"player_enName": "Antoine Semenyo",
21+
"player_name": "塞门约"
22+
},
23+
{
24+
"id": "50204813",
25+
"x": 50,
26+
"y": 89,
27+
"isTeamA": "1",
28+
"player_id": "50204813",
29+
"player_shirtnumber": "9",
30+
"player_enName": "Dominic Solanke",
31+
"player_name": "索兰克"
32+
},
33+
{
34+
"id": "50250175",
35+
"x": 75,
36+
"y": 68,
37+
"isTeamA": "1",
38+
"player_id": "50250175",
39+
"player_shirtnumber": "16",
40+
"player_enName": "Marcus Tavernier",
41+
"player_name": "塔韦尼耶"
42+
},
43+
{
44+
"id": "50213675",
45+
"x": 65,
46+
"y": 48,
47+
"isTeamA": "1",
48+
"player_id": "50213675",
49+
"player_shirtnumber": "4",
50+
"player_enName": "Lewis Cook",
51+
"player_name": "刘易斯-库克"
52+
},
53+
{
54+
"id": "50186648",
55+
"x": 35,
56+
"y": 48,
57+
"isTeamA": "1",
58+
"player_id": "50186648",
59+
"player_shirtnumber": "10",
60+
"player_enName": "Ryan Christie",
61+
"player_name": "克里斯蒂"
62+
},
63+
{
64+
"id": "50279448",
65+
"x": 38,
66+
"y": 28,
67+
"isTeamA": "1",
68+
"player_id": "50279448",
69+
"player_shirtnumber": "6",
70+
"player_enName": "Chris Mepham",
71+
"player_name": "迈帕姆"
72+
},
73+
{
74+
"id": "50061646",
75+
"x": 15,
76+
"y": 28,
77+
"isTeamA": "1",
78+
"player_id": "50061646",
79+
"player_shirtnumber": "15",
80+
"player_enName": "Adam Smith",
81+
"player_name": "亚当-史密斯"
82+
},
83+
{
84+
"id": "50472140",
85+
"x": 62,
86+
"y": 28,
87+
"isTeamA": "1",
88+
"player_id": "50472140",
89+
"player_shirtnumber": "27",
90+
"player_enName": "Ilya Zabarnyi",
91+
"player_name": "扎巴尔尼"
92+
},
93+
{
94+
"id": "50544346",
95+
"x": 85,
96+
"y": 28,
97+
"isTeamA": "1",
98+
"player_id": "50544346",
99+
"player_shirtnumber": "3",
100+
"player_enName": "Milos Kerkez",
101+
"player_name": "科尔克兹"
102+
},
103+
{
104+
"id": "50062598",
105+
"x": 50,
106+
"y": 7,
107+
"isTeamA": "1",
108+
"player_id": "50062598",
109+
"player_shirtnumber": "1",
110+
"player_enName": "Neto",
111+
"player_name": "内托"
112+
}
113+
]
114+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import { ExtensionCategory, register } from '@antv/g6';
2+
import { ReactNode } from '@antv/g6-extension-react';
3+
import styled from 'styled-components';
4+
import data from '../dataset/euro-cup.json';
5+
import { Graph } from '../graph';
6+
7+
const Player = styled.div`
8+
width: 100%;
9+
height: 100%;
10+
display: flex;
11+
flex-direction: column;
12+
justify-content: center;
13+
align-items: center;
14+
`;
15+
16+
const Shirt = styled.div`
17+
width: 40px;
18+
height: 40px;
19+
display: flex;
20+
align-items: center;
21+
justify-content: center;
22+
position: relative;
23+
24+
img {
25+
width: 40px;
26+
position: absolute;
27+
left: 0;
28+
top: 0;
29+
}
30+
`;
31+
32+
const Number = styled.div`
33+
color: #fff;
34+
font-family: 'DingTalk-JinBuTi';
35+
font-size: 10px;
36+
top: 20px;
37+
left: 15px;
38+
z-index: 1;
39+
margin-top: 16px;
40+
margin-left: -2px;
41+
`;
42+
43+
const Label = styled.div`
44+
max-width: 120px;
45+
padding: 0 8px;
46+
color: #fff;
47+
font-size: 10px;
48+
background-image: url('https://mdn.alipayobjects.com/huamei_92awrc/afts/img/A*s2csQ48M0AkAAAAAAAAAAAAADsvfAQ/original');
49+
background-repeat: no-repeat;
50+
background-size: cover;
51+
display: flex;
52+
justify-content: center;
53+
overflow: hidden;
54+
text-overflow: ellipsis;
55+
`;
56+
57+
const PlayerNode = ({ playerInfo }: any) => {
58+
const { isTeamA, player_shirtnumber, player_name } = playerInfo;
59+
return (
60+
<Player>
61+
<Shirt>
62+
<img
63+
src={
64+
isTeamA
65+
? 'https://mdn.alipayobjects.com/huamei_92awrc/afts/img/A*0oAaS42vqWcAAAAAAAAAAAAADsvfAQ/original'
66+
: 'https://mdn.alipayobjects.com/huamei_92awrc/afts/img/A*BYH5SauBNecAAAAAAAAAAAAADsvfAQ/original'
67+
}
68+
/>
69+
<Number>{player_shirtnumber}</Number>
70+
</Shirt>
71+
<Label>{player_name}</Label>
72+
</Player>
73+
);
74+
};
75+
76+
register(ExtensionCategory.NODE, 'react', ReactNode);
77+
78+
export const EuroCup = () => {
79+
return (
80+
<div>
81+
<Graph
82+
options={{
83+
data,
84+
animation: false,
85+
x: 10,
86+
y: 50,
87+
width: 480,
88+
height: 720,
89+
node: {
90+
type: 'react',
91+
style: {
92+
size: [100, 60],
93+
ports: [{ placement: 'center' }],
94+
x: (d: any) => d.x * 3.5,
95+
y: (d: any) => d.y * 3.5,
96+
fill: 'transparent',
97+
component: (data: any) => <PlayerNode playerInfo={data} />,
98+
},
99+
},
100+
plugins: [
101+
{
102+
type: 'background',
103+
width: '480px',
104+
height: '720px',
105+
backgroundImage:
106+
'url(https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*EmPXQLrX2xIAAAAAAAAAAAAADmJ7AQ/original)',
107+
backgroundRepeat: 'no-repeat',
108+
backgroundSize: 'contain',
109+
opacity: 1,
110+
},
111+
],
112+
}}
113+
/>
114+
</div>
115+
);
116+
};
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { Graph } from '../graph';
2+
3+
export const G6Graph = () => {
4+
return (
5+
<Graph
6+
options={{
7+
data: {
8+
nodes: [
9+
{ id: 'node-1', style: { x: 100, y: 100, labelText: 'Hello' } },
10+
{ id: 'node-2', style: { x: 300, y: 100, labelText: 'World' } },
11+
],
12+
edges: [{ source: 'node-1', target: 'node-2' }],
13+
},
14+
behaviors: ['drag-element'],
15+
}}
16+
onRender={() => {
17+
console.log('render');
18+
}}
19+
onDestroy={() => {
20+
console.log('destroy');
21+
}}
22+
/>
23+
);
24+
};
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export * from './euro-cup';
2+
export * from './graph';
3+
export * from './performance-diagnosis';
4+
export * from './react-node';

0 commit comments

Comments
 (0)