balibabu commited on
Commit
8d4e686
·
1 Parent(s): c61ccc8

feat: Add IndentedTree #162 (#1792)

Browse files

### What problem does this PR solve?

feat: Add IndentedTree #162

### Type of change


- [x] New Feature (non-breaking change which adds functionality)

web/src/pages/add-knowledge/components/knowledge-chunk/components/knowledge-graph/force-graph.tsx CHANGED
@@ -1,7 +1,6 @@
1
- import { useFetchKnowledgeGraph } from '@/hooks/chunk-hooks';
2
  import { ElementDatum, Graph, IElementEvent } from '@antv/g6';
3
  import { useCallback, useEffect, useMemo, useRef } from 'react';
4
- import { buildNodesAndCombos, isDataExist } from './util';
5
 
6
  import styles from './index.less';
7
 
@@ -11,14 +10,18 @@ const TooltipColorMap = {
11
  edge: 'blue',
12
  };
13
 
14
- const ForceGraph = () => {
 
 
 
 
 
15
  const containerRef = useRef<HTMLDivElement>(null);
16
  const graphRef = useRef<Graph | null>(null);
17
- const { data } = useFetchKnowledgeGraph();
18
 
19
  const nextData = useMemo(() => {
20
- if (isDataExist(data)) {
21
- const graphData = data.data;
22
  const mi = buildNodesAndCombos(graphData.nodes);
23
  return { edges: graphData.links, ...mi };
24
  }
@@ -113,7 +116,7 @@ const ForceGraph = () => {
113
  }, [nextData]);
114
 
115
  useEffect(() => {
116
- if (isDataExist(data)) {
117
  render();
118
  }
119
  }, [data, render]);
@@ -122,7 +125,11 @@ const ForceGraph = () => {
122
  <div
123
  ref={containerRef}
124
  className={styles.forceContainer}
125
- style={{ width: '100%', height: '80vh' }}
 
 
 
 
126
  />
127
  );
128
  };
 
 
1
  import { ElementDatum, Graph, IElementEvent } from '@antv/g6';
2
  import { useCallback, useEffect, useMemo, useRef } from 'react';
3
+ import { buildNodesAndCombos } from './util';
4
 
5
  import styles from './index.less';
6
 
 
10
  edge: 'blue',
11
  };
12
 
13
+ interface IProps {
14
+ data: any;
15
+ show: boolean;
16
+ }
17
+
18
+ const ForceGraph = ({ data, show }: IProps) => {
19
  const containerRef = useRef<HTMLDivElement>(null);
20
  const graphRef = useRef<Graph | null>(null);
 
21
 
22
  const nextData = useMemo(() => {
23
+ if (data) {
24
+ const graphData = data;
25
  const mi = buildNodesAndCombos(graphData.nodes);
26
  return { edges: graphData.links, ...mi };
27
  }
 
116
  }, [nextData]);
117
 
118
  useEffect(() => {
119
+ if (data) {
120
  render();
121
  }
122
  }, [data, render]);
 
125
  <div
126
  ref={containerRef}
127
  className={styles.forceContainer}
128
+ style={{
129
+ width: '90vh',
130
+ height: '80vh',
131
+ display: show ? 'block' : 'none',
132
+ }}
133
  />
134
  );
135
  };
web/src/pages/add-knowledge/components/knowledge-chunk/components/knowledge-graph/indented-tree.tsx ADDED
@@ -0,0 +1,432 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Rect } from '@antv/g';
2
+ import {
3
+ Badge,
4
+ BaseBehavior,
5
+ BaseNode,
6
+ CommonEvent,
7
+ ExtensionCategory,
8
+ Graph,
9
+ NodeEvent,
10
+ Point,
11
+ Polyline,
12
+ PolylineStyleProps,
13
+ register,
14
+ subStyleProps,
15
+ treeToGraphData,
16
+ } from '@antv/g6';
17
+ import { TreeData } from '@antv/g6/lib/types';
18
+ import isEmpty from 'lodash/isEmpty';
19
+ import { useCallback, useEffect, useRef } from 'react';
20
+
21
+ const rootId = 'Modeling Methods';
22
+
23
+ const COLORS = [
24
+ '#5B8FF9',
25
+ '#F6BD16',
26
+ '#5AD8A6',
27
+ '#945FB9',
28
+ '#E86452',
29
+ '#6DC8EC',
30
+ '#FF99C3',
31
+ '#1E9493',
32
+ '#FF9845',
33
+ '#5D7092',
34
+ ];
35
+
36
+ const TreeEvent = {
37
+ COLLAPSE_EXPAND: 'collapse-expand',
38
+ ADD_CHILD: 'add-child',
39
+ };
40
+
41
+ class IndentedNode extends BaseNode {
42
+ static defaultStyleProps = {
43
+ ports: [
44
+ {
45
+ key: 'in',
46
+ placement: 'right-bottom',
47
+ },
48
+ {
49
+ key: 'out',
50
+ placement: 'left-bottom',
51
+ },
52
+ ],
53
+ } as any;
54
+
55
+ constructor(options: any) {
56
+ Object.assign(options.style, IndentedNode.defaultStyleProps);
57
+ super(options);
58
+ }
59
+
60
+ get childrenData() {
61
+ return this.attributes.context?.model.getChildrenData(this.id);
62
+ }
63
+
64
+ getKeyStyle(attributes: any) {
65
+ const [width, height] = this.getSize(attributes);
66
+ const keyStyle = super.getKeyStyle(attributes);
67
+ return {
68
+ width,
69
+ height,
70
+ ...keyStyle,
71
+ fill: 'transparent',
72
+ };
73
+ }
74
+
75
+ drawKeyShape(attributes: any, container: any) {
76
+ const keyStyle = this.getKeyStyle(attributes);
77
+ return this.upsert('key', Rect, keyStyle, container);
78
+ }
79
+
80
+ getLabelStyle(attributes: any) {
81
+ if (attributes.label === false || !attributes.labelText) return false;
82
+ return subStyleProps(this.getGraphicStyle(attributes), 'label') as any;
83
+ }
84
+
85
+ drawIconArea(attributes: any, container: any) {
86
+ const [, h] = this.getSize(attributes);
87
+ const iconAreaStyle = {
88
+ fill: 'transparent',
89
+ height: 30,
90
+ width: 12,
91
+ x: -6,
92
+ y: h,
93
+ zIndex: -1,
94
+ };
95
+ this.upsert('icon-area', Rect, iconAreaStyle, container);
96
+ }
97
+
98
+ forwardEvent(target: any, type: any, listener: any) {
99
+ if (target && !Reflect.has(target, '__bind__')) {
100
+ Reflect.set(target, '__bind__', true);
101
+ target.addEventListener(type, listener);
102
+ }
103
+ }
104
+
105
+ getCountStyle(attributes: any) {
106
+ const { collapsed, color } = attributes;
107
+ if (collapsed) {
108
+ const [, height] = this.getSize(attributes);
109
+ return {
110
+ backgroundFill: color,
111
+ cursor: 'pointer',
112
+ fill: '#fff',
113
+ fontSize: 8,
114
+ padding: [0, 10],
115
+ text: `${this.childrenData?.length}`,
116
+ textAlign: 'center',
117
+ y: height + 8,
118
+ };
119
+ }
120
+
121
+ return false;
122
+ }
123
+
124
+ drawCountShape(attributes: any, container: any) {
125
+ const countStyle = this.getCountStyle(attributes);
126
+ const btn = this.upsert('count', Badge, countStyle as any, container);
127
+
128
+ this.forwardEvent(btn, CommonEvent.CLICK, (event: any) => {
129
+ event.stopPropagation();
130
+ attributes.context.graph.emit(TreeEvent.COLLAPSE_EXPAND, {
131
+ id: this.id,
132
+ collapsed: false,
133
+ });
134
+ });
135
+ }
136
+
137
+ isShowCollapse(attributes: any) {
138
+ return (
139
+ !attributes.collapsed &&
140
+ Array.isArray(this.childrenData) &&
141
+ this.childrenData?.length > 0
142
+ );
143
+ }
144
+
145
+ getCollapseStyle(attributes: any) {
146
+ const { showIcon, color } = attributes;
147
+ if (!this.isShowCollapse(attributes)) return false;
148
+ const [, height] = this.getSize(attributes);
149
+ return {
150
+ visibility: showIcon ? 'visible' : 'hidden',
151
+ backgroundFill: color,
152
+ backgroundHeight: 12,
153
+ backgroundWidth: 12,
154
+ cursor: 'pointer',
155
+ fill: '#fff',
156
+ fontFamily: 'iconfont',
157
+ fontSize: 8,
158
+ text: '\ue6e4',
159
+ textAlign: 'center',
160
+ x: -1, // half of edge line width
161
+ y: height + 8,
162
+ };
163
+ }
164
+
165
+ drawCollapseShape(attributes: any, container: any) {
166
+ const iconStyle = this.getCollapseStyle(attributes);
167
+ const btn = this.upsert(
168
+ 'collapse-expand',
169
+ Badge,
170
+ iconStyle as any,
171
+ container,
172
+ );
173
+
174
+ this.forwardEvent(btn, CommonEvent.CLICK, (event: any) => {
175
+ event.stopPropagation();
176
+ attributes.context.graph.emit(TreeEvent.COLLAPSE_EXPAND, {
177
+ id: this.id,
178
+ collapsed: !attributes.collapsed,
179
+ });
180
+ });
181
+ }
182
+
183
+ getAddStyle(attributes: any) {
184
+ const { collapsed, showIcon } = attributes;
185
+ if (collapsed) return false;
186
+ const [, height] = this.getSize(attributes);
187
+ const color = '#ddd';
188
+ const lineWidth = 1;
189
+
190
+ return {
191
+ visibility: showIcon ? 'visible' : 'hidden',
192
+ backgroundFill: '#fff',
193
+ backgroundHeight: 12,
194
+ backgroundLineWidth: lineWidth,
195
+ backgroundStroke: color,
196
+ backgroundWidth: 12,
197
+ cursor: 'pointer',
198
+ fill: color,
199
+ fontFamily: 'iconfont',
200
+ text: '\ue664',
201
+ textAlign: 'center',
202
+ x: -1,
203
+ y: height + (this.isShowCollapse(attributes) ? 22 : 8),
204
+ };
205
+ }
206
+
207
+ drawAddShape(attributes: any, container: any) {
208
+ const addStyle = this.getAddStyle(attributes);
209
+ const btn = this.upsert('add', Badge, addStyle as any, container);
210
+
211
+ this.forwardEvent(btn, CommonEvent.CLICK, (event: any) => {
212
+ event.stopPropagation();
213
+ attributes.context.graph.emit(TreeEvent.ADD_CHILD, { id: this.id });
214
+ });
215
+ }
216
+
217
+ render(attributes = this.parsedAttributes, container = this) {
218
+ super.render(attributes, container);
219
+
220
+ this.drawCountShape(attributes, container);
221
+
222
+ this.drawIconArea(attributes, container);
223
+ this.drawCollapseShape(attributes, container);
224
+ this.drawAddShape(attributes, container);
225
+ }
226
+ }
227
+
228
+ class IndentedEdge extends Polyline {
229
+ getControlPoints(
230
+ attributes: Required<PolylineStyleProps>,
231
+ sourcePoint: Point,
232
+ targetPoint: Point,
233
+ ) {
234
+ const [sx] = sourcePoint;
235
+ const [, ty] = targetPoint;
236
+ return [[sx, ty]] as any;
237
+ }
238
+ }
239
+
240
+ class CollapseExpandTree extends BaseBehavior {
241
+ constructor(context: any, options: any) {
242
+ super(context, options);
243
+ this.bindEvents();
244
+ }
245
+
246
+ update(options: any) {
247
+ this.unbindEvents();
248
+ super.update(options);
249
+ this.bindEvents();
250
+ }
251
+
252
+ bindEvents() {
253
+ const { graph } = this.context;
254
+
255
+ graph.on(NodeEvent.POINTER_ENTER, this.showIcon);
256
+ graph.on(NodeEvent.POINTER_LEAVE, this.hideIcon);
257
+ graph.on(TreeEvent.COLLAPSE_EXPAND, this.onCollapseExpand);
258
+ graph.on(TreeEvent.ADD_CHILD, this.addChild);
259
+ }
260
+
261
+ unbindEvents() {
262
+ const { graph } = this.context;
263
+
264
+ graph.off(NodeEvent.POINTER_ENTER, this.showIcon);
265
+ graph.off(NodeEvent.POINTER_LEAVE, this.hideIcon);
266
+ graph.off(TreeEvent.COLLAPSE_EXPAND, this.onCollapseExpand);
267
+ graph.off(TreeEvent.ADD_CHILD, this.addChild);
268
+ }
269
+
270
+ status = 'idle';
271
+
272
+ showIcon = (event: any) => {
273
+ this.setIcon(event, true);
274
+ };
275
+
276
+ hideIcon = (event: any) => {
277
+ this.setIcon(event, false);
278
+ };
279
+
280
+ setIcon = (event: any, show: boolean) => {
281
+ if (this.status !== 'idle') return;
282
+ const { target } = event;
283
+ const id = target.id;
284
+ const { graph, element } = this.context;
285
+ graph.updateNodeData([{ id, style: { showIcon: show } }]);
286
+ element?.draw({ animation: false, silence: true });
287
+ };
288
+
289
+ onCollapseExpand = async (event: any) => {
290
+ this.status = 'busy';
291
+ const { id, collapsed } = event;
292
+ const { graph } = this.context;
293
+ if (collapsed) await graph.collapseElement(id);
294
+ else await graph.expandElement(id);
295
+ this.status = 'idle';
296
+ };
297
+
298
+ addChild(event: any) {
299
+ const {
300
+ onCreateChild = () => ({
301
+ id: `${Date.now()}`,
302
+ style: { labelText: 'new node' },
303
+ }),
304
+ } = this.options;
305
+ const { graph } = this.context;
306
+ const datum = onCreateChild(event.id);
307
+ graph.addNodeData([datum]);
308
+ graph.addEdgeData([{ source: event.id, target: datum.id }]);
309
+ const parent = graph.getNodeData(event.id);
310
+ graph.updateNodeData([
311
+ {
312
+ id: event.id,
313
+ children: [...(parent.children || []), datum.id],
314
+ style: { collapsed: false },
315
+ },
316
+ ]);
317
+ graph.render();
318
+ }
319
+ }
320
+
321
+ register(ExtensionCategory.NODE, 'indented', IndentedNode);
322
+ register(ExtensionCategory.EDGE, 'indented', IndentedEdge);
323
+ register(
324
+ ExtensionCategory.BEHAVIOR,
325
+ 'collapse-expand-tree',
326
+ CollapseExpandTree,
327
+ );
328
+
329
+ interface IProps {
330
+ data: TreeData;
331
+ show: boolean;
332
+ }
333
+
334
+ const IndentedTree = ({ data, show }: IProps) => {
335
+ const containerRef = useRef<HTMLDivElement>(null);
336
+ const graphRef = useRef<Graph | null>(null);
337
+
338
+ const render = useCallback(async (data: TreeData) => {
339
+ const graph: Graph = new Graph({
340
+ container: containerRef.current!,
341
+ x: 60,
342
+ node: {
343
+ type: 'indented',
344
+ style: {
345
+ size: (d) => [d.id.length * 6 + 10, 20],
346
+ labelBackground: (datum) => datum.id === rootId,
347
+ labelBackgroundRadius: 0,
348
+ labelBackgroundFill: '#576286',
349
+ labelFill: (datum) => (datum.id === rootId ? '#fff' : '#666'),
350
+ labelText: (d) => d.style?.labelText || d.id,
351
+ labelTextAlign: (datum) => (datum.id === rootId ? 'center' : 'left'),
352
+ labelTextBaseline: 'top',
353
+ color: (datum: any) => {
354
+ const depth = graph.getAncestorsData(datum.id, 'tree').length - 1;
355
+ return COLORS[depth % COLORS.length] || '#576286';
356
+ },
357
+ },
358
+ state: {
359
+ selected: {
360
+ lineWidth: 0,
361
+ labelFill: '#40A8FF',
362
+ labelBackground: true,
363
+ labelFontWeight: 'normal',
364
+ labelBackgroundFill: '#e8f7ff',
365
+ labelBackgroundRadius: 10,
366
+ },
367
+ },
368
+ },
369
+ edge: {
370
+ type: 'indented',
371
+ style: {
372
+ radius: 16,
373
+ lineWidth: 2,
374
+ sourcePort: 'out',
375
+ targetPort: 'in',
376
+ stroke: (datum: any) => {
377
+ const depth = graph.getAncestorsData(datum.source, 'tree').length;
378
+ return COLORS[depth % COLORS.length];
379
+ },
380
+ },
381
+ },
382
+ layout: {
383
+ type: 'indented',
384
+ direction: 'LR',
385
+ isHorizontal: true,
386
+ indent: 40,
387
+ getHeight: () => 20,
388
+ getVGap: () => 10,
389
+ },
390
+ behaviors: [
391
+ 'scroll-canvas',
392
+ 'drag-branch',
393
+ 'collapse-expand-tree',
394
+ {
395
+ type: 'click-select',
396
+ enable: (event: any) =>
397
+ event.targetType === 'node' && event.target.id !== rootId,
398
+ },
399
+ ],
400
+ });
401
+
402
+ if (graphRef.current) {
403
+ graphRef.current.destroy();
404
+ }
405
+
406
+ graphRef.current = graph;
407
+
408
+ graph.setData(treeToGraphData(data));
409
+
410
+ graph.render();
411
+ }, []);
412
+
413
+ useEffect(() => {
414
+ if (!isEmpty(data)) {
415
+ render(data);
416
+ }
417
+ }, [render, data]);
418
+
419
+ return (
420
+ <div
421
+ id="tree"
422
+ ref={containerRef}
423
+ style={{
424
+ width: '90vh',
425
+ height: '80vh',
426
+ display: show ? 'block' : 'none',
427
+ }}
428
+ />
429
+ );
430
+ };
431
+
432
+ export default IndentedTree;
web/src/pages/add-knowledge/components/knowledge-chunk/components/knowledge-graph/index.tsx DELETED
@@ -1,5 +0,0 @@
1
- const KnowledgeGraph = () => {
2
- return <div>KnowledgeGraph</div>;
3
- };
4
-
5
- export default KnowledgeGraph;
 
 
 
 
 
 
web/src/pages/add-knowledge/components/knowledge-chunk/components/knowledge-graph/modal.tsx CHANGED
@@ -1,14 +1,20 @@
1
  import { useFetchKnowledgeGraph } from '@/hooks/chunk-hooks';
2
- import { Modal } from 'antd';
3
  import React, { useEffect, useState } from 'react';
4
  import ForceGraph from './force-graph';
5
-
6
  import styles from './index.less';
7
  import { isDataExist } from './util';
8
 
 
 
 
 
 
9
  const KnowledgeGraphModal: React.FC = () => {
10
  const [isModalOpen, setIsModalOpen] = useState(false);
11
  const { data } = useFetchKnowledgeGraph();
 
12
 
13
  const handleOk = () => {
14
  setIsModalOpen(false);
@@ -34,7 +40,22 @@ const KnowledgeGraphModal: React.FC = () => {
34
  footer={null}
35
  >
36
  <section className={styles.modalContainer}>
37
- <ForceGraph></ForceGraph>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  </section>
39
  </Modal>
40
  );
 
1
  import { useFetchKnowledgeGraph } from '@/hooks/chunk-hooks';
2
+ import { Flex, Modal, Segmented } from 'antd';
3
  import React, { useEffect, useState } from 'react';
4
  import ForceGraph from './force-graph';
5
+ import IndentedTree from './indented-tree';
6
  import styles from './index.less';
7
  import { isDataExist } from './util';
8
 
9
+ enum SegmentedValue {
10
+ Graph = 'Graph',
11
+ Mind = 'Mind',
12
+ }
13
+
14
  const KnowledgeGraphModal: React.FC = () => {
15
  const [isModalOpen, setIsModalOpen] = useState(false);
16
  const { data } = useFetchKnowledgeGraph();
17
+ const [value, setValue] = useState<SegmentedValue>(SegmentedValue.Graph);
18
 
19
  const handleOk = () => {
20
  setIsModalOpen(false);
 
40
  footer={null}
41
  >
42
  <section className={styles.modalContainer}>
43
+ <Flex justify="end">
44
+ <Segmented
45
+ size="large"
46
+ options={[SegmentedValue.Graph, SegmentedValue.Mind]}
47
+ value={value}
48
+ onChange={(v) => setValue(v as SegmentedValue)}
49
+ />
50
+ </Flex>
51
+ <ForceGraph
52
+ data={data?.data?.graph}
53
+ show={value === SegmentedValue.Graph}
54
+ ></ForceGraph>
55
+ <IndentedTree
56
+ data={data?.data?.mind_map}
57
+ show={value === SegmentedValue.Mind}
58
+ ></IndentedTree>
59
  </section>
60
  </Modal>
61
  );
web/src/pages/add-knowledge/components/knowledge-chunk/index.tsx CHANGED
@@ -195,7 +195,7 @@ const Chunk = () => {
195
  onOk={onChunkUpdatingOk}
196
  />
197
  )}
198
- {false && <KnowledgeGraphModal></KnowledgeGraphModal>}
199
  </>
200
  );
201
  };
 
195
  onOk={onChunkUpdatingOk}
196
  />
197
  )}
198
+ <KnowledgeGraphModal></KnowledgeGraphModal>
199
  </>
200
  );
201
  };