mirror of
https://gitee.com/antv/g6.git
synced 2024-12-02 03:38:20 +08:00
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:
parent
923c62a639
commit
d0c3c7dbc5
@ -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';
|
||||
|
79
packages/g6/__tests__/demos/plugin-legend.ts
Normal file
79
packages/g6/__tests__/demos/plugin-legend.ts
Normal 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;
|
||||
};
|
1137
packages/g6/__tests__/snapshots/plugins/legend/click-again.svg
Normal file
1137
packages/g6/__tests__/snapshots/plugins/legend/click-again.svg
Normal file
File diff suppressed because it is too large
Load Diff
After Width: | Height: | Size: 82 KiB |
1176
packages/g6/__tests__/snapshots/plugins/legend/click.svg
Normal file
1176
packages/g6/__tests__/snapshots/plugins/legend/click.svg
Normal file
File diff suppressed because it is too large
Load Diff
After Width: | Height: | Size: 85 KiB |
1176
packages/g6/__tests__/snapshots/plugins/legend/mouseenter.svg
Normal file
1176
packages/g6/__tests__/snapshots/plugins/legend/mouseenter.svg
Normal file
File diff suppressed because it is too large
Load Diff
After Width: | Height: | Size: 85 KiB |
1137
packages/g6/__tests__/snapshots/plugins/legend/mouseleave.svg
Normal file
1137
packages/g6/__tests__/snapshots/plugins/legend/mouseleave.svg
Normal file
File diff suppressed because it is too large
Load Diff
After Width: | Height: | Size: 81 KiB |
1137
packages/g6/__tests__/snapshots/plugins/legend/normal.svg
Normal file
1137
packages/g6/__tests__/snapshots/plugins/legend/normal.svg
Normal file
File diff suppressed because it is too large
Load Diff
After Width: | Height: | Size: 81 KiB |
67
packages/g6/__tests__/unit/plugins/legend.spec.ts
Normal file
67
packages/g6/__tests__/unit/plugins/legend.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
@ -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",
|
||||
|
@ -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';
|
||||
|
321
packages/g6/src/plugins/legend.ts
Normal file
321
packages/g6/src/plugins/legend.ts
Normal 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();
|
||||
}
|
||||
}
|
@ -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,
|
||||
},
|
||||
};
|
||||
|
98
packages/site/examples/tool/legend/demo/legend-click.ts
Normal file
98
packages/site/examples/tool/legend/demo/legend-click.ts
Normal 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();
|
@ -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();
|
||||
|
@ -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'],
|
||||
},
|
||||
});
|
@ -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'],
|
||||
},
|
||||
});
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
101
packages/site/examples/tool/legend/demo/style.ts
Normal file
101
packages/site/examples/tool/legend/demo/style.ts
Normal 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();
|
Loading…
Reference in New Issue
Block a user