mirror of
https://gitee.com/antv/g6.git
synced 2024-12-02 19:58:46 +08:00
Merge pull request #4873 from k644606347/feat-scroll-canvas
Feat scroll canvas
This commit is contained in:
commit
64c56373ed
@ -14,3 +14,4 @@ export * from './rotate-canvas-3d';
|
||||
export * from './track-canvas-3d';
|
||||
export * from './zoom-canvas-3d';
|
||||
export * from './shortcuts-call';
|
||||
export * from './scroll-canvas';
|
||||
|
313
packages/g6/src/stdlib/behavior/scroll-canvas.ts
Normal file
313
packages/g6/src/stdlib/behavior/scroll-canvas.ts
Normal file
@ -0,0 +1,313 @@
|
||||
import { isBoolean, isObject } from '@antv/util';
|
||||
import { Behavior } from '../../types/behavior';
|
||||
import { ID, IG6GraphEvent } from '../../types';
|
||||
|
||||
interface ScrollCanvasOptions {
|
||||
/**
|
||||
* The direction of dragging that is allowed. Options: 'x', 'y', 'both'. 'both' by default.
|
||||
*/
|
||||
direction?: string;
|
||||
/**
|
||||
* Whether enable optimize strategies, which will hide all the shapes excluding node keyShape while scrolling.
|
||||
*/
|
||||
enableOptimize?: boolean;
|
||||
/**
|
||||
* When the zoom ratio of the graph is smaller than ```optimizeZoom```, all shapes except for node keyShape will always be hidden.
|
||||
* This option requires ```enableOptimize=true```;
|
||||
*/
|
||||
optimizeZoom?: number;
|
||||
/**
|
||||
* Switch to zooming while pressing the key and wheeling. Options: 'shift', 'ctrl', 'alt', 'control', 'meta', using an array of these options allows any of these keys to trigger zooming;
|
||||
* Use ```'ctrl'``` by default;
|
||||
*/
|
||||
zoomKey?: string | string[];
|
||||
/** Switch to zooming while pressing the key and wheeling. This option allows you to control the zoom ratio for each event.
|
||||
* Use ```'0.05'``` by default;
|
||||
*/
|
||||
zoomRatio?: number;
|
||||
/**
|
||||
* The range of canvas to limit dragging, 0 by default, which means the graph cannot be dragged totally out of the view port range.
|
||||
* If scalableRange is number or a string without 'px', means it is a ratio of the graph content.
|
||||
* If scalableRange is a string with 'px', it is regarded as pixels.
|
||||
* If scalableRange = 0, no constrains;
|
||||
* If scalableRange > 0, the graph can be dragged out of the view port range
|
||||
* If scalableRange < 0, the range is smaller than the view port.
|
||||
* Refer to https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*IFfoS67_HssAAAAAAAAAAAAAARQnAQ
|
||||
*/
|
||||
scalableRange?: string | number;
|
||||
/**
|
||||
* Whether allow trigger this behavior when drag start on nodes / edges / combos.
|
||||
*/
|
||||
allowDragOnItem?:
|
||||
| boolean
|
||||
| {
|
||||
node?: boolean;
|
||||
edge?: boolean;
|
||||
combo?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
const DEFAULT_OPTIONS: ScrollCanvasOptions = {
|
||||
direction: 'both',
|
||||
enableOptimize: false,
|
||||
zoomKey: 'ctrl',
|
||||
// scroll-canvas 可滚动的扩展范围,默认为 0,即最多可以滚动一屏的位置
|
||||
// 当设置的值大于 0 时,即滚动可以超过一屏
|
||||
// 当设置的值小于 0 时,相当于缩小了可滚动范围
|
||||
// 具体实例可参考:https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*IFfoS67_HssAAAAAAAAAAAAAARQnAQ
|
||||
scalableRange: 0,
|
||||
allowDragOnItem: true,
|
||||
zoomRatio: 0.05,
|
||||
};
|
||||
|
||||
|
||||
export class ScrollCanvas extends Behavior {
|
||||
private hiddenEdgeIds: ID[];
|
||||
private hiddenNodeIds: ID[];
|
||||
|
||||
declare options: ScrollCanvasOptions;
|
||||
timeout?: number;
|
||||
optimized = false;
|
||||
constructor(options: Partial<ScrollCanvasOptions>) {
|
||||
const finalOptions = Object.assign({}, DEFAULT_OPTIONS, options, {
|
||||
zoomKey: initZoomKey(options.zoomKey),
|
||||
});
|
||||
super(finalOptions);
|
||||
}
|
||||
|
||||
getEvents = () => {
|
||||
return {
|
||||
wheel: this.onWheel,
|
||||
};
|
||||
};
|
||||
|
||||
onWheel(ev: IG6GraphEvent & { deltaX?: number; deltaY?: number; }) {
|
||||
if (!this.allowDrag(ev)) return;
|
||||
const graph = this.graph;
|
||||
const { zoomKey, zoomRatio, scalableRange, direction, enableOptimize } = this.options;
|
||||
const zoomKeys = Array.isArray(zoomKey) ? [].concat(zoomKey) : [zoomKey];
|
||||
if (zoomKeys.includes('control')) zoomKeys.push('ctrl');
|
||||
const keyDown = zoomKeys.some((ele) => ev[`${ele}Key`]);
|
||||
|
||||
const nativeEvent = ev.nativeEvent as WheelEvent & { wheelDelta: number } | undefined;
|
||||
|
||||
if (keyDown) {
|
||||
const canvas = graph.canvas;
|
||||
const point = canvas.client2Viewport({ x: ev.client.x, y: ev.client.y});
|
||||
let ratio = graph.getZoom();
|
||||
if (nativeEvent && nativeEvent.wheelDelta > 0) {
|
||||
ratio = ratio + ratio * zoomRatio;
|
||||
} else {
|
||||
ratio = ratio - ratio * zoomRatio;
|
||||
}
|
||||
graph.zoomTo(ratio, {
|
||||
x: point.x,
|
||||
y: point.y,
|
||||
});
|
||||
} else {
|
||||
const diffX = ev.deltaX || ev.movement.x;
|
||||
const diffY = ev.deltaY || ev.movement.y;
|
||||
|
||||
const { dx, dy } = this.formatDisplacement(diffX, diffY);
|
||||
graph.translate({ dx: -dx, dy: -dy });
|
||||
}
|
||||
|
||||
if (enableOptimize) {
|
||||
const optimized = this.optimized;
|
||||
|
||||
// hiding
|
||||
if (!optimized) {
|
||||
this.hideShapes();
|
||||
this.optimized = true;
|
||||
}
|
||||
|
||||
// showing after 100ms
|
||||
clearTimeout(this.timeout);
|
||||
this.timeout = undefined;
|
||||
const timeout = window.setTimeout(() => {
|
||||
this.showShapes();
|
||||
this.optimized = false;
|
||||
}, 100);
|
||||
this.timeout = timeout;
|
||||
}
|
||||
}
|
||||
private formatDisplacement(dx: number, dy: number) {
|
||||
const { graph } = this;
|
||||
const { scalableRange, direction } = this.options;
|
||||
const [width, height] = graph.getSize();
|
||||
const graphBBox = graph.canvas.getRoot().getRenderBounds();
|
||||
let rangeNum = Number(scalableRange);
|
||||
let isPixel;
|
||||
if (typeof scalableRange === 'string') {
|
||||
if (scalableRange.includes('px')) {
|
||||
isPixel = scalableRange.includes('px');
|
||||
rangeNum = Number(scalableRange.replace('px', ''));
|
||||
}
|
||||
if (scalableRange.includes('%')) {
|
||||
rangeNum = Number(scalableRange.replace('%', '')) / 100;
|
||||
}
|
||||
}
|
||||
|
||||
let expandWidth = rangeNum;
|
||||
let expandHeight = rangeNum;
|
||||
// If it is not a string with 'px', regard as ratio
|
||||
if (!isPixel) {
|
||||
expandWidth = width * rangeNum;
|
||||
expandHeight = height * rangeNum;
|
||||
}
|
||||
const leftTopClient = graph.getViewportByCanvas({
|
||||
x: graphBBox.min[0],
|
||||
y: graphBBox.min[1],
|
||||
});
|
||||
const rightBottomClient = graph.getViewportByCanvas({
|
||||
x: graphBBox.max[0],
|
||||
y: graphBBox.max[1],
|
||||
});
|
||||
const minX = leftTopClient.x;
|
||||
const minY = leftTopClient.y;
|
||||
const maxX = rightBottomClient.x;
|
||||
const maxY = rightBottomClient.y;
|
||||
if (dx > 0) {
|
||||
if (maxX < -expandWidth) {
|
||||
dx = 0;
|
||||
} else if (maxX - dx < -expandWidth) {
|
||||
dx = maxX + expandWidth;
|
||||
}
|
||||
} else if (dx < 0) {
|
||||
if (minX > width + expandWidth) {
|
||||
dx = 0;
|
||||
} else if (minX - dx > width + expandWidth) {
|
||||
dx = minX - (width + expandWidth);
|
||||
}
|
||||
}
|
||||
|
||||
if (dy > 0) {
|
||||
if (maxY < -expandHeight) {
|
||||
dy = 0;
|
||||
} else if (maxY - dy < -expandHeight) {
|
||||
dy = maxY + expandHeight;
|
||||
}
|
||||
} else if (dy < 0) {
|
||||
if (minY > height + expandHeight) {
|
||||
dy = 0;
|
||||
} else if (minY - dy > height + expandHeight) {
|
||||
dy = minY - (height + expandHeight);
|
||||
}
|
||||
}
|
||||
|
||||
if (direction === 'x') {
|
||||
dy = 0;
|
||||
} else if (direction === 'y') {
|
||||
dx = 0;
|
||||
}
|
||||
|
||||
return { dx, dy };
|
||||
}
|
||||
|
||||
private allowDrag(evt: IG6GraphEvent) {
|
||||
const { itemType } = evt;
|
||||
const { allowDragOnItem } = this.options;
|
||||
const targetIsCanvas = itemType === 'canvas';
|
||||
if (isBoolean(allowDragOnItem) && !allowDragOnItem && !targetIsCanvas)
|
||||
return false;
|
||||
if (isObject(allowDragOnItem)) {
|
||||
const { node, edge, combo } = allowDragOnItem;
|
||||
if (!node && itemType === 'node') return false;
|
||||
if (!edge && itemType === 'edge') return false;
|
||||
if (!combo && itemType === 'combo') return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private hideShapes() {
|
||||
const { graph, options } = this;
|
||||
const { optimizeZoom } = options;
|
||||
if (this.options.enableOptimize) {
|
||||
const currentZoom = graph.getZoom();
|
||||
const newHiddenEdgeIds = graph
|
||||
.getAllEdgesData()
|
||||
.map((edge) => edge.id)
|
||||
.filter((id) => graph.getItemVisible(id));
|
||||
graph.hideItem(newHiddenEdgeIds, true);
|
||||
|
||||
if (currentZoom < optimizeZoom) {
|
||||
this.hiddenEdgeIds.push(...newHiddenEdgeIds);
|
||||
} else {
|
||||
this.hiddenEdgeIds = newHiddenEdgeIds;
|
||||
}
|
||||
|
||||
const newHiddenNodeIds = graph
|
||||
.getAllNodesData()
|
||||
.map((node) => node.id)
|
||||
.filter((id) => graph.getItemVisible(id));
|
||||
// draw node's keyShapes on transient, and then hidden the real nodes;
|
||||
newHiddenNodeIds.forEach((id) => {
|
||||
graph.drawTransient('node', id, {
|
||||
onlyDrawKeyShape: true,
|
||||
upsertAncestors: false,
|
||||
});
|
||||
});
|
||||
graph.hideItem(newHiddenNodeIds, true);
|
||||
|
||||
if (currentZoom < optimizeZoom) {
|
||||
this.hiddenNodeIds.push(...newHiddenNodeIds);
|
||||
} else {
|
||||
this.hiddenNodeIds = newHiddenNodeIds;
|
||||
}
|
||||
}
|
||||
}
|
||||
private showShapes() {
|
||||
const { graph, hiddenEdgeIds, hiddenNodeIds } = this;
|
||||
const currentZoom = graph.getZoom();
|
||||
const { optimizeZoom } = this.options;
|
||||
|
||||
// hide the shapes when the zoom ratio is smaller than optimizeZoom
|
||||
// hide the shapes when zoomming
|
||||
if (currentZoom < optimizeZoom) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.hiddenEdgeIds = this.hiddenNodeIds = [];
|
||||
if (!this.options.enableOptimize) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (hiddenEdgeIds) {
|
||||
graph.showItem(hiddenEdgeIds, true);
|
||||
}
|
||||
if (hiddenNodeIds) {
|
||||
hiddenNodeIds.forEach((id) => {
|
||||
this.graph.drawTransient('node', id, { action: 'remove' });
|
||||
});
|
||||
graph.showItem(hiddenNodeIds, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const ALLOW_EVENTS = ['shift', 'ctrl', 'alt', 'control', 'meta'];
|
||||
|
||||
function initZoomKey(zoomKey?: string | string[]) {
|
||||
const zoomKeys = zoomKey
|
||||
? Array.isArray(zoomKey)
|
||||
? zoomKey
|
||||
: [zoomKey]
|
||||
: [];
|
||||
|
||||
const validZoomKeys = zoomKeys.filter((zoomKey) => {
|
||||
const keyIsValid = ALLOW_EVENTS.includes(zoomKey);
|
||||
if (!keyIsValid)
|
||||
console.warn(
|
||||
`Invalid zoomKey: ${zoomKey}, please use a valid zoomKey: ${JSON.stringify(
|
||||
ALLOW_EVENTS,
|
||||
)}`,
|
||||
);
|
||||
|
||||
return keyIsValid;
|
||||
});
|
||||
|
||||
if (validZoomKeys.length === 0) {
|
||||
validZoomKeys.push('ctrl');
|
||||
}
|
||||
|
||||
return validZoomKeys;
|
||||
}
|
@ -62,6 +62,7 @@ const {
|
||||
DragCombo,
|
||||
ClickSelect,
|
||||
ShortcutsCall,
|
||||
ScrollCanvas,
|
||||
} = Behaviors;
|
||||
const {
|
||||
History,
|
||||
@ -124,6 +125,7 @@ const stdLib = {
|
||||
'collapse-expand-combo': CollapseExpandCombo,
|
||||
'collapse-expand-tree': CollapseExpandTree,
|
||||
'click-select': ClickSelect,
|
||||
'scroll-canvas': ScrollCanvas,
|
||||
},
|
||||
plugins: {
|
||||
history: History,
|
||||
|
46
packages/g6/tests/demo/behaviors/drag-canvas.ts
Normal file
46
packages/g6/tests/demo/behaviors/drag-canvas.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import G6 from '../../../src/index';
|
||||
import { TestCaseContext } from '../interface';
|
||||
|
||||
export default (context: TestCaseContext) => {
|
||||
return new G6.Graph({
|
||||
...context,
|
||||
type: 'graph',
|
||||
layout: {
|
||||
type: 'grid',
|
||||
},
|
||||
node: {
|
||||
labelShape: {
|
||||
text: {
|
||||
fields: ['id'],
|
||||
formatter: (model) => model.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
data: {
|
||||
nodes: [
|
||||
{ id: 'node1', data: {} },
|
||||
{ id: 'node2', data: {} },
|
||||
{ id: 'node3', data: {} },
|
||||
{ id: 'node4', data: {} },
|
||||
{ id: 'node5', data: {} },
|
||||
],
|
||||
edges: [
|
||||
{ id: 'edge1', source: 'node1', target: 'node2', data: {} },
|
||||
{ id: 'edge2', source: 'node1', target: 'node3', data: {} },
|
||||
{ id: 'edge3', source: 'node1', target: 'node4', data: {} },
|
||||
{ id: 'edge4', source: 'node2', target: 'node3', data: {} },
|
||||
{ id: 'edge5', source: 'node3', target: 'node4', data: {} },
|
||||
{ id: 'edge6', source: 'node4', target: 'node5', data: {} },
|
||||
],
|
||||
},
|
||||
modes: {
|
||||
default: [
|
||||
{
|
||||
type: 'drag-canvas',
|
||||
enableOptimize: true,
|
||||
// scalableRange: 0.5,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
};
|
49
packages/g6/tests/demo/behaviors/scroll-canvas.ts
Normal file
49
packages/g6/tests/demo/behaviors/scroll-canvas.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import G6 from '../../../src/index';
|
||||
import { TestCaseContext } from '../interface';
|
||||
|
||||
export default (context: TestCaseContext) => {
|
||||
return new G6.Graph({
|
||||
...context,
|
||||
type: 'graph',
|
||||
layout: {
|
||||
type: 'grid',
|
||||
},
|
||||
node: {
|
||||
labelShape: {
|
||||
text: {
|
||||
fields: ['id'],
|
||||
formatter: (model) => model.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
data: {
|
||||
nodes: [
|
||||
{ id: 'node1', data: {} },
|
||||
{ id: 'node2', data: {} },
|
||||
{ id: 'node3', data: {} },
|
||||
{ id: 'node4', data: {} },
|
||||
{ id: 'node5', data: {} },
|
||||
],
|
||||
edges: [
|
||||
{ id: 'edge1', source: 'node1', target: 'node2', data: {} },
|
||||
{ id: 'edge2', source: 'node1', target: 'node3', data: {} },
|
||||
{ id: 'edge3', source: 'node1', target: 'node4', data: {} },
|
||||
{ id: 'edge4', source: 'node2', target: 'node3', data: {} },
|
||||
{ id: 'edge5', source: 'node3', target: 'node4', data: {} },
|
||||
{ id: 'edge6', source: 'node4', target: 'node5', data: {} },
|
||||
],
|
||||
},
|
||||
modes: {
|
||||
default: [
|
||||
{
|
||||
type: 'scroll-canvas',
|
||||
enableOptimize: true,
|
||||
zoomRatio: 0.2,
|
||||
// scalableRange: 0.5,
|
||||
// direction: 'y',
|
||||
// optimizeZoom: 0.5,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
};
|
@ -4,6 +4,8 @@ import animations_node_build_in from './animations/node-build-in';
|
||||
import arrow from './item/edge/arrow';
|
||||
import behaviors_activateRelations from './behaviors/activate-relations';
|
||||
import behaviors_shortcuts_call from './behaviors/shortcuts-call';
|
||||
import behaviors_dragCanvas from './behaviors/drag-canvas';
|
||||
import behaviors_scrollCanvas from './behaviors/scroll-canvas';
|
||||
import behaviors_brush_select from './behaviors/brush-select';
|
||||
import behaviors_click_select from './behaviors/click-select';
|
||||
import behaviors_collapse_expand_tree from './behaviors/collapse-expand-tree';
|
||||
@ -68,6 +70,8 @@ export {
|
||||
animations_node_build_in,
|
||||
arrow,
|
||||
behaviors_activateRelations,
|
||||
behaviors_dragCanvas,
|
||||
behaviors_scrollCanvas,
|
||||
behaviors_shortcuts_call,
|
||||
behaviors_brush_select,
|
||||
behaviors_click_select,
|
||||
|
@ -0,0 +1,99 @@
|
||||
import { resetEntityCounter } from '@antv/g';
|
||||
import scrollCanvas from '../demo/behaviors/scroll-canvas';
|
||||
import './utils/useSnapshotMatchers';
|
||||
import { createContext, triggerEvent } from './utils';
|
||||
|
||||
function sleep(time: number) {
|
||||
return new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
resolve(null)
|
||||
}, time)
|
||||
})
|
||||
}
|
||||
describe('Scroll canvas behavior', () => {
|
||||
beforeEach(() => {
|
||||
/**
|
||||
* SVG Snapshot testing will generate a unique id for each element.
|
||||
* Reset to 0 to keep snapshot consistent.
|
||||
*/
|
||||
resetEntityCounter();
|
||||
});
|
||||
|
||||
it('should be rendered correctly with Canvas2D', (done) => {
|
||||
const dir = `${__dirname}/snapshots/canvas`;
|
||||
const { backgroundCanvas, canvas, transientCanvas, container } =
|
||||
createContext('canvas', 500, 500);
|
||||
|
||||
const graph = scrollCanvas({
|
||||
container,
|
||||
backgroundCanvas,
|
||||
canvas,
|
||||
transientCanvas,
|
||||
width: 500,
|
||||
height: 500,
|
||||
});
|
||||
|
||||
graph.on('afterlayout', async () => {
|
||||
await expect(canvas).toMatchCanvasSnapshot(
|
||||
dir,
|
||||
'behaviors-scroll-canvas',
|
||||
);
|
||||
|
||||
graph.emit('wheel', {
|
||||
deltaX: 50,
|
||||
deltaY: 50,
|
||||
});
|
||||
await sleep(2000)
|
||||
await expect(canvas).toMatchCanvasSnapshot(dir, 'behaviors-scroll-canvas-wheel');
|
||||
|
||||
graph.emit('wheel', {
|
||||
client: { x: 50, y: 50 },
|
||||
ctrlKey: true
|
||||
});
|
||||
|
||||
await sleep(2000)
|
||||
await expect(canvas).toMatchCanvasSnapshot(dir, 'behaviors-scroll-canvas-zoom');
|
||||
|
||||
graph.destroy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should be rendered correctly with SVG', (done) => {
|
||||
const dir = `${__dirname}/snapshots/svg`;
|
||||
const { backgroundCanvas, canvas, transientCanvas, container } =
|
||||
createContext('svg', 500, 500);
|
||||
|
||||
const graph = scrollCanvas({
|
||||
container,
|
||||
backgroundCanvas,
|
||||
canvas,
|
||||
transientCanvas,
|
||||
width: 500,
|
||||
height: 500,
|
||||
});
|
||||
|
||||
graph.on('afterlayout', async () => {
|
||||
await expect(canvas).toMatchSVGSnapshot(dir, 'behaviors-scroll-canvas');
|
||||
|
||||
graph.emit('wheel', {
|
||||
deltaX: 50,
|
||||
deltaY: 50,
|
||||
});
|
||||
|
||||
await sleep(2000)
|
||||
await expect(canvas).toMatchSVGSnapshot(dir, 'behaviors-scroll-canvas-wheel');
|
||||
|
||||
graph.emit('wheel', {
|
||||
client: { x: 50, y: 50 },
|
||||
ctrlKey: true
|
||||
});
|
||||
|
||||
await sleep(2000)
|
||||
await expect(canvas).toMatchSVGSnapshot(dir, 'behaviors-scroll-canvas-zoom');
|
||||
|
||||
graph.destroy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
Binary file not shown.
After Width: | Height: | Size: 7.4 KiB |
Binary file not shown.
After Width: | Height: | Size: 6.8 KiB |
Binary file not shown.
After Width: | Height: | Size: 5.2 KiB |
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 5.1 KiB |
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 5.2 KiB |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="500" height="500" style="background: transparent; position: fixed; outline: none;" color-interpolation-filters="sRGB" tabindex="1"><defs/><g transform="matrix(1,0,0,1,0,0)"><g fill="none" transform="matrix(1,0,0,1,0,0)"><g fill="none" stroke="transparent" stroke-width="01"/><g fill="none" transform="matrix(1,0,0,1,0,0)"/><g fill="none" transform="matrix(1,0,0,1,0,0)"><g fill="none" transform="matrix(1,0,0,1,0,0)"><g transform="matrix(1,0,0,1,138.312805,92.208534)"><line fill="none" x1="0" y1="0" x2="223.374390581189" y2="148.91626038745935" stroke-width="1" stroke="rgba(153,173,209,1)"/><line fill="none" x1="0" y1="0" x2="0" y2="0" stroke-width="3" stroke="transparent"/></g></g><g fill="none" transform="matrix(1,0,0,1,0,0)"><g transform="matrix(1,0,0,1,141,83.333336)"><line fill="none" x1="0" y1="0" x2="218" y2="0" stroke-width="1" stroke="rgba(153,173,209,1)"/><line fill="none" x1="0" y1="0" x2="0" y2="0" stroke-width="3" stroke="transparent"/></g></g><g fill="none" transform="matrix(1,0,0,1,0,0)"><g transform="matrix(1,0,0,1,125,99.333336)"><line fill="none" x1="0" y1="0" x2="0" y2="134.66666666666669" stroke-width="1" stroke="rgba(153,173,209,1)"/><line fill="none" x1="0" y1="0" x2="0" y2="0" stroke-width="3" stroke="transparent"/></g></g><g fill="none" transform="matrix(1,0,0,1,0,0)"><g transform="matrix(1,0,0,1,375,99.333336)"><line fill="none" x1="0" y1="134.66666666666669" x2="0" y2="0" stroke-width="1" stroke="rgba(153,173,209,1)"/><line fill="none" x1="0" y1="0" x2="0" y2="0" stroke-width="3" stroke="transparent"/></g></g><g fill="none" transform="matrix(1,0,0,1,0,0)"><g transform="matrix(1,0,0,1,138.312805,92.208534)"><line fill="none" x1="223.374390581189" y1="0" x2="0" y2="148.91626038745935" stroke-width="1" stroke="rgba(153,173,209,1)"/><line fill="none" x1="0" y1="0" x2="0" y2="0" stroke-width="3" stroke="transparent"/></g></g><g fill="none" transform="matrix(1,0,0,1,0,0)"><g transform="matrix(1,0,0,1,125,266)"><line fill="none" x1="0" y1="0" x2="0" y2="134.66666666666663" stroke-width="1" stroke="rgba(153,173,209,1)"/><line fill="none" x1="0" y1="0" x2="0" y2="0" stroke-width="3" stroke="transparent"/></g></g></g><g fill="none" transform="matrix(1,0,0,1,0,0)"><g fill="none" transform="matrix(1,0,0,1,125,83.333336)"><g transform="matrix(1,0,0,1,0,0)"><circle fill="rgba(34,126,255,1)" transform="translate(-16,-16)" cx="16" cy="16" r="16" stroke-width="0"/></g><g transform="matrix(1,0,0,1,0,18)"><text fill="rgba(0,0,0,1)" dominant-baseline="central" paint-order="stroke" dx="0" dy="7.5px" text-anchor="middle" font-size="12" font-family="sans-serif" font-weight="normal" font-variant="normal" font-style="normal" stroke-width="0">node1</text></g></g><g fill="none" transform="matrix(1,0,0,1,375,250)"><g transform="matrix(1,0,0,1,0,0)"><circle fill="rgba(34,126,255,1)" transform="translate(-16,-16)" cx="16" cy="16" r="16" stroke-width="0"/></g><g transform="matrix(1,0,0,1,0,18)"><text fill="rgba(0,0,0,1)" dominant-baseline="central" paint-order="stroke" dx="0" dy="7.5px" text-anchor="middle" font-size="12" font-family="sans-serif" font-weight="normal" font-variant="normal" font-style="normal" stroke-width="0">node2</text></g></g><g fill="none" transform="matrix(1,0,0,1,375,83.333336)"><g transform="matrix(1,0,0,1,0,0)"><circle fill="rgba(34,126,255,1)" transform="translate(-16,-16)" cx="16" cy="16" r="16" stroke-width="0"/></g><g transform="matrix(1,0,0,1,0,18)"><text fill="rgba(0,0,0,1)" dominant-baseline="central" paint-order="stroke" dx="0" dy="7.5px" text-anchor="middle" font-size="12" font-family="sans-serif" font-weight="normal" font-variant="normal" font-style="normal" stroke-width="0">node3</text></g></g><g fill="none" transform="matrix(1,0,0,1,125,250)"><g transform="matrix(1,0,0,1,0,0)"><circle fill="rgba(34,126,255,1)" transform="translate(-16,-16)" cx="16" cy="16" r="16" stroke-width="0"/></g><g transform="matrix(1,0,0,1,0,18)"><text fill="rgba(0,0,0,1)" dominant-baseline="central" paint-order="stroke" dx="0" dy="7.5px" text-anchor="middle" font-size="12" font-family="sans-serif" font-weight="normal" font-variant="normal" font-style="normal" stroke-width="0">node4</text></g></g><g fill="none" transform="matrix(1,0,0,1,125,416.666656)"><g transform="matrix(1,0,0,1,0,0)"><circle fill="rgba(34,126,255,1)" transform="translate(-16,-16)" cx="16" cy="16" r="16" stroke-width="0"/></g><g transform="matrix(1,0,0,1,0,18)"><text fill="rgba(0,0,0,1)" dominant-baseline="central" paint-order="stroke" dx="0" dy="7.5px" text-anchor="middle" font-size="12" font-family="sans-serif" font-weight="normal" font-variant="normal" font-style="normal" stroke-width="0">node5</text></g></g></g></g></g></svg>
|
After Width: | Height: | Size: 4.6 KiB |
Loading…
Reference in New Issue
Block a user