feat: plugin legend (#5593)

* chore: rebase v5

* fix: resolve conversation

* ci: update snapshots

* refactor(plugin): update legend case and snapshots

---------

Co-authored-by: Aaron <yangtao.yangtao@antgroup.com>
This commit is contained in:
Joel Alan 2024-04-02 23:29:06 +08:00 committed by GitHub
parent 923c62a639
commit d0c3c7dbc5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 6505 additions and 496 deletions

View File

@ -80,6 +80,7 @@ export * from './layout-radial-prevent-overlap-unstrict';
export * from './layout-radial-sort';
export * from './plugin-contextmenu';
export * from './plugin-grid-line';
export * from './plugin-legend';
export * from './plugin-toolbar-build-in';
export * from './plugin-toolbar-iconfont';
export * from './plugin-tooltip';

View File

@ -0,0 +1,79 @@
import { Graph } from '@/src';
import data from '@@/dataset/cluster.json';
import { isObject } from '@antv/util';
export const pluginLegend: TestCase = async (context) => {
const { nodes, edges } = data;
const findCluster = (id: string) => {
return nodes.find(({ id: node }) => node === id)?.data.cluster;
};
const graph = new Graph({
...context,
data: {
nodes,
edges: edges.map(({ source, target }) => {
return {
source,
target,
id: `${source}-${target}`,
data: {
cluster: `${findCluster(source)}-${findCluster(target)}`,
},
};
}),
},
layout: { type: 'd3force' },
behaviors: ['drag-canvas', 'drag-element'],
node: {
style: {
labelText: (d) => d.id,
lineWidth: 0,
type: (item: any) => {
if (item.data.cluster === 'a') return 'diamond';
if (item.data.cluster === 'b') return 'rect';
if (item.data.cluster === 'c') return 'triangle';
return 'circle';
},
color: (item: any) => {
if (item.data.cluster === 'a') return 'red';
if (item.data.cluster === 'b') return 'blue';
if (item.data.cluster === 'c') return 'green';
return '#99add1';
},
},
},
plugins: [
{
key: 'legend',
type: 'legend',
titleText: 'Cluster Legend',
nodeField: 'cluster',
edgeField: 'cluster',
trigger: 'click',
},
],
});
await graph.render();
pluginLegend.form = (panel) => {
const config = {
trigger: 'hover',
};
return [
panel
.add(config, 'trigger', ['hover', 'click'])
.name('Change Trigger Method')
.onChange((trigger: string) => {
graph.setPlugins((plugins) =>
plugins.map((plugin) => {
if (isObject(plugin) && plugin.type === 'legend') return { ...plugin, trigger };
return plugin;
}),
);
}),
];
};
return graph;
};

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 82 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 85 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 85 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 81 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 81 KiB

View File

@ -0,0 +1,67 @@
import { Legend } from '@/src/plugins/legend';
import { pluginLegend } from '@@/demos';
import { createDemoGraph } from '@@/utils';
const mockEvent: any = {
__data__: {
id: 'node__0',
index: 0,
style: {
layout: 'flex',
labelText: 'a',
markerLineWidth: 3,
marker: 'diamond',
markerStroke: '#000000',
markerFill: 'red',
spacing: 4,
markerSize: 16,
labelFontSize: 16,
markerOpacity: 1,
labelOpacity: 1,
},
},
};
describe('plugin legend', () => {
it('normal', async () => {
const graph = await createDemoGraph(pluginLegend);
await expect(graph).toMatchSnapshot(__filename, 'normal');
graph.destroy();
});
it('click', async () => {
const graph = await createDemoGraph(pluginLegend);
const legend = graph.getPluginInstance<Legend>('legend');
legend.click(mockEvent);
await expect(graph).toMatchSnapshot(__filename, 'click');
legend.click(mockEvent);
await expect(graph).toMatchSnapshot(__filename, 'click-again');
graph.destroy();
});
it('update trigger to hover', async () => {
const graph = await createDemoGraph(pluginLegend);
graph.setPlugins((plugins) =>
plugins.map((plugin) => {
if (typeof plugin === 'object') {
return {
...plugin,
trigger: 'hover',
position: 'top',
};
}
return plugin;
}),
);
const legend = graph.getPluginInstance<Legend>('legend');
legend.mouseenter(mockEvent);
await expect(graph).toMatchSnapshot(__filename, 'mouseenter');
legend.mouseleave(mockEvent);
await expect(graph).toMatchSnapshot(__filename, 'mouseleave');
graph.destroy();
});
});

View File

@ -55,7 +55,7 @@
"type-check": "tsc --noEmit"
},
"dependencies": {
"@antv/component": "^1.0.2",
"@antv/component": "^1.0.3",
"@antv/event-emitter": "latest",
"@antv/g": "^5.18.25",
"@antv/g-canvas": "^1.11.27",

View File

@ -1,6 +1,7 @@
export { BasePlugin } from './base-plugin';
export { Contextmenu } from './contextmenu';
export { GridLine } from './grid-line';
export { Legend } from './legend';
export { Toolbar } from './toolbar';
export { Tooltip } from './tooltip';
export { Watermark } from './watermark';
@ -8,6 +9,7 @@ export { Watermark } from './watermark';
export type { BasePluginOptions } from './base-plugin';
export type { ContextmenuOptions } from './contextmenu';
export type { GridLineOptions } from './grid-line';
export type { LegendOptions } from './legend';
export type { ToolbarOptions } from './toolbar';
export type { TooltipOptions } from './tooltip';
export type { WatermarkOptions } from './watermark';

View File

@ -0,0 +1,321 @@
import type { CategoryOptions } from '@antv/component';
import { Category, Layout, Selection } from '@antv/component';
import type { ID } from '@antv/graphlib';
import { get, isFunction } from '@antv/util';
import { GraphEvent } from '../constants';
import type { RuntimeContext } from '../runtime/types';
import type { ElementDatum, ElementType, State } from '../types';
import type { CardinalPlacement } from '../types/placement';
import type { BasePluginOptions } from './base-plugin';
import { BasePlugin } from './base-plugin';
type Field = string | ((item: ElementDatum) => string);
type Datum = {
id?: string;
label?: string;
color?: string;
marker?: string;
elementType?: ElementType;
[key: string]: any;
};
export interface LegendOptions extends BasePluginOptions, Omit<CategoryOptions, 'data'> {
/** <zh/> 触发方式 | <en/> Event type that triggers display of legend */
trigger?: 'hover' | 'click';
/** <zh/> 位置 | <en/> Legend position */
position?: CardinalPlacement;
/** <zh/> 节点分类标识 | <en/> Node Classification Identifier */
nodeField?: Field;
/** <zh/> 边分类标识 | <en/> Edge Classification Identifier */
edgeField?: Field;
/** <zh/> Combo分类标识 | <en/> Combo Classification Identifier */
comboField?: Field;
}
export class Legend extends BasePlugin<LegendOptions> {
static defaultOptions: Partial<LegendOptions> = {
position: 'bottom',
trigger: 'hover',
orientation: 'horizontal',
layout: 'flex',
itemSpacing: 4,
rowPadding: 10,
colPadding: 10,
itemMarkerSize: 16,
itemLabelFontSize: 16,
};
private typePrefix = '__data__';
private element: Layout | null = null;
private draw = false;
private fieldMap = {
node: new Map<string, ID[]>(),
edge: new Map<string, ID[]>(),
combo: new Map<string, ID[]>(),
};
private selectedItems: string[] = [];
constructor(context: RuntimeContext, options: LegendOptions) {
super(context, Object.assign({}, Legend.defaultOptions, options));
this.bindEvents();
}
public update(options: Partial<LegendOptions>) {
super.update(options);
this.clear();
this.createElement();
}
private clear() {
this.element?.destroy();
this.element = null;
this.draw = false;
}
private bindEvents = () => {
const { graph } = this.context;
graph.on(GraphEvent.AFTER_DRAW, this.createElement);
};
private changeState = (el: Selection, state: State | State[]) => {
const { graph } = this.context;
const { typePrefix } = this;
const composeId = get(el, [typePrefix, 'id']);
const category = get(el, [typePrefix, 'style', 'labelText']);
const [type] = composeId.split('__');
const ids = this.fieldMap[type as keyof typeof this.fieldMap].get(category) || [];
graph.setElementState(Object.fromEntries(ids?.map((id) => [id, state])));
};
public click = (el: Selection) => {
if (this.options.trigger === 'hover') return;
const composeId = get(el, [this.typePrefix, 'id']);
if (!this.selectedItems.includes(composeId)) {
this.selectedItems.push(composeId);
this.changeState(el, 'selected');
} else {
this.selectedItems = this.selectedItems.filter((item) => item !== composeId);
this.changeState(el, []);
}
};
public mouseleave = (el: Selection) => {
if (this.options.trigger === 'click') return;
this.selectedItems = [];
this.changeState(el, []);
};
public mouseenter = (el: Selection) => {
if (this.options.trigger === 'click') return;
const composeId = get(el, [this.typePrefix, 'id']);
if (!this.selectedItems.includes(composeId)) {
this.selectedItems.push(composeId);
this.changeState(el, 'active');
} else {
this.selectedItems = this.selectedItems.filter((item) => item !== composeId);
}
};
public updateElement() {
if (!this.element) return;
const category = this.element.getChildByIndex(0) as Category;
category.update({
itemMarkerOpacity: ({ id }) => {
if (!this.selectedItems.length || this.selectedItems.includes(id)) return 1;
return 0.5;
},
itemLabelOpacity: ({ id }) => {
if (!this.selectedItems.length || this.selectedItems.includes(id)) return 1;
return 0.5;
},
});
}
private setFieldMap = (field: string, id: ID, type: ElementType) => {
if (!field) return;
const map = this.fieldMap[type];
if (!map) return;
if (!map.has(field)) {
map.set(field, [id]);
} else {
const ids = map.get(field);
if (ids) {
ids.push(id);
map.set(field, ids);
}
}
};
private getEvents = () => {
return {
mouseenter: this.mouseenter,
mouseleave: this.mouseleave,
click: this.click,
};
};
private getMarkerData = (field: Field, elementType: ElementType) => {
if (!field) return [];
const { model, element, graph } = this.context;
const { nodes, edges, combos } = model.getData();
const items: { [key: string]: Datum } = {};
const getField = (item: ElementDatum) => {
if (isFunction(field)) return field(item);
return field;
};
const defaultType = {
node: 'circle',
edge: 'line',
combo: 'rect',
};
/** 用于将 G6 element 转换为 componets 支持的类型 */
const markerMapping: { [key: string]: string } = {
circle: 'circle',
ellipse: 'circle', // 待 components 支持 ellipse
image: 'bowtie',
rect: 'square',
star: 'cross',
triangle: 'triangle',
diamond: 'diamond',
cubic: 'dot',
line: 'hyphen',
polyline: 'hyphen',
quadratic: 'hv',
'cubic-horizontal': 'hyphen',
'cubic-vertical': 'line',
};
const getElementStyle = (type: ElementType, datum: ElementDatum) => {
const style = element?.getElementComputedStyle(type, datum);
return style;
};
const getElementModel = (data: ElementDatum[], type: ElementType) => {
data.forEach((item) => {
const { id } = item;
const value = get(item, ['data', getField(item)]);
const { color = '#1783ff', type: marker = 'circle' } = getElementStyle(type, item);
if ((id || id === 0) && value && value.replace(/\s+/g, '')) {
this.setFieldMap(value, id, type);
if (!items[value]) {
items[value] = {
id: `${type}__${id}`,
label: value,
marker: markerMapping[marker] || defaultType[type],
elementType: type,
lineWidth: 1,
stroke: color,
fill: color,
};
}
}
});
};
switch (elementType) {
case 'node':
getElementModel(nodes, 'node');
break;
case 'edge':
getElementModel(edges, 'edge');
break;
case 'combo':
getElementModel(combos, 'combo');
break;
default:
return [];
}
return Object.values(items);
};
public layout = (position: CardinalPlacement) => {
const preset = {
flexDirection: 'row',
alignItems: 'flex-end',
justifyContent: 'center',
};
let { flexDirection, alignItems, justifyContent } = preset;
const layout = {
top: ['row', 'flex-start', 'center'],
bottom: ['row', 'flex-end', 'center'],
left: ['column', 'flex-start', 'center'],
right: ['column', 'flex-end', 'center'],
};
if (position in layout) {
[flexDirection, alignItems, justifyContent] = layout[position];
}
return {
display: 'flex',
flexDirection,
justifyContent,
alignItems,
};
};
private createElement = () => {
if (this.draw) {
this.updateElement();
return;
}
const { canvas } = this.context;
const [canvasWidth, canvasHeiht] = canvas.getSize();
const {
width = canvasWidth,
height = canvasHeiht,
nodeField,
edgeField,
comboField,
trigger,
position,
...rest
} = this.options;
const nodeItems = this.getMarkerData(nodeField, 'node');
const edgeItems = this.getMarkerData(edgeField, 'edge');
const comboItems = this.getMarkerData(comboField, 'combo');
const items = [...nodeItems, ...comboItems, ...edgeItems];
const layout = this.layout(position);
const layoutWrapper = new Layout({
style: {
width,
height,
...layout,
},
});
const categoryStyle = {
width,
height,
data: items,
itemMarkerLineWidth: ({ lineWidth }: Datum) => lineWidth,
itemMarker: ({ marker }: Datum) => marker,
itemMarkerStroke: ({ stroke }: Datum) => stroke,
itemMarkerFill: ({ fill }: Datum) => fill,
gridCol: nodeItems.length,
...rest,
...this.getEvents(),
};
const category = new Category({
className: 'legend',
style: categoryStyle,
});
layoutWrapper.appendChild(category);
canvas.appendChild(layoutWrapper);
this.element = layoutWrapper;
this.draw = true;
};
public destroy(): void {
this.clear();
this.context.graph.off(GraphEvent.AFTER_DRAW, this.createElement);
super.destroy();
}
}

View File

@ -38,7 +38,7 @@ import {
mindmap,
} from '../layouts';
import { blues, greens, oranges, spectral } from '../palettes';
import { Contextmenu, GridLine, Toolbar, Tooltip, Watermark } from '../plugins';
import { Contextmenu, GridLine, Legend, Toolbar, Tooltip, Watermark } from '../plugins';
import { dark, light } from '../themes';
import type { ExtensionRegistry } from './types';
@ -117,5 +117,6 @@ export const BUILT_IN_EXTENSIONS: ExtensionRegistry = {
tooltip: Tooltip,
contextmenu: Contextmenu,
toolbar: Toolbar,
legend: Legend,
},
};

View File

@ -0,0 +1,98 @@
import { Graph } from '@antv/g6';
const data = {
nodes: [
{
id: '1',
style: {
type: 'circle',
color: '#5B8FF9',
},
data: { cluster: 'node-type1' },
},
{
id: '2',
style: {
type: 'rect',
color: '#5AD8A6',
},
data: { cluster: 'node-type2' },
},
{
id: '3',
style: {
type: 'triangle',
color: '#5D7092',
},
data: { cluster: 'node-type3' },
},
{
id: '4',
style: {
type: 'diamond',
color: '#6F5EF9',
},
data: { cluster: 'node-type4' },
},
],
edges: [
{
id: '1-2',
source: '1',
target: '2',
style: {
type: 'quadratic',
color: '#F6BD16',
},
data: { cluster: 'edge-type1' },
},
{
id: '1-4',
source: '1',
target: '4',
data: { cluster: 'edge-type2' },
},
{
id: '3-4',
source: '3',
target: '4',
},
{
id: '2-4',
source: '2',
target: '4',
data: { cluster: 'edge-type3' },
},
],
};
const graph = new Graph({
container: 'container',
data,
node: {
style: {
labelPosition: 'bottom',
stroke: '#fff',
lineWidth: 4,
},
},
edge: {
style: {
stroke: '#fff',
lineWidth: 4,
},
},
layout: {
type: 'force',
},
plugins: [
{
type: 'legend',
nodeField: 'cluster',
edgeField: 'cluster',
trigger: 'click',
},
],
});
graph.render();

View File

@ -1,167 +1,88 @@
import { Graph as BaseGraph, Extensions, extend } from '@antv/g6';
import { Graph } from '@antv/g6';
const Graph = extend(BaseGraph, {
plugins: {
legend: Extensions.Legend,
},
behaviors: {
'brush-select': Extensions.BrushSelect,
'activate-relations': Extensions.ActivateRelations,
'zoom-canvas': Extensions.ZoomCanvas,
},
});
const container = document.getElementById('container') as HTMLElement;
const width = container.scrollWidth;
const height = (container.scrollHeight || 500) - 50;
/** graph schema */
const GraphSchema = {
const data = {
nodes: [
{
nodeType: 'person',
properties: {
id: 'string',
name: 'string',
phoneNumber: 'number',
id: '1',
style: {
type: 'circle',
color: '#5B8FF9',
},
count: 10,
data: { cluster: 'node-type1' },
},
{
nodeType: 'company',
properties: {
id: 'string',
name: 'string',
id: '2',
style: {
type: 'rect',
color: '#5AD8A6',
},
count: 3,
data: { cluster: 'node-type2' },
},
{
id: '3',
style: {
type: 'triangle',
color: '#5D7092',
},
data: { cluster: 'node-type3' },
},
{
id: '4',
style: {
type: 'diamond',
color: '#6F5EF9',
},
data: { cluster: 'node-type4' },
},
],
edges: [
{
edgeType: 'friend',
sourceType: 'person',
targetType: 'person',
properties: {},
count: 3,
source: '1',
target: '2',
data: { cluster: 'edge-type1' },
},
{
edgeType: 'employ',
sourceType: 'company',
targetType: 'person',
properties: {},
count: 4,
source: '1',
target: '4',
data: { cluster: 'edge-type2' },
},
{
edgeType: 'legal',
sourceType: 'person',
targetType: 'company',
properties: {},
count: 5,
source: '3',
target: '4',
},
{
source: '2',
target: '4',
data: { cluster: 'edge-type3' },
},
],
};
const nodeMap = new Map();
const nodeTypeMap = new Map();
const edgeMap = new Map();
GraphSchema.nodes.forEach((item, index) => {
const { nodeType, count, properties } = item;
Array.from({ length: count }).map((c, i) => {
const id = `${nodeType}-${index}-${i}`;
const node = {
id,
data: {
...properties, // can import facker
nodeType,
id,
},
};
nodeMap.set(id, node);
const nty = nodeTypeMap.get(nodeType) || [];
nodeTypeMap.set(nodeType, [...nty, node]);
});
});
GraphSchema.edges.forEach((item, index) => {
const { edgeType, count, properties, sourceType, targetType } = item;
const sources = nodeTypeMap.get(sourceType);
const targets = nodeTypeMap.get(targetType);
Array.from({ length: count }).forEach((c, i) => {
const id = `${edgeType}-${index}-${i}`;
const edge = {
id,
data: {
...properties, // can import facker
edgeType,
id,
},
source: sources[i % sources.length].id,
target: targets[i % targets.length].id,
};
edgeMap.set(id, edge);
});
});
const data = {
nodes: [...nodeMap.values()],
edges: [...edgeMap.values()],
};
const legend = {
key: 'default-legend',
type: 'legend',
size: [250, 'fit-content'],
background: 'rgba(0,0,0,0.05)',
const graph = new Graph({
container: 'container',
data,
node: {
enable: true,
padding: [20, 20],
title: 'node-legend',
typeField: 'nodeType',
rows: 1,
cols: 4,
labelStyle: {
spacing: 8,
fontSize: 20,
style: {
labelPosition: 'bottom',
stroke: '#fff',
lineWidth: 4,
},
},
edge: {
enable: true,
padding: [10, 20],
title: 'edge-legend',
typeField: 'edgeType',
style: {
stroke: '#fff',
lineWidth: 4,
},
},
};
new Graph({
container,
width,
height,
data,
plugins: [legend],
layout: {
type: 'force',
},
theme: {
type: 'spec',
specification: {
node: {
dataTypeField: 'nodeType',
},
edge: {
dataTypeField: 'edgeType',
},
plugins: [
{
type: 'legend',
nodeField: 'cluster',
},
},
node: {
labelShape: {
text: {
fields: ['id'],
formatter: (model) => {
return model.id;
},
},
},
},
modes: {
default: ['brush-select', 'zoom-canvas', 'activate-relations', 'drag-canvas', 'drag-element'],
},
],
});
graph.render();

View File

@ -1,177 +0,0 @@
import { Graph as BaseGraph, Extensions, extend } from '@antv/g6';
const Graph = extend(BaseGraph, {
plugins: {
legend: Extensions.Legend,
},
behaviors: {
'brush-select': Extensions.BrushSelect,
'activate-relations': Extensions.ActivateRelations,
'zoom-canvas': Extensions.ZoomCanvas,
},
});
const container = document.getElementById('container') as HTMLElement;
const width = container.scrollWidth;
const height = (container.scrollHeight || 500) - 50;
/** graph schema */
const GraphSchema = {
nodes: [
{
nodeType: 'person',
properties: {
id: 'string',
name: 'string',
phoneNumber: 'number',
},
count: 10,
},
{
nodeType: 'company',
properties: {
id: 'string',
name: 'string',
},
count: 3,
},
],
edges: [
{
edgeType: 'friend',
sourceType: 'person',
targetType: 'person',
properties: {},
count: 3,
},
{
edgeType: 'employ',
sourceType: 'company',
targetType: 'person',
properties: {},
count: 4,
},
{
edgeType: 'legal',
sourceType: 'person',
targetType: 'company',
properties: {},
count: 5,
},
],
};
const nodeMap = new Map();
const nodeTypeMap = new Map();
const edgeMap = new Map();
GraphSchema.nodes.forEach((item, index) => {
const { nodeType, count, properties } = item;
Array.from({ length: count }).map((c, i) => {
const id = `${nodeType}-${index}-${i}`;
const node = {
id,
data: {
...properties, // can import facker
nodeType,
id,
},
};
nodeMap.set(id, node);
const nty = nodeTypeMap.get(nodeType) || [];
nodeTypeMap.set(nodeType, [...nty, node]);
});
});
GraphSchema.edges.forEach((item, index) => {
const { edgeType, count, properties, sourceType, targetType } = item;
const sources = nodeTypeMap.get(sourceType);
const targets = nodeTypeMap.get(targetType);
Array.from({ length: count }).forEach((c, i) => {
const id = `${edgeType}-${index}-${i}`;
const edge = {
id,
data: {
...properties, // can import facker
edgeType,
id,
},
source: sources[i % sources.length].id,
target: targets[i % targets.length].id,
};
edgeMap.set(id, edge);
});
});
const data = {
nodes: [...nodeMap.values()],
edges: [...edgeMap.values()],
};
const legend = {
key: 'default-legend',
type: 'legend',
size: [250, 'fit-content'],
background: 'rgba(0,0,0,0.05)',
node: {
enable: true,
padding: [20, 20],
title: 'node-legend',
typeField: 'nodeType',
rows: 1,
cols: 4,
labelStyle: {
spacing: 8,
fontSize: 20,
},
markerStyle: {
shape: 'circle',
size: (type) => {
return type === 'person' ? 10 : 20;
},
color: (type) => {
return type === 'person' ? '#f00' : '#0f0';
},
},
},
edge: {
enable: true,
padding: [10, 20],
title: 'edge-legend',
typeField: 'edgeType',
markerStyle: {
color: (type) => {
switch (type) {
case 'friend':
return '#00f';
case 'employ':
return '#f0f';
case 'legal':
return '#0ff';
}
},
},
},
};
new Graph({
container,
width,
height,
data,
layout: {
type: 'force',
},
plugins: [legend],
node: {
labelShape: {
text: {
fields: ['id'],
formatter: (model) => {
return model.id;
},
},
},
},
modes: {
default: ['brush-select', 'zoom-canvas', 'activate-relations', 'drag-canvas', 'drag-element'],
},
});

View File

@ -1,168 +0,0 @@
import { Graph as BaseGraph, Extensions, extend } from '@antv/g6';
const Graph = extend(BaseGraph, {
plugins: {
legend: Extensions.Legend,
},
behaviors: {
'brush-select': Extensions.BrushSelect,
'activate-relations': Extensions.ActivateRelations,
'zoom-canvas': Extensions.ZoomCanvas,
},
});
const container = document.getElementById('container') as HTMLElement;
const width = container.scrollWidth;
const height = (container.scrollHeight || 500) - 50;
/** graph schema */
const GraphSchema = {
nodes: [
{
nodeType: 'person',
properties: {
id: 'string',
name: 'string',
phoneNumber: 'number',
},
count: 10,
},
{
nodeType: 'company',
properties: {
id: 'string',
name: 'string',
},
count: 3,
},
],
edges: [
{
edgeType: 'friend',
sourceType: 'person',
targetType: 'person',
properties: {},
count: 3,
},
{
edgeType: 'employ',
sourceType: 'company',
targetType: 'person',
properties: {},
count: 4,
},
{
edgeType: 'legal',
sourceType: 'person',
targetType: 'company',
properties: {},
count: 5,
},
],
};
const nodeMap = new Map();
const nodeTypeMap = new Map();
const edgeMap = new Map();
GraphSchema.nodes.forEach((item, index) => {
const { nodeType, count, properties } = item;
Array.from({ length: count }).map((c, i) => {
const id = `${nodeType}-${index}-${i}`;
const node = {
id,
data: {
...properties, // can import facker
nodeType,
id,
},
};
nodeMap.set(id, node);
const nty = nodeTypeMap.get(nodeType) || [];
nodeTypeMap.set(nodeType, [...nty, node]);
});
});
GraphSchema.edges.forEach((item, index) => {
const { edgeType, count, properties, sourceType, targetType } = item;
const sources = nodeTypeMap.get(sourceType);
const targets = nodeTypeMap.get(targetType);
Array.from({ length: count }).forEach((c, i) => {
const id = `${edgeType}-${index}-${i}`;
const edge = {
id,
data: {
...properties, // can import facker
edgeType,
id,
},
source: sources[i % sources.length].id,
target: targets[i % targets.length].id,
};
edgeMap.set(id, edge);
});
});
const data = {
nodes: [...nodeMap.values()],
edges: [...edgeMap.values()],
};
const legend = {
key: 'default-legend',
type: 'legend',
renderer: 'svg',
size: [250, 'fit-content'],
background: 'rgba(0,0,0,0.05)',
node: {
enable: true,
padding: [20, 20],
title: 'node-legend',
typeField: 'nodeType',
rows: 1,
cols: 4,
labelStyle: {
spacing: 8,
fontSize: 20,
},
},
edge: {
enable: true,
padding: [10, 20],
title: 'edge-legend',
typeField: 'edgeType',
},
};
new Graph({
container,
width,
height,
data,
plugins: [legend],
layout: {
type: 'force',
},
theme: {
type: 'spec',
specification: {
node: {
dataTypeField: 'nodeType',
},
edge: {
dataTypeField: 'edgeType',
},
},
},
node: {
labelShape: {
text: {
fields: ['id'],
formatter: (model) => {
return model.id;
},
},
},
},
modes: {
default: ['brush-select', 'zoom-canvas', 'activate-relations', 'drag-canvas', 'drag-element'],
},
});

View File

@ -7,26 +7,26 @@
{
"filename": "legend.ts",
"title": {
"zh": "图例-点击与 hover 筛选",
"en": "Filtering legend by clicking and hovering"
"zh": "默认图例",
"en": "Default legend"
},
"screenshot": "https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*53oGRpdKpwsAAAAAAAAAAAAADmJ7AQ/original"
"screenshot": "https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*pGMMQIVmtdoAAAAAAAAAAAAADmJ7AQ/original"
},
{
"filename": "legendCustomMarker.ts",
"filename": "legend-click.ts",
"title": {
"zh": "图例-自定义 Marker 样式",
"en": "Customize legend marker style"
"zh": "点击图例",
"en": "Click legend"
},
"screenshot": "https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*40awTrXVQ1UAAAAAAAAAAAAADmJ7AQ/original"
"screenshot": "https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*krcoS6DXEnYAAAAAAAAAAAAADmJ7AQ/original"
},
{
"filename": "legendSVG.ts",
"filename": "style.ts",
"title": {
"zh": "图例-使用 SVG 绘制图例",
"en": "Render legend with SVG"
"zh": "Marker 样式",
"en": "Marker style"
},
"screenshot": "https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*53oGRpdKpwsAAAAAAAAAAAAADmJ7AQ/original"
"screenshot": "https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*cnYaQLn2zkQAAAAAAAAAAAAADmJ7AQ/original"
}
]
}

View File

@ -0,0 +1,101 @@
import { Graph } from '@antv/g6';
const data = {
nodes: [
{
id: '1',
style: {
type: 'circle',
color: '#5B8FF9',
},
data: { cluster: 'node-type1' },
},
{
id: '2',
style: {
type: 'rect',
color: '#5AD8A6',
},
data: { cluster: 'node-type2' },
},
{
id: '3',
style: {
type: 'triangle',
color: '#5D7092',
},
data: { cluster: 'node-type3' },
},
{
id: '4',
style: {
type: 'diamond',
color: '#6F5EF9',
},
data: { cluster: 'node-type4' },
},
],
edges: [
{
id: '1-2',
source: '1',
target: '2',
style: {
type: 'quadratic',
color: '#F6BD16',
},
data: { cluster: 'edge-type1' },
},
{
id: '1-4',
source: '1',
target: '4',
data: { cluster: 'edge-type2' },
},
{
id: '3-4',
source: '3',
target: '4',
},
{
id: '2-4',
source: '2',
target: '4',
data: { cluster: 'edge-type3' },
},
],
};
const graph = new Graph({
container: 'container',
data,
node: {
style: {
labelPosition: 'bottom',
stroke: '#fff',
lineWidth: 4,
},
},
edge: {
style: {
stroke: '#fff',
lineWidth: 4,
},
},
layout: {
type: 'force',
},
plugins: [
{
type: 'legend',
nodeField: 'cluster',
edgeField: 'cluster',
titleText: 'Legend Title',
trigger: 'click',
position: 'top',
gridCol: 3,
},
],
});
graph.render();