feat(runtime): add element controller (#5393)

* feat(themes): add theme plugin type definition

* feat(palettes): add palette utils and plugin type definition

* feat(utils): add computeElementCallbackStyle util

* refactor(spec): edge data style support config sourcePort and targetPort

* refactor(animation): adjust executor to adapt undefined animation

* test(spec): fix spec animation test case

* feat(palettes): add built-in palettes

* refactor(utils): adjust palette default logic

* feat(theme): add built in theme

* feat(runtime): add element controller

* refactor(spec): rename port to anchor

* test(registry): update registry test case

* refactor(palettes): remove built in palettes to canstants

* refactor(runtime): data controler remove event emit and provide getChanges API

* refactor(registry): register built-in nodes and edges

* refactor(runtime): adapt data controller changes, store animation result

* refactor(runtime): element style callback returns index and element data extractly

* fix(animation): remove parseAnimation to avoid circular dependencies

* test: update test case

* refactor: adjust demo env

* refactor(animation): executor support specific modifiedStyle, and provide default style value infer

* refactor(themes): update built-in themes

* refactor(utils): update createAnimationsProxy to avoid sync onframe and onfinish to all instances

* refactor(runtime): refactor cavas init function

* refactor(spec): support to disable animation

* test: update test spec

* test: update test case

* refactor(runtime): update element controller and integration cases

* chore: update editor config

* refactor(runtime): update render logic and fix issue that data states change
This commit is contained in:
Aaron 2024-02-02 17:34:33 +08:00 committed by GitHub
parent 2b780f489a
commit c763217197
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
68 changed files with 4396 additions and 284 deletions

View File

@ -1,7 +1,10 @@
{
"cSpell.words": [
"afterrender",
"beforerender",
"Fruchterman",
"gforce",
"graphlib"
"graphlib",
"onframe"
]
}

View File

@ -0,0 +1,98 @@
import type { G6Spec } from '../../../src';
import { DataController } from '../../../src/runtime/data';
import { ElementController } from '../../../src/runtime/element';
import type { RuntimeContext } from '../../../src/runtime/types';
import { Graph } from '../../mock';
import type { AnimationTestCase } from '../types';
const createContext = (canvas: any, options: G6Spec): RuntimeContext => {
const dataController = new DataController();
dataController.setData(options.data || {});
return {
canvas,
graph: new Graph() as any,
options,
dataController,
};
};
export const controllerElementState: AnimationTestCase = async (context) => {
const { canvas } = context;
const options: G6Spec = {
data: {
nodes: [
{ id: 'node-1', style: { cx: 50, cy: 50, states: ['active', 'selected'] } },
{ id: 'node-2', style: { cx: 200, cy: 50 } },
{ id: 'node-3', style: { cx: 125, cy: 150, states: ['active'] } },
],
edges: [
{ source: 'node-1', target: 'node-2', style: { states: ['active'] } },
{ source: 'node-2', target: 'node-3' },
{ source: 'node-3', target: 'node-1' },
],
},
theme: 'light',
node: {
style: {
lineWidth: 1,
r: 10,
},
state: {
active: {
lineWidth: 2,
},
selected: {
fill: 'pink',
},
},
animation: {
update: [{ fields: ['lineWidth', 'fill'] }],
},
},
edge: {
style: {
lineWidth: 1,
},
state: {
active: {
lineWidth: 2,
stroke: 'pink',
},
},
animation: {
update: [
{
fields: ['lineWidth', 'stroke'],
},
],
},
},
};
const elementContext = createContext(canvas, options);
const elementController = new ElementController(elementContext);
const renderResult = await elementController.render(elementContext);
await renderResult?.finished;
elementContext.dataController.updateData({
nodes: [
{ id: 'node-1', style: { states: [] } },
{ id: 'node-2', style: { states: ['active'] } },
{ id: 'node-3', style: { states: ['selected'] } },
],
edges: [
{ source: 'node-1', target: 'node-2', style: { states: [] } },
{ source: 'node-2', target: 'node-3', style: { states: ['active'] } },
],
});
const result = await elementController.render(elementContext);
return result;
};
controllerElementState.times = [0, 1000];

View File

@ -0,0 +1,69 @@
import type { G6Spec } from '../../../src';
import { DataController } from '../../../src/runtime/data';
import { ElementController } from '../../../src/runtime/element';
import type { RuntimeContext } from '../../../src/runtime/types';
import { Graph } from '../../mock';
import type { AnimationTestCase } from '../types';
const createContext = (canvas: any, options: G6Spec): RuntimeContext => {
const dataController = new DataController();
dataController.setData(options.data || {});
return {
canvas,
graph: new Graph() as any,
options,
dataController,
};
};
export const controllerElement: AnimationTestCase = async (context) => {
const { canvas } = context;
const options: G6Spec = {
data: {
nodes: [
{ id: 'node-1', style: { cx: 50, cy: 50 } },
{ id: 'node-2', style: { cx: 200, cy: 50 } },
{ id: 'node-3', style: { cx: 125, cy: 150 } },
],
edges: [
{ source: 'node-1', target: 'node-2' },
{ source: 'node-2', target: 'node-3' },
{ source: 'node-3', target: 'node-1' },
],
},
theme: 'light',
node: {
style: {
r: 10,
},
},
edge: {
style: {},
},
};
const elementContext = createContext(canvas, options);
const elementController = new ElementController(elementContext);
const renderResult = await elementController.render(elementContext);
await renderResult?.finished;
elementContext.dataController.addNodeData([
{ id: 'node-4', style: { cx: 50, cy: 200, stroke: 'orange' } },
{ id: 'node-5', style: { cx: 75, cy: 150, stroke: 'purple' } },
{ id: 'node-6', style: { cx: 200, cy: 100, stroke: 'cyan' } },
]);
elementContext.dataController.removeNodeData(['node-1']);
elementContext.dataController.updateNodeData([{ id: 'node-2', style: { cx: 200, cy: 200, stroke: 'green' } }]);
const result = await elementController.render(elementContext);
return result;
};
controllerElement.times = [50, 200, 1000];

View File

@ -1,10 +1,10 @@
import '../../../src/preset';
export * from './animation-breathe';
export * from './animation-fade-in';
export * from './animation-translate';
export * from './animation-wave';
export * from './animation-zoom-in';
export * from './controller-element';
export * from './controller-element-state';
export * from './edge-cubic';
export * from './edge-line';
export * from './edge-quadratic';

View File

@ -0,0 +1,58 @@
import type { G6Spec } from '../../../src';
import { DataController } from '../../../src/runtime/data';
import { ElementController } from '../../../src/runtime/element';
import type { RuntimeContext } from '../../../src/runtime/types';
import { sleep } from '../../integration/utils/sleep';
import { Graph } from '../../mock';
import type { StaticTestCase } from '../types';
const createContext = (canvas: any, options: G6Spec): RuntimeContext => {
const dataController = new DataController();
dataController.setData(options.data || {});
return {
canvas,
graph: new Graph() as any,
options,
dataController,
};
};
export const controllerElement: StaticTestCase = async (context) => {
const { canvas } = context;
const options: G6Spec = {
data: {
nodes: [
{ id: 'node-1', style: { cx: 50, cy: 50 } },
{ id: 'node-2', style: { cx: 200, cy: 50 } },
{ id: 'node-3', style: { cx: 125, cy: 150 } },
],
edges: [
{ source: 'node-1', target: 'node-2' },
{ source: 'node-2', target: 'node-3' },
{ source: 'node-3', target: 'node-1' },
],
},
theme: 'light',
node: {
style: {
r: 10,
},
animation: false,
},
edge: {
style: {},
animation: false,
},
};
const elementContext = createContext(canvas, options);
const elementController = new ElementController(elementContext);
const result = await elementController.render(elementContext);
await result?.finished;
await sleep(100);
};

View File

@ -84,8 +84,6 @@ export const edgeLine: StaticTestCase = async (context) => {
},
});
await canvas.init();
canvas.appendChild(line1);
canvas.appendChild(line2);
canvas.appendChild(line3);

View File

@ -73,8 +73,6 @@ export const edgeQuadratic: StaticTestCase = async (context) => {
},
});
await canvas.init();
canvas.appendChild(quadratic1);
canvas.appendChild(quadratic2);
canvas.appendChild(quadratic3);

View File

@ -1,3 +1,4 @@
export * from './controller-element';
export * from './edge-cubic';
export * from './edge-cubic-horizontal';
export * from './edge-cubic-vertical';

View File

@ -23,8 +23,6 @@ export const layeredCanvas: StaticTestCase = async (context) => {
},
});
await canvas.init();
canvas.appendChild(circle);
canvas.appendChild(rect);

View File

@ -65,8 +65,6 @@ export const nodeCircle: StaticTestCase = async (context) => {
},
});
await canvas.init();
canvas.appendChild(c1);
canvas.appendChild(c2);
canvas.appendChild(c3);

View File

@ -53,8 +53,6 @@ export const shapeBadge: StaticTestCase = async (context) => {
},
});
await canvas.init();
canvas.appendChild(badge1);
canvas.appendChild(badge2);
canvas.appendChild(badge3);

View File

@ -26,8 +26,6 @@ export const shapeIcon: StaticTestCase = async (context) => {
},
});
await canvas.init();
canvas.appendChild(i1);
canvas.appendChild(i2);
};

View File

@ -59,8 +59,6 @@ export const shapeLabel: StaticTestCase = async (context) => {
},
});
await canvas.init();
canvas.appendChild(label1);
canvas.appendChild(label2);
canvas.appendChild(label3);

View File

@ -1,32 +1,119 @@
<!doctype html>
<html lang="en">
<html lang="en" data-theme="light">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>G6: Preview</title>
<style>
body {
margin: 0;
font-size: 14px;
color: var(--text-color);
background-color: var(--background-color);
background-image: linear-gradient(var(--stroke-color) 1px, transparent 1px),
linear-gradient(90deg, var(--stroke-color) 1px, transparent 1px);
background-size: 25px 25px;
transition: all 0.5s;
}
#app {
display: flex;
flex-direction: row-reverse;
justify-content: flex-end;
}
#panel {
background-color: var(--background-color2);
border-radius: 4px;
color: var(--text-color);
display: flex;
flex-direction: column;
gap: 5px;
left: 500px;
padding: 10px;
position: absolute;
top: 20px;
transition:
background-color 0.5s,
color 0.5s;
user-select: none;
z-index: 1;
}
#container {
width: 500px;
height: 500px;
background-image: linear-gradient(rgba(0, 0, 0, 0.1) 1px, transparent 1px),
linear-gradient(90deg, rgba(0, 0, 0, 0.1) 1px, transparent 1px);
background-size: 25px 25px;
}
[data-theme='light'] {
--text-color: #000;
--background-color: #fff;
--background-color2: #ddd;
--stroke-color: #0001;
}
[data-theme='dark'] {
--text-color: #fff;
--background-color: #000;
--background-color2: #333;
--stroke-color: #333;
}
</style>
</head>
<body style="font-family: Arial, Helvetica, sans-serif">
<div id="app">
<label for="demo-select">Cases: </label>
<select id="demo-select" style="cursor: pointer; width: 180px"></select>
<label for="renderer-select">Renderer: </label>
<select id="renderer-select" style="cursor: pointer">
<option value="default" selected>Default</option>
<option value="canvas" selected>Canvas</option>
<option value="svg">SVG</option>
<option value="webgl">WebGL</option>
</select>
<div id="panel">
<label for="demo-select">Cases: </label>
<select id="demo-select" style="cursor: pointer; width: 180px"></select>
<label for="renderer-select">Renderer: </label>
<select id="renderer-select" style="cursor: pointer">
<option value="default" selected>Default</option>
<option value="canvas" selected>Canvas</option>
<option value="svg">SVG</option>
<option value="webgl">WebGL</option>
</select>
<label for="theme">Theme: </label>
<button onclick="toggleTheme()">Toggle</button>
</div>
</div>
<script>
function toggleTheme() {
const theme = document.documentElement.getAttribute('data-theme');
if (theme === 'light') {
document.documentElement.setAttribute('data-theme', 'dark');
} else {
document.documentElement.setAttribute('data-theme', 'light');
}
}
(function draggable() {
const draggableElement = document.getElementById('panel');
let offsetX,
offsetY,
isDragging = false;
draggableElement.addEventListener('mousedown', function (e) {
isDragging = true;
offsetX = e.clientX - draggableElement.getBoundingClientRect().left;
offsetY = e.clientY - draggableElement.getBoundingClientRect().top;
});
document.addEventListener('mousemove', function (e) {
if (isDragging) {
const x = e.clientX - offsetX;
const y = e.clientY - offsetY;
draggableElement.style.left = `${x}px`;
draggableElement.style.top = `${y}px`;
}
});
document.addEventListener('mouseup', function () {
isDragging = false;
});
})();
</script>
<script type="module" src="./main.ts"></script>
</body>
</html>

View File

@ -16,6 +16,7 @@ describe('static', () => {
const { times = [], preprocess, postprocess } = testCase;
await preprocess?.();
await canvas.init();
const animationResult = await testCase({ canvas });
if (!animationResult) throw new Error('animation result should not be null');

View File

@ -0,0 +1,376 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="500"
height="500"
style="background: transparent; position: absolute; outline: none;"
color-interpolation-filters="sRGB"
tabindex="1"
>
<defs />
<g id="g-svg-camera" transform="matrix(1,0,0,1,0,0)">
<g id="g-root" fill="none" transform="matrix(1,0,0,1,0,0)">
<g id="g-svg-7" fill="none" transform="matrix(1,0,0,1,0,0)" />
<g id="g-svg-6" fill="none" transform="matrix(1,0,0,1,0,0)">
<g
id="g-svg-32"
fill="none"
marker-start="false"
marker-end="false"
transform="matrix(1,0,0,1,0,0)"
>
<g transform="matrix(1,0,0,1,50,50)">
<line
id="key"
fill="none"
x1="0"
y1="0"
x2="150"
y2="0"
stroke-width="1"
stroke="rgba(139,155,175,1)"
opacity="0"
/>
</g>
<g id="label" fill="none" transform="matrix(1,0,0,1,129,44)">
<g transform="matrix(1,0,0,1,-5,-5)">
<path
id="background"
fill="none"
d="M 0,0 l 10,0 l 0,10 l-10 0 z"
width="10"
height="10"
opacity="0"
/>
</g>
<g transform="matrix(1,0,0,1,0,0)">
<text
id="text"
fill="rgba(0,0,0,1)"
dominant-baseline="text-after-edge"
paint-order="stroke"
text-anchor="middle"
opacity="0"
/>
</g>
</g>
</g>
<g
id="g-svg-37"
fill="none"
marker-start="false"
marker-end="false"
transform="matrix(1,0,0,1,0,0)"
>
<g transform="matrix(1,0,0,1,125,150)">
<line
id="key"
fill="none"
x1="75"
y1="50"
x2="0"
y2="0"
stroke-width="1"
stroke="rgba(139,155,175,1)"
/>
</g>
<g
id="label"
fill="none"
transform="matrix(0.832050,0.554700,-0.554700,0.832050,162.500000,167.788895)"
>
<g transform="matrix(1,0,0,1,-5,-5)">
<path
id="background"
fill="none"
d="M 0,0 l 10,0 l 0,10 l-10 0 z"
width="10"
height="10"
/>
</g>
<g transform="matrix(1,0,0,1,0,0)">
<text
id="text"
fill="rgba(0,0,0,1)"
dominant-baseline="text-after-edge"
paint-order="stroke"
text-anchor="middle"
/>
</g>
</g>
</g>
</g>
<g id="g-svg-5" fill="none" transform="matrix(1,0,0,1,0,0)">
<g id="g-svg-16" fill="none" r="10" transform="matrix(1,0,0,1,0,0)">
<g transform="matrix(1,0,0,1,200,200)">
<circle
id="key"
fill="rgba(248,248,248,1)"
transform="translate(-10,-10)"
cx="10"
cy="10"
stroke="rgba(0,128,0,1)"
r="10"
/>
</g>
<g id="label" fill="none" transform="matrix(1,0,0,1,200,200)">
<g transform="matrix(1,0,0,1,-5,-5)">
<path
id="background"
fill="none"
d="M 0,0 l 10,0 l 0,10 l-10 0 z"
width="10"
height="10"
/>
</g>
<g transform="matrix(1,0,0,1,0,0)">
<text
id="text"
fill="rgba(0,0,0,1)"
dominant-baseline="central"
paint-order="stroke"
text-anchor="middle"
/>
</g>
</g>
<g transform="matrix(1,0,0,1,200,200)">
<circle
id="halo"
fill="rgba(248,248,248,1)"
cx="0"
cy="0"
stroke="rgba(0,128,0,1)"
r="0"
/>
</g>
<g id="icon" fill="none" transform="matrix(1,0,0,1,200,200)">
<g transform="matrix(1,0,0,1,0,0)">
<text
id="icon"
fill="rgba(0,0,0,1)"
dominant-baseline="central"
paint-order="stroke"
text-anchor="middle"
/>
</g>
</g>
</g>
<g id="g-svg-24" fill="none" r="10" transform="matrix(1,0,0,1,0,0)">
<g transform="matrix(1,0,0,1,125,150)">
<circle
id="key"
fill="rgba(248,248,248,1)"
transform="translate(-10,-10)"
cx="10"
cy="10"
stroke="rgba(139,155,175,1)"
r="10"
/>
</g>
<g id="label" fill="none" transform="matrix(1,0,0,1,125,150)">
<g transform="matrix(1,0,0,1,-5,-5)">
<path
id="background"
fill="none"
d="M 0,0 l 10,0 l 0,10 l-10 0 z"
width="10"
height="10"
/>
</g>
<g transform="matrix(1,0,0,1,0,0)">
<text
id="text"
fill="rgba(0,0,0,1)"
dominant-baseline="central"
paint-order="stroke"
text-anchor="middle"
/>
</g>
</g>
<g transform="matrix(1,0,0,1,125,150)">
<circle
id="halo"
fill="rgba(248,248,248,1)"
cx="0"
cy="0"
stroke="rgba(139,155,175,1)"
r="0"
/>
</g>
<g id="icon" fill="none" transform="matrix(1,0,0,1,125,150)">
<g transform="matrix(1,0,0,1,0,0)">
<text
id="icon"
fill="rgba(0,0,0,1)"
dominant-baseline="central"
paint-order="stroke"
text-anchor="middle"
/>
</g>
</g>
</g>
<g id="g-svg-47" fill="none" r="10" transform="matrix(1,0,0,1,0,0)">
<g transform="matrix(1,0,0,1,50,200)">
<circle
id="key"
fill="rgba(248,248,248,1)"
transform="translate(-10,-10)"
cx="10"
cy="10"
stroke="rgba(255,165,0,1)"
r="10"
/>
</g>
<g id="label" fill="none" transform="matrix(1,0,0,1,50,200)">
<g transform="matrix(1,0,0,1,-5,-5)">
<path
id="background"
fill="none"
d="M 0,0 l 10,0 l 0,10 l-10 0 z"
width="10"
height="10"
/>
</g>
<g transform="matrix(1,0,0,1,0,0)">
<text
id="text"
fill="rgba(0,0,0,1)"
dominant-baseline="central"
paint-order="stroke"
text-anchor="middle"
/>
</g>
</g>
<g transform="matrix(1,0,0,1,50,200)">
<circle
id="halo"
fill="rgba(248,248,248,1)"
cx="0"
cy="0"
stroke="rgba(255,165,0,1)"
r="0"
/>
</g>
<g id="icon" fill="none" transform="matrix(1,0,0,1,50,200)">
<g transform="matrix(1,0,0,1,0,0)">
<text
id="icon"
fill="rgba(0,0,0,1)"
dominant-baseline="central"
paint-order="stroke"
text-anchor="middle"
/>
</g>
</g>
</g>
<g id="g-svg-55" fill="none" r="10" transform="matrix(1,0,0,1,0,0)">
<g transform="matrix(1,0,0,1,75,150)">
<circle
id="key"
fill="rgba(248,248,248,1)"
transform="translate(-10,-10)"
cx="10"
cy="10"
stroke="rgba(128,0,128,1)"
r="10"
/>
</g>
<g id="label" fill="none" transform="matrix(1,0,0,1,75,150)">
<g transform="matrix(1,0,0,1,-5,-5)">
<path
id="background"
fill="none"
d="M 0,0 l 10,0 l 0,10 l-10 0 z"
width="10"
height="10"
/>
</g>
<g transform="matrix(1,0,0,1,0,0)">
<text
id="text"
fill="rgba(0,0,0,1)"
dominant-baseline="central"
paint-order="stroke"
text-anchor="middle"
/>
</g>
</g>
<g transform="matrix(1,0,0,1,75,150)">
<circle
id="halo"
fill="rgba(248,248,248,1)"
cx="0"
cy="0"
stroke="rgba(128,0,128,1)"
r="0"
/>
</g>
<g id="icon" fill="none" transform="matrix(1,0,0,1,75,150)">
<g transform="matrix(1,0,0,1,0,0)">
<text
id="icon"
fill="rgba(0,0,0,1)"
dominant-baseline="central"
paint-order="stroke"
text-anchor="middle"
/>
</g>
</g>
</g>
<g id="g-svg-63" fill="none" r="10" transform="matrix(1,0,0,1,0,0)">
<g transform="matrix(1,0,0,1,200,100)">
<circle
id="key"
fill="rgba(248,248,248,1)"
transform="translate(-10,-10)"
cx="10"
cy="10"
stroke="rgba(0,255,255,1)"
r="10"
/>
</g>
<g id="label" fill="none" transform="matrix(1,0,0,1,200,100)">
<g transform="matrix(1,0,0,1,-5,-5)">
<path
id="background"
fill="none"
d="M 0,0 l 10,0 l 0,10 l-10 0 z"
width="10"
height="10"
/>
</g>
<g transform="matrix(1,0,0,1,0,0)">
<text
id="text"
fill="rgba(0,0,0,1)"
dominant-baseline="central"
paint-order="stroke"
text-anchor="middle"
/>
</g>
</g>
<g transform="matrix(1,0,0,1,200,100)">
<circle
id="halo"
fill="rgba(248,248,248,1)"
cx="0"
cy="0"
stroke="rgba(0,255,255,1)"
r="0"
/>
</g>
<g id="icon" fill="none" transform="matrix(1,0,0,1,200,100)">
<g transform="matrix(1,0,0,1,0,0)">
<text
id="icon"
fill="rgba(0,0,0,1)"
dominant-baseline="central"
paint-order="stroke"
text-anchor="middle"
/>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -0,0 +1,497 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="500"
height="500"
style="background: transparent; position: absolute; outline: none;"
color-interpolation-filters="sRGB"
tabindex="1"
>
<defs />
<g id="g-svg-camera" transform="matrix(1,0,0,1,0,0)">
<g id="g-root" fill="none" transform="matrix(1,0,0,1,0,0)">
<g id="g-svg-7" fill="none" transform="matrix(1,0,0,1,0,0)" />
<g id="g-svg-6" fill="none" transform="matrix(1,0,0,1,0,0)">
<g
id="g-svg-32"
fill="none"
marker-start="false"
marker-end="false"
transform="matrix(1,0,0,1,0,0)"
>
<g transform="matrix(1,0,0,1,50,50)">
<line
id="key"
fill="none"
x1="0"
y1="0"
x2="150"
y2="0"
stroke-width="1"
stroke="rgba(139,155,175,1)"
opacity="0.37293273049835485"
/>
</g>
<g id="label" fill="none" transform="matrix(1,0,0,1,129,44)">
<g transform="matrix(1,0,0,1,-5,-5)">
<path
id="background"
fill="none"
d="M 0,0 l 10,0 l 0,10 l-10 0 z"
width="10"
height="10"
opacity="0.37293273049835485"
/>
</g>
<g transform="matrix(1,0,0,1,0,0)">
<text
id="text"
fill="rgba(0,0,0,1)"
dominant-baseline="text-after-edge"
paint-order="stroke"
text-anchor="middle"
opacity="0.37293273049835485"
/>
</g>
</g>
</g>
<g
id="g-svg-37"
fill="none"
marker-start="false"
marker-end="false"
transform="matrix(1,0,0,1,0,0)"
>
<g transform="matrix(1,0,0,1,125,144.060089)">
<line
id="key"
fill="none"
x1="75"
y1="0"
x2="0"
y2="5.939909574753216"
stroke-width="1"
stroke="rgba(139,155,175,1)"
/>
</g>
<g
id="label"
fill="none"
transform="matrix(0.832050,0.554700,-0.554700,0.832050,162.500000,167.788895)"
>
<g transform="matrix(1,0,0,1,-5,-5)">
<path
id="background"
fill="none"
d="M 0,0 l 10,0 l 0,10 l-10 0 z"
width="10"
height="10"
/>
</g>
<g transform="matrix(1,0,0,1,0,0)">
<text
id="text"
fill="rgba(0,0,0,1)"
dominant-baseline="text-after-edge"
paint-order="stroke"
text-anchor="middle"
/>
</g>
</g>
</g>
<g
id="g-svg-42"
fill="none"
marker-start="false"
marker-end="false"
transform="matrix(1,0,0,1,0,0)"
>
<g transform="matrix(1,0,0,1,50,50)">
<line
id="key"
fill="none"
x1="75"
y1="100"
x2="0"
y2="0"
stroke-width="1"
stroke="rgba(139,155,175,1)"
opacity="0.37293273049835485"
/>
</g>
<g
id="label"
fill="none"
transform="matrix(0.600000,0.800000,-0.800000,0.600000,89.900002,93.199997)"
>
<g transform="matrix(1,0,0,1,-5,-5)">
<path
id="background"
fill="none"
d="M 0,0 l 10,0 l 0,10 l-10 0 z"
width="10"
height="10"
opacity="0.37293273049835485"
/>
</g>
<g transform="matrix(1,0,0,1,0,0)">
<text
id="text"
fill="rgba(0,0,0,1)"
dominant-baseline="text-after-edge"
paint-order="stroke"
text-anchor="middle"
opacity="0.37293273049835485"
/>
</g>
</g>
</g>
</g>
<g id="g-svg-5" fill="none" transform="matrix(1,0,0,1,0,0)">
<g id="g-svg-8" fill="none" r="10" transform="matrix(1,0,0,1,0,0)">
<g transform="matrix(1,0,0,1,50,50)">
<circle
id="key"
fill="rgba(248,248,248,1)"
transform="translate(-10,-10)"
cx="10"
cy="10"
stroke="rgba(139,155,175,1)"
r="10"
opacity="0.37293273049835485"
/>
</g>
<g id="label" fill="none" transform="matrix(1,0,0,1,50,50)">
<g transform="matrix(1,0,0,1,-5,-5)">
<path
id="background"
fill="none"
d="M 0,0 l 10,0 l 0,10 l-10 0 z"
width="10"
height="10"
opacity="0.37293273049835485"
/>
</g>
<g transform="matrix(1,0,0,1,0,0)">
<text
id="text"
fill="rgba(0,0,0,1)"
dominant-baseline="central"
paint-order="stroke"
text-anchor="middle"
opacity="0.37293273049835485"
/>
</g>
</g>
<g transform="matrix(1,0,0,1,50,50)">
<circle
id="halo"
fill="rgba(248,248,248,1)"
cx="0"
cy="0"
stroke="rgba(139,155,175,1)"
r="0"
opacity="0.37293273049835485"
/>
</g>
<g id="icon" fill="none" transform="matrix(1,0,0,1,50,50)">
<g transform="matrix(1,0,0,1,0,0)">
<text
id="icon"
fill="rgba(0,0,0,1)"
dominant-baseline="central"
paint-order="stroke"
text-anchor="middle"
opacity="0.37293273049835485"
/>
</g>
</g>
</g>
<g id="g-svg-16" fill="none" r="10" transform="matrix(1,0,0,1,0,0)">
<g transform="matrix(1,0,0,1,200,144.060089)">
<circle
id="key"
fill="rgba(248,248,248,1)"
transform="translate(-10,-10)"
cx="10"
cy="10"
stroke="rgba(0,128,0,1)"
r="10"
/>
</g>
<g id="label" fill="none" transform="matrix(1,0,0,1,200,200)">
<g transform="matrix(1,0,0,1,-5,-5)">
<path
id="background"
fill="none"
d="M 0,0 l 10,0 l 0,10 l-10 0 z"
width="10"
height="10"
/>
</g>
<g transform="matrix(1,0,0,1,0,0)">
<text
id="text"
fill="rgba(0,0,0,1)"
dominant-baseline="central"
paint-order="stroke"
text-anchor="middle"
/>
</g>
</g>
<g transform="matrix(1,0,0,1,200,144.060089)">
<circle
id="halo"
fill="rgba(248,248,248,1)"
cx="0"
cy="0"
stroke="rgba(0,128,0,1)"
r="0"
/>
</g>
<g id="icon" fill="none" transform="matrix(1,0,0,1,200,200)">
<g transform="matrix(1,0,0,1,0,0)">
<text
id="icon"
fill="rgba(0,0,0,1)"
dominant-baseline="central"
paint-order="stroke"
text-anchor="middle"
/>
</g>
</g>
</g>
<g id="g-svg-24" fill="none" r="10" transform="matrix(1,0,0,1,0,0)">
<g transform="matrix(1,0,0,1,125,150)">
<circle
id="key"
fill="rgba(248,248,248,1)"
transform="translate(-10,-10)"
cx="10"
cy="10"
stroke="rgba(139,155,175,1)"
r="10"
/>
</g>
<g id="label" fill="none" transform="matrix(1,0,0,1,125,150)">
<g transform="matrix(1,0,0,1,-5,-5)">
<path
id="background"
fill="none"
d="M 0,0 l 10,0 l 0,10 l-10 0 z"
width="10"
height="10"
/>
</g>
<g transform="matrix(1,0,0,1,0,0)">
<text
id="text"
fill="rgba(0,0,0,1)"
dominant-baseline="central"
paint-order="stroke"
text-anchor="middle"
/>
</g>
</g>
<g transform="matrix(1,0,0,1,125,150)">
<circle
id="halo"
fill="rgba(248,248,248,1)"
cx="0"
cy="0"
stroke="rgba(139,155,175,1)"
r="0"
/>
</g>
<g id="icon" fill="none" transform="matrix(1,0,0,1,125,150)">
<g transform="matrix(1,0,0,1,0,0)">
<text
id="icon"
fill="rgba(0,0,0,1)"
dominant-baseline="central"
paint-order="stroke"
text-anchor="middle"
/>
</g>
</g>
</g>
<g id="g-svg-47" fill="none" r="10" transform="matrix(1,0,0,1,0,0)">
<g transform="matrix(1,0,0,1,50,200)">
<circle
id="key"
fill="rgba(248,248,248,1)"
transform="translate(-10,-10)"
cx="10"
cy="10"
stroke="rgba(255,165,0,1)"
r="10"
opacity="0.6270672695016452"
/>
</g>
<g id="label" fill="none" transform="matrix(1,0,0,1,50,200)">
<g transform="matrix(1,0,0,1,-5,-5)">
<path
id="background"
fill="none"
d="M 0,0 l 10,0 l 0,10 l-10 0 z"
width="10"
height="10"
opacity="0.6270672695016452"
/>
</g>
<g transform="matrix(1,0,0,1,0,0)">
<text
id="text"
fill="rgba(0,0,0,1)"
dominant-baseline="central"
paint-order="stroke"
text-anchor="middle"
opacity="0.6270672695016452"
/>
</g>
</g>
<g transform="matrix(1,0,0,1,50,200)">
<circle
id="halo"
fill="rgba(248,248,248,1)"
cx="0"
cy="0"
stroke="rgba(255,165,0,1)"
r="0"
opacity="0.6270672695016452"
/>
</g>
<g id="icon" fill="none" transform="matrix(1,0,0,1,50,200)">
<g transform="matrix(1,0,0,1,0,0)">
<text
id="icon"
fill="rgba(0,0,0,1)"
dominant-baseline="central"
paint-order="stroke"
text-anchor="middle"
opacity="0.6270672695016452"
/>
</g>
</g>
</g>
<g id="g-svg-55" fill="none" r="10" transform="matrix(1,0,0,1,0,0)">
<g transform="matrix(1,0,0,1,75,150)">
<circle
id="key"
fill="rgba(248,248,248,1)"
transform="translate(-10,-10)"
cx="10"
cy="10"
stroke="rgba(128,0,128,1)"
r="10"
opacity="0.6270672695016452"
/>
</g>
<g id="label" fill="none" transform="matrix(1,0,0,1,75,150)">
<g transform="matrix(1,0,0,1,-5,-5)">
<path
id="background"
fill="none"
d="M 0,0 l 10,0 l 0,10 l-10 0 z"
width="10"
height="10"
opacity="0.6270672695016452"
/>
</g>
<g transform="matrix(1,0,0,1,0,0)">
<text
id="text"
fill="rgba(0,0,0,1)"
dominant-baseline="central"
paint-order="stroke"
text-anchor="middle"
opacity="0.6270672695016452"
/>
</g>
</g>
<g transform="matrix(1,0,0,1,75,150)">
<circle
id="halo"
fill="rgba(248,248,248,1)"
cx="0"
cy="0"
stroke="rgba(128,0,128,1)"
r="0"
opacity="0.6270672695016452"
/>
</g>
<g id="icon" fill="none" transform="matrix(1,0,0,1,75,150)">
<g transform="matrix(1,0,0,1,0,0)">
<text
id="icon"
fill="rgba(0,0,0,1)"
dominant-baseline="central"
paint-order="stroke"
text-anchor="middle"
opacity="0.6270672695016452"
/>
</g>
</g>
</g>
<g id="g-svg-63" fill="none" r="10" transform="matrix(1,0,0,1,0,0)">
<g transform="matrix(1,0,0,1,200,100)">
<circle
id="key"
fill="rgba(248,248,248,1)"
transform="translate(-10,-10)"
cx="10"
cy="10"
stroke="rgba(0,255,255,1)"
r="10"
opacity="0.6270672695016452"
/>
</g>
<g id="label" fill="none" transform="matrix(1,0,0,1,200,100)">
<g transform="matrix(1,0,0,1,-5,-5)">
<path
id="background"
fill="none"
d="M 0,0 l 10,0 l 0,10 l-10 0 z"
width="10"
height="10"
opacity="0.6270672695016452"
/>
</g>
<g transform="matrix(1,0,0,1,0,0)">
<text
id="text"
fill="rgba(0,0,0,1)"
dominant-baseline="central"
paint-order="stroke"
text-anchor="middle"
opacity="0.6270672695016452"
/>
</g>
</g>
<g transform="matrix(1,0,0,1,200,100)">
<circle
id="halo"
fill="rgba(248,248,248,1)"
cx="0"
cy="0"
stroke="rgba(0,255,255,1)"
r="0"
opacity="0.6270672695016452"
/>
</g>
<g id="icon" fill="none" transform="matrix(1,0,0,1,200,100)">
<g transform="matrix(1,0,0,1,0,0)">
<text
id="icon"
fill="rgba(0,0,0,1)"
dominant-baseline="central"
paint-order="stroke"
text-anchor="middle"
opacity="0.6270672695016452"
/>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -0,0 +1,497 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="500"
height="500"
style="background: transparent; position: absolute; outline: none;"
color-interpolation-filters="sRGB"
tabindex="1"
>
<defs />
<g id="g-svg-camera" transform="matrix(1,0,0,1,0,0)">
<g id="g-root" fill="none" transform="matrix(1,0,0,1,0,0)">
<g id="g-svg-7" fill="none" transform="matrix(1,0,0,1,0,0)" />
<g id="g-svg-6" fill="none" transform="matrix(1,0,0,1,0,0)">
<g
id="g-svg-32"
fill="none"
marker-start="false"
marker-end="false"
transform="matrix(1,0,0,1,0,0)"
>
<g transform="matrix(1,0,0,1,50,50)">
<line
id="key"
fill="none"
x1="0"
y1="0"
x2="150"
y2="0"
stroke-width="1"
stroke="rgba(139,155,175,1)"
opacity="0.8255817601983712"
/>
</g>
<g id="label" fill="none" transform="matrix(1,0,0,1,129,44)">
<g transform="matrix(1,0,0,1,-5,-5)">
<path
id="background"
fill="none"
d="M 0,0 l 10,0 l 0,10 l-10 0 z"
width="10"
height="10"
opacity="0.8255817601983712"
/>
</g>
<g transform="matrix(1,0,0,1,0,0)">
<text
id="text"
fill="rgba(0,0,0,1)"
dominant-baseline="text-after-edge"
paint-order="stroke"
text-anchor="middle"
opacity="0.8255817601983712"
/>
</g>
</g>
</g>
<g
id="g-svg-37"
fill="none"
marker-start="false"
marker-end="false"
transform="matrix(1,0,0,1,0,0)"
>
<g transform="matrix(1,0,0,1,125,76.162735)">
<line
id="key"
fill="none"
x1="75"
y1="0"
x2="0"
y2="73.83726402975567"
stroke-width="1"
stroke="rgba(139,155,175,1)"
/>
</g>
<g
id="label"
fill="none"
transform="matrix(0.832050,0.554700,-0.554700,0.832050,162.500000,167.788895)"
>
<g transform="matrix(1,0,0,1,-5,-5)">
<path
id="background"
fill="none"
d="M 0,0 l 10,0 l 0,10 l-10 0 z"
width="10"
height="10"
/>
</g>
<g transform="matrix(1,0,0,1,0,0)">
<text
id="text"
fill="rgba(0,0,0,1)"
dominant-baseline="text-after-edge"
paint-order="stroke"
text-anchor="middle"
/>
</g>
</g>
</g>
<g
id="g-svg-42"
fill="none"
marker-start="false"
marker-end="false"
transform="matrix(1,0,0,1,0,0)"
>
<g transform="matrix(1,0,0,1,50,50)">
<line
id="key"
fill="none"
x1="75"
y1="100"
x2="0"
y2="0"
stroke-width="1"
stroke="rgba(139,155,175,1)"
opacity="0.8255817601983712"
/>
</g>
<g
id="label"
fill="none"
transform="matrix(0.600000,0.800000,-0.800000,0.600000,89.900002,93.199997)"
>
<g transform="matrix(1,0,0,1,-5,-5)">
<path
id="background"
fill="none"
d="M 0,0 l 10,0 l 0,10 l-10 0 z"
width="10"
height="10"
opacity="0.8255817601983712"
/>
</g>
<g transform="matrix(1,0,0,1,0,0)">
<text
id="text"
fill="rgba(0,0,0,1)"
dominant-baseline="text-after-edge"
paint-order="stroke"
text-anchor="middle"
opacity="0.8255817601983712"
/>
</g>
</g>
</g>
</g>
<g id="g-svg-5" fill="none" transform="matrix(1,0,0,1,0,0)">
<g id="g-svg-8" fill="none" r="10" transform="matrix(1,0,0,1,0,0)">
<g transform="matrix(1,0,0,1,50,50)">
<circle
id="key"
fill="rgba(248,248,248,1)"
transform="translate(-10,-10)"
cx="10"
cy="10"
stroke="rgba(139,155,175,1)"
r="10"
opacity="0.8255817601983712"
/>
</g>
<g id="label" fill="none" transform="matrix(1,0,0,1,50,50)">
<g transform="matrix(1,0,0,1,-5,-5)">
<path
id="background"
fill="none"
d="M 0,0 l 10,0 l 0,10 l-10 0 z"
width="10"
height="10"
opacity="0.8255817601983712"
/>
</g>
<g transform="matrix(1,0,0,1,0,0)">
<text
id="text"
fill="rgba(0,0,0,1)"
dominant-baseline="central"
paint-order="stroke"
text-anchor="middle"
opacity="0.8255817601983712"
/>
</g>
</g>
<g transform="matrix(1,0,0,1,50,50)">
<circle
id="halo"
fill="rgba(248,248,248,1)"
cx="0"
cy="0"
stroke="rgba(139,155,175,1)"
r="0"
opacity="0.8255817601983712"
/>
</g>
<g id="icon" fill="none" transform="matrix(1,0,0,1,50,50)">
<g transform="matrix(1,0,0,1,0,0)">
<text
id="icon"
fill="rgba(0,0,0,1)"
dominant-baseline="central"
paint-order="stroke"
text-anchor="middle"
opacity="0.8255817601983712"
/>
</g>
</g>
</g>
<g id="g-svg-16" fill="none" r="10" transform="matrix(1,0,0,1,0,0)">
<g transform="matrix(1,0,0,1,200,76.162735)">
<circle
id="key"
fill="rgba(248,248,248,1)"
transform="translate(-10,-10)"
cx="10"
cy="10"
stroke="rgba(0,128,0,1)"
r="10"
/>
</g>
<g id="label" fill="none" transform="matrix(1,0,0,1,200,200)">
<g transform="matrix(1,0,0,1,-5,-5)">
<path
id="background"
fill="none"
d="M 0,0 l 10,0 l 0,10 l-10 0 z"
width="10"
height="10"
/>
</g>
<g transform="matrix(1,0,0,1,0,0)">
<text
id="text"
fill="rgba(0,0,0,1)"
dominant-baseline="central"
paint-order="stroke"
text-anchor="middle"
/>
</g>
</g>
<g transform="matrix(1,0,0,1,200,76.162735)">
<circle
id="halo"
fill="rgba(248,248,248,1)"
cx="0"
cy="0"
stroke="rgba(0,128,0,1)"
r="0"
/>
</g>
<g id="icon" fill="none" transform="matrix(1,0,0,1,200,200)">
<g transform="matrix(1,0,0,1,0,0)">
<text
id="icon"
fill="rgba(0,0,0,1)"
dominant-baseline="central"
paint-order="stroke"
text-anchor="middle"
/>
</g>
</g>
</g>
<g id="g-svg-24" fill="none" r="10" transform="matrix(1,0,0,1,0,0)">
<g transform="matrix(1,0,0,1,125,150)">
<circle
id="key"
fill="rgba(248,248,248,1)"
transform="translate(-10,-10)"
cx="10"
cy="10"
stroke="rgba(139,155,175,1)"
r="10"
/>
</g>
<g id="label" fill="none" transform="matrix(1,0,0,1,125,150)">
<g transform="matrix(1,0,0,1,-5,-5)">
<path
id="background"
fill="none"
d="M 0,0 l 10,0 l 0,10 l-10 0 z"
width="10"
height="10"
/>
</g>
<g transform="matrix(1,0,0,1,0,0)">
<text
id="text"
fill="rgba(0,0,0,1)"
dominant-baseline="central"
paint-order="stroke"
text-anchor="middle"
/>
</g>
</g>
<g transform="matrix(1,0,0,1,125,150)">
<circle
id="halo"
fill="rgba(248,248,248,1)"
cx="0"
cy="0"
stroke="rgba(139,155,175,1)"
r="0"
/>
</g>
<g id="icon" fill="none" transform="matrix(1,0,0,1,125,150)">
<g transform="matrix(1,0,0,1,0,0)">
<text
id="icon"
fill="rgba(0,0,0,1)"
dominant-baseline="central"
paint-order="stroke"
text-anchor="middle"
/>
</g>
</g>
</g>
<g id="g-svg-47" fill="none" r="10" transform="matrix(1,0,0,1,0,0)">
<g transform="matrix(1,0,0,1,50,200)">
<circle
id="key"
fill="rgba(248,248,248,1)"
transform="translate(-10,-10)"
cx="10"
cy="10"
stroke="rgba(255,165,0,1)"
r="10"
opacity="0.17441823980162885"
/>
</g>
<g id="label" fill="none" transform="matrix(1,0,0,1,50,200)">
<g transform="matrix(1,0,0,1,-5,-5)">
<path
id="background"
fill="none"
d="M 0,0 l 10,0 l 0,10 l-10 0 z"
width="10"
height="10"
opacity="0.17441823980162885"
/>
</g>
<g transform="matrix(1,0,0,1,0,0)">
<text
id="text"
fill="rgba(0,0,0,1)"
dominant-baseline="central"
paint-order="stroke"
text-anchor="middle"
opacity="0.17441823980162885"
/>
</g>
</g>
<g transform="matrix(1,0,0,1,50,200)">
<circle
id="halo"
fill="rgba(248,248,248,1)"
cx="0"
cy="0"
stroke="rgba(255,165,0,1)"
r="0"
opacity="0.17441823980162885"
/>
</g>
<g id="icon" fill="none" transform="matrix(1,0,0,1,50,200)">
<g transform="matrix(1,0,0,1,0,0)">
<text
id="icon"
fill="rgba(0,0,0,1)"
dominant-baseline="central"
paint-order="stroke"
text-anchor="middle"
opacity="0.17441823980162885"
/>
</g>
</g>
</g>
<g id="g-svg-55" fill="none" r="10" transform="matrix(1,0,0,1,0,0)">
<g transform="matrix(1,0,0,1,75,150)">
<circle
id="key"
fill="rgba(248,248,248,1)"
transform="translate(-10,-10)"
cx="10"
cy="10"
stroke="rgba(128,0,128,1)"
r="10"
opacity="0.17441823980162885"
/>
</g>
<g id="label" fill="none" transform="matrix(1,0,0,1,75,150)">
<g transform="matrix(1,0,0,1,-5,-5)">
<path
id="background"
fill="none"
d="M 0,0 l 10,0 l 0,10 l-10 0 z"
width="10"
height="10"
opacity="0.17441823980162885"
/>
</g>
<g transform="matrix(1,0,0,1,0,0)">
<text
id="text"
fill="rgba(0,0,0,1)"
dominant-baseline="central"
paint-order="stroke"
text-anchor="middle"
opacity="0.17441823980162885"
/>
</g>
</g>
<g transform="matrix(1,0,0,1,75,150)">
<circle
id="halo"
fill="rgba(248,248,248,1)"
cx="0"
cy="0"
stroke="rgba(128,0,128,1)"
r="0"
opacity="0.17441823980162885"
/>
</g>
<g id="icon" fill="none" transform="matrix(1,0,0,1,75,150)">
<g transform="matrix(1,0,0,1,0,0)">
<text
id="icon"
fill="rgba(0,0,0,1)"
dominant-baseline="central"
paint-order="stroke"
text-anchor="middle"
opacity="0.17441823980162885"
/>
</g>
</g>
</g>
<g id="g-svg-63" fill="none" r="10" transform="matrix(1,0,0,1,0,0)">
<g transform="matrix(1,0,0,1,200,100)">
<circle
id="key"
fill="rgba(248,248,248,1)"
transform="translate(-10,-10)"
cx="10"
cy="10"
stroke="rgba(0,255,255,1)"
r="10"
opacity="0.17441823980162885"
/>
</g>
<g id="label" fill="none" transform="matrix(1,0,0,1,200,100)">
<g transform="matrix(1,0,0,1,-5,-5)">
<path
id="background"
fill="none"
d="M 0,0 l 10,0 l 0,10 l-10 0 z"
width="10"
height="10"
opacity="0.17441823980162885"
/>
</g>
<g transform="matrix(1,0,0,1,0,0)">
<text
id="text"
fill="rgba(0,0,0,1)"
dominant-baseline="central"
paint-order="stroke"
text-anchor="middle"
opacity="0.17441823980162885"
/>
</g>
</g>
<g transform="matrix(1,0,0,1,200,100)">
<circle
id="halo"
fill="rgba(248,248,248,1)"
cx="0"
cy="0"
stroke="rgba(0,255,255,1)"
r="0"
opacity="0.17441823980162885"
/>
</g>
<g id="icon" fill="none" transform="matrix(1,0,0,1,200,100)">
<g transform="matrix(1,0,0,1,0,0)">
<text
id="icon"
fill="rgba(0,0,0,1)"
dominant-baseline="central"
paint-order="stroke"
text-anchor="middle"
opacity="0.17441823980162885"
/>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -0,0 +1,315 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="500"
height="500"
style="background: transparent; position: absolute; outline: none;"
color-interpolation-filters="sRGB"
tabindex="1"
>
<defs />
<g id="g-svg-camera" transform="matrix(1,0,0,1,0,0)">
<g id="g-root" fill="none" transform="matrix(1,0,0,1,0,0)">
<g id="g-svg-7" fill="none" transform="matrix(1,0,0,1,0,0)" />
<g id="g-svg-6" fill="none" transform="matrix(1,0,0,1,0,0)">
<g
id="g-svg-32"
fill="none"
marker-start="false"
marker-end="false"
transform="matrix(1,0,0,1,0,0)"
>
<g transform="matrix(1,0,0,1,50,50)">
<line
id="key"
fill="none"
x1="0"
y1="0"
x2="150"
y2="0"
stroke-width="2"
stroke="rgba(255,192,203,1)"
/>
</g>
<g id="label" fill="none" transform="matrix(1,0,0,1,129,44)">
<g transform="matrix(1,0,0,1,-5,-5)">
<path
id="background"
fill="none"
d="M 0,0 l 10,0 l 0,10 l-10 0 z"
width="10"
height="10"
/>
</g>
<g transform="matrix(1,0,0,1,0,0)">
<text
id="text"
fill="rgba(0,0,0,1)"
dominant-baseline="text-after-edge"
paint-order="stroke"
text-anchor="middle"
/>
</g>
</g>
</g>
<g
id="g-svg-37"
fill="none"
marker-start="false"
marker-end="false"
transform="matrix(1,0,0,1,0,0)"
>
<g transform="matrix(1,0,0,1,125,50)">
<line
id="key"
fill="none"
x1="75"
y1="0"
x2="0"
y2="100"
stroke-width="1"
stroke="rgba(139,155,175,1)"
/>
</g>
<g
id="label"
fill="none"
transform="matrix(0.600000,-0.800000,0.800000,0.600000,155.300003,99.599998)"
>
<g transform="matrix(1,0,0,1,-5,-5)">
<path
id="background"
fill="none"
d="M 0,0 l 10,0 l 0,10 l-10 0 z"
width="10"
height="10"
/>
</g>
<g transform="matrix(1,0,0,1,0,0)">
<text
id="text"
fill="rgba(0,0,0,1)"
dominant-baseline="text-after-edge"
paint-order="stroke"
text-anchor="middle"
/>
</g>
</g>
</g>
<g
id="g-svg-42"
fill="none"
marker-start="false"
marker-end="false"
transform="matrix(1,0,0,1,0,0)"
>
<g transform="matrix(1,0,0,1,50,50)">
<line
id="key"
fill="none"
x1="75"
y1="100"
x2="0"
y2="0"
stroke-width="1"
stroke="rgba(139,155,175,1)"
/>
</g>
<g
id="label"
fill="none"
transform="matrix(0.600000,0.800000,-0.800000,0.600000,89.900002,93.199997)"
>
<g transform="matrix(1,0,0,1,-5,-5)">
<path
id="background"
fill="none"
d="M 0,0 l 10,0 l 0,10 l-10 0 z"
width="10"
height="10"
/>
</g>
<g transform="matrix(1,0,0,1,0,0)">
<text
id="text"
fill="rgba(0,0,0,1)"
dominant-baseline="text-after-edge"
paint-order="stroke"
text-anchor="middle"
/>
</g>
</g>
</g>
</g>
<g id="g-svg-5" fill="none" transform="matrix(1,0,0,1,0,0)">
<g id="g-svg-8" fill="none" r="10" transform="matrix(1,0,0,1,0,0)">
<g transform="matrix(1,0,0,1,50,50)">
<circle
id="key"
fill="rgba(255,192,203,1)"
transform="translate(-10,-10)"
cx="10"
cy="10"
stroke="rgba(139,155,175,1)"
stroke-width="2"
r="10"
/>
</g>
<g id="label" fill="none" transform="matrix(1,0,0,1,50,50)">
<g transform="matrix(1,0,0,1,-5,-5)">
<path
id="background"
fill="none"
d="M 0,0 l 10,0 l 0,10 l-10 0 z"
width="10"
height="10"
/>
</g>
<g transform="matrix(1,0,0,1,0,0)">
<text
id="text"
fill="rgba(0,0,0,1)"
dominant-baseline="central"
paint-order="stroke"
text-anchor="middle"
/>
</g>
</g>
<g transform="matrix(1,0,0,1,50,50)">
<circle
id="halo"
fill="rgba(255,192,203,1)"
cx="0"
cy="0"
stroke="rgba(139,155,175,1)"
stroke-width="2"
r="0"
/>
</g>
<g id="icon" fill="none" transform="matrix(1,0,0,1,50,50)">
<g transform="matrix(1,0,0,1,0,0)">
<text
id="icon"
fill="rgba(0,0,0,1)"
dominant-baseline="central"
paint-order="stroke"
text-anchor="middle"
/>
</g>
</g>
</g>
<g id="g-svg-16" fill="none" r="10" transform="matrix(1,0,0,1,0,0)">
<g transform="matrix(1,0,0,1,200,50)">
<circle
id="key"
fill="rgba(248,248,248,1)"
transform="translate(-10,-10)"
cx="10"
cy="10"
stroke="rgba(139,155,175,1)"
stroke-width="1"
r="10"
/>
</g>
<g id="label" fill="none" transform="matrix(1,0,0,1,200,50)">
<g transform="matrix(1,0,0,1,-5,-5)">
<path
id="background"
fill="none"
d="M 0,0 l 10,0 l 0,10 l-10 0 z"
width="10"
height="10"
/>
</g>
<g transform="matrix(1,0,0,1,0,0)">
<text
id="text"
fill="rgba(0,0,0,1)"
dominant-baseline="central"
paint-order="stroke"
text-anchor="middle"
/>
</g>
</g>
<g transform="matrix(1,0,0,1,200,50)">
<circle
id="halo"
fill="rgba(248,248,248,1)"
cx="0"
cy="0"
stroke="rgba(139,155,175,1)"
stroke-width="1"
r="0"
/>
</g>
<g id="icon" fill="none" transform="matrix(1,0,0,1,200,50)">
<g transform="matrix(1,0,0,1,0,0)">
<text
id="icon"
fill="rgba(0,0,0,1)"
dominant-baseline="central"
paint-order="stroke"
text-anchor="middle"
/>
</g>
</g>
</g>
<g id="g-svg-24" fill="none" r="10" transform="matrix(1,0,0,1,0,0)">
<g transform="matrix(1,0,0,1,125,150)">
<circle
id="key"
fill="rgba(248,248,248,1)"
transform="translate(-10,-10)"
cx="10"
cy="10"
stroke="rgba(139,155,175,1)"
stroke-width="2"
r="10"
/>
</g>
<g id="label" fill="none" transform="matrix(1,0,0,1,125,150)">
<g transform="matrix(1,0,0,1,-5,-5)">
<path
id="background"
fill="none"
d="M 0,0 l 10,0 l 0,10 l-10 0 z"
width="10"
height="10"
/>
</g>
<g transform="matrix(1,0,0,1,0,0)">
<text
id="text"
fill="rgba(0,0,0,1)"
dominant-baseline="central"
paint-order="stroke"
text-anchor="middle"
/>
</g>
</g>
<g transform="matrix(1,0,0,1,125,150)">
<circle
id="halo"
fill="rgba(248,248,248,1)"
cx="0"
cy="0"
stroke="rgba(139,155,175,1)"
stroke-width="2"
r="0"
/>
</g>
<g id="icon" fill="none" transform="matrix(1,0,0,1,125,150)">
<g transform="matrix(1,0,0,1,0,0)">
<text
id="icon"
fill="rgba(0,0,0,1)"
dominant-baseline="central"
paint-order="stroke"
text-anchor="middle"
/>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

@ -0,0 +1,315 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="500"
height="500"
style="background: transparent; position: absolute; outline: none;"
color-interpolation-filters="sRGB"
tabindex="1"
>
<defs />
<g id="g-svg-camera" transform="matrix(1,0,0,1,0,0)">
<g id="g-root" fill="none" transform="matrix(1,0,0,1,0,0)">
<g id="g-svg-7" fill="none" transform="matrix(1,0,0,1,0,0)" />
<g id="g-svg-6" fill="none" transform="matrix(1,0,0,1,0,0)">
<g
id="g-svg-32"
fill="none"
marker-start="false"
marker-end="false"
transform="matrix(1,0,0,1,0,0)"
>
<g transform="matrix(1,0,0,1,50,50)">
<line
id="key"
fill="none"
x1="0"
y1="0"
x2="150"
y2="0"
stroke-width="1"
stroke="rgba(139,155,175,1)"
/>
</g>
<g id="label" fill="none" transform="matrix(1,0,0,1,129,44)">
<g transform="matrix(1,0,0,1,-5,-5)">
<path
id="background"
fill="none"
d="M 0,0 l 10,0 l 0,10 l-10 0 z"
width="10"
height="10"
/>
</g>
<g transform="matrix(1,0,0,1,0,0)">
<text
id="text"
fill="rgba(0,0,0,1)"
dominant-baseline="text-after-edge"
paint-order="stroke"
text-anchor="middle"
/>
</g>
</g>
</g>
<g
id="g-svg-37"
fill="none"
marker-start="false"
marker-end="false"
transform="matrix(1,0,0,1,0,0)"
>
<g transform="matrix(1,0,0,1,125,50)">
<line
id="key"
fill="none"
x1="75"
y1="0"
x2="0"
y2="100"
stroke-width="2"
stroke="rgba(255,192,203,1)"
/>
</g>
<g
id="label"
fill="none"
transform="matrix(0.600000,-0.800000,0.800000,0.600000,155.300003,99.599998)"
>
<g transform="matrix(1,0,0,1,-5,-5)">
<path
id="background"
fill="none"
d="M 0,0 l 10,0 l 0,10 l-10 0 z"
width="10"
height="10"
/>
</g>
<g transform="matrix(1,0,0,1,0,0)">
<text
id="text"
fill="rgba(0,0,0,1)"
dominant-baseline="text-after-edge"
paint-order="stroke"
text-anchor="middle"
/>
</g>
</g>
</g>
<g
id="g-svg-42"
fill="none"
marker-start="false"
marker-end="false"
transform="matrix(1,0,0,1,0,0)"
>
<g transform="matrix(1,0,0,1,50,50)">
<line
id="key"
fill="none"
x1="75"
y1="100"
x2="0"
y2="0"
stroke-width="1"
stroke="rgba(139,155,175,1)"
/>
</g>
<g
id="label"
fill="none"
transform="matrix(0.600000,0.800000,-0.800000,0.600000,89.900002,93.199997)"
>
<g transform="matrix(1,0,0,1,-5,-5)">
<path
id="background"
fill="none"
d="M 0,0 l 10,0 l 0,10 l-10 0 z"
width="10"
height="10"
/>
</g>
<g transform="matrix(1,0,0,1,0,0)">
<text
id="text"
fill="rgba(0,0,0,1)"
dominant-baseline="text-after-edge"
paint-order="stroke"
text-anchor="middle"
/>
</g>
</g>
</g>
</g>
<g id="g-svg-5" fill="none" transform="matrix(1,0,0,1,0,0)">
<g id="g-svg-8" fill="none" r="10" transform="matrix(1,0,0,1,0,0)">
<g transform="matrix(1,0,0,1,50,50)">
<circle
id="key"
fill="rgba(248,248,248,1)"
transform="translate(-10,-10)"
cx="10"
cy="10"
stroke="rgba(139,155,175,1)"
stroke-width="1"
r="10"
/>
</g>
<g id="label" fill="none" transform="matrix(1,0,0,1,50,50)">
<g transform="matrix(1,0,0,1,-5,-5)">
<path
id="background"
fill="none"
d="M 0,0 l 10,0 l 0,10 l-10 0 z"
width="10"
height="10"
/>
</g>
<g transform="matrix(1,0,0,1,0,0)">
<text
id="text"
fill="rgba(0,0,0,1)"
dominant-baseline="central"
paint-order="stroke"
text-anchor="middle"
/>
</g>
</g>
<g transform="matrix(1,0,0,1,50,50)">
<circle
id="halo"
fill="rgba(248,248,248,1)"
cx="0"
cy="0"
stroke="rgba(139,155,175,1)"
stroke-width="1"
r="0"
/>
</g>
<g id="icon" fill="none" transform="matrix(1,0,0,1,50,50)">
<g transform="matrix(1,0,0,1,0,0)">
<text
id="icon"
fill="rgba(0,0,0,1)"
dominant-baseline="central"
paint-order="stroke"
text-anchor="middle"
/>
</g>
</g>
</g>
<g id="g-svg-16" fill="none" r="10" transform="matrix(1,0,0,1,0,0)">
<g transform="matrix(1,0,0,1,200,50)">
<circle
id="key"
fill="rgba(248,248,248,1)"
transform="translate(-10,-10)"
cx="10"
cy="10"
stroke="rgba(139,155,175,1)"
stroke-width="2"
r="10"
/>
</g>
<g id="label" fill="none" transform="matrix(1,0,0,1,200,50)">
<g transform="matrix(1,0,0,1,-5,-5)">
<path
id="background"
fill="none"
d="M 0,0 l 10,0 l 0,10 l-10 0 z"
width="10"
height="10"
/>
</g>
<g transform="matrix(1,0,0,1,0,0)">
<text
id="text"
fill="rgba(0,0,0,1)"
dominant-baseline="central"
paint-order="stroke"
text-anchor="middle"
/>
</g>
</g>
<g transform="matrix(1,0,0,1,200,50)">
<circle
id="halo"
fill="rgba(248,248,248,1)"
cx="0"
cy="0"
stroke="rgba(139,155,175,1)"
stroke-width="2"
r="0"
/>
</g>
<g id="icon" fill="none" transform="matrix(1,0,0,1,200,50)">
<g transform="matrix(1,0,0,1,0,0)">
<text
id="icon"
fill="rgba(0,0,0,1)"
dominant-baseline="central"
paint-order="stroke"
text-anchor="middle"
/>
</g>
</g>
</g>
<g id="g-svg-24" fill="none" r="10" transform="matrix(1,0,0,1,0,0)">
<g transform="matrix(1,0,0,1,125,150)">
<circle
id="key"
fill="rgba(255,192,203,1)"
transform="translate(-10,-10)"
cx="10"
cy="10"
stroke="rgba(139,155,175,1)"
stroke-width="1"
r="10"
/>
</g>
<g id="label" fill="none" transform="matrix(1,0,0,1,125,150)">
<g transform="matrix(1,0,0,1,-5,-5)">
<path
id="background"
fill="none"
d="M 0,0 l 10,0 l 0,10 l-10 0 z"
width="10"
height="10"
/>
</g>
<g transform="matrix(1,0,0,1,0,0)">
<text
id="text"
fill="rgba(0,0,0,1)"
dominant-baseline="central"
paint-order="stroke"
text-anchor="middle"
/>
</g>
</g>
<g transform="matrix(1,0,0,1,125,150)">
<circle
id="halo"
fill="rgba(255,192,203,1)"
cx="0"
cy="0"
stroke="rgba(139,155,175,1)"
stroke-width="1"
r="0"
/>
</g>
<g id="icon" fill="none" transform="matrix(1,0,0,1,125,150)">
<g transform="matrix(1,0,0,1,0,0)">
<text
id="icon"
fill="rgba(0,0,0,1)"
dominant-baseline="central"
paint-order="stroke"
text-anchor="middle"
/>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

@ -0,0 +1,309 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="500"
height="500"
style="background: transparent; position: absolute; outline: none;"
color-interpolation-filters="sRGB"
tabindex="1"
>
<defs />
<g id="g-svg-camera" transform="matrix(1,0,0,1,0,0)">
<g id="g-root" fill="none" transform="matrix(1,0,0,1,0,0)">
<g id="g-svg-7" fill="none" transform="matrix(1,0,0,1,0,0)" />
<g id="g-svg-6" fill="none" transform="matrix(1,0,0,1,0,0)">
<g
id="g-svg-32"
fill="none"
marker-start="false"
marker-end="false"
transform="matrix(1,0,0,1,0,0)"
>
<g transform="matrix(1,0,0,1,50,50)">
<line
id="key"
fill="none"
x1="0"
y1="0"
x2="150"
y2="0"
stroke-width="1"
stroke="rgba(139,155,175,1)"
/>
</g>
<g id="label" fill="none" transform="matrix(1,0,0,1,129,44)">
<g transform="matrix(1,0,0,1,-5,-5)">
<path
id="background"
fill="none"
d="M 0,0 l 10,0 l 0,10 l-10 0 z"
width="10"
height="10"
/>
</g>
<g transform="matrix(1,0,0,1,0,0)">
<text
id="text"
fill="rgba(0,0,0,1)"
dominant-baseline="text-after-edge"
paint-order="stroke"
text-anchor="middle"
/>
</g>
</g>
</g>
<g
id="g-svg-37"
fill="none"
marker-start="false"
marker-end="false"
transform="matrix(1,0,0,1,0,0)"
>
<g transform="matrix(1,0,0,1,125,50)">
<line
id="key"
fill="none"
x1="75"
y1="0"
x2="0"
y2="100"
stroke-width="1"
stroke="rgba(139,155,175,1)"
/>
</g>
<g
id="label"
fill="none"
transform="matrix(0.600000,-0.800000,0.800000,0.600000,155.300003,99.599998)"
>
<g transform="matrix(1,0,0,1,-5,-5)">
<path
id="background"
fill="none"
d="M 0,0 l 10,0 l 0,10 l-10 0 z"
width="10"
height="10"
/>
</g>
<g transform="matrix(1,0,0,1,0,0)">
<text
id="text"
fill="rgba(0,0,0,1)"
dominant-baseline="text-after-edge"
paint-order="stroke"
text-anchor="middle"
/>
</g>
</g>
</g>
<g
id="g-svg-42"
fill="none"
marker-start="false"
marker-end="false"
transform="matrix(1,0,0,1,0,0)"
>
<g transform="matrix(1,0,0,1,50,50)">
<line
id="key"
fill="none"
x1="75"
y1="100"
x2="0"
y2="0"
stroke-width="1"
stroke="rgba(139,155,175,1)"
/>
</g>
<g
id="label"
fill="none"
transform="matrix(0.600000,0.800000,-0.800000,0.600000,89.900002,93.199997)"
>
<g transform="matrix(1,0,0,1,-5,-5)">
<path
id="background"
fill="none"
d="M 0,0 l 10,0 l 0,10 l-10 0 z"
width="10"
height="10"
/>
</g>
<g transform="matrix(1,0,0,1,0,0)">
<text
id="text"
fill="rgba(0,0,0,1)"
dominant-baseline="text-after-edge"
paint-order="stroke"
text-anchor="middle"
/>
</g>
</g>
</g>
</g>
<g id="g-svg-5" fill="none" transform="matrix(1,0,0,1,0,0)">
<g id="g-svg-8" fill="none" r="10" transform="matrix(1,0,0,1,0,0)">
<g transform="matrix(1,0,0,1,50,50)">
<circle
id="key"
fill="rgba(248,248,248,1)"
transform="translate(-10,-10)"
cx="10"
cy="10"
stroke="rgba(139,155,175,1)"
r="10"
/>
</g>
<g id="label" fill="none" transform="matrix(1,0,0,1,50,50)">
<g transform="matrix(1,0,0,1,-5,-5)">
<path
id="background"
fill="none"
d="M 0,0 l 10,0 l 0,10 l-10 0 z"
width="10"
height="10"
/>
</g>
<g transform="matrix(1,0,0,1,0,0)">
<text
id="text"
fill="rgba(0,0,0,1)"
dominant-baseline="central"
paint-order="stroke"
text-anchor="middle"
/>
</g>
</g>
<g transform="matrix(1,0,0,1,50,50)">
<circle
id="halo"
fill="rgba(248,248,248,1)"
cx="0"
cy="0"
stroke="rgba(139,155,175,1)"
r="0"
/>
</g>
<g id="icon" fill="none" transform="matrix(1,0,0,1,50,50)">
<g transform="matrix(1,0,0,1,0,0)">
<text
id="icon"
fill="rgba(0,0,0,1)"
dominant-baseline="central"
paint-order="stroke"
text-anchor="middle"
/>
</g>
</g>
</g>
<g id="g-svg-16" fill="none" r="10" transform="matrix(1,0,0,1,0,0)">
<g transform="matrix(1,0,0,1,200,50)">
<circle
id="key"
fill="rgba(248,248,248,1)"
transform="translate(-10,-10)"
cx="10"
cy="10"
stroke="rgba(139,155,175,1)"
r="10"
/>
</g>
<g id="label" fill="none" transform="matrix(1,0,0,1,200,50)">
<g transform="matrix(1,0,0,1,-5,-5)">
<path
id="background"
fill="none"
d="M 0,0 l 10,0 l 0,10 l-10 0 z"
width="10"
height="10"
/>
</g>
<g transform="matrix(1,0,0,1,0,0)">
<text
id="text"
fill="rgba(0,0,0,1)"
dominant-baseline="central"
paint-order="stroke"
text-anchor="middle"
/>
</g>
</g>
<g transform="matrix(1,0,0,1,200,50)">
<circle
id="halo"
fill="rgba(248,248,248,1)"
cx="0"
cy="0"
stroke="rgba(139,155,175,1)"
r="0"
/>
</g>
<g id="icon" fill="none" transform="matrix(1,0,0,1,200,50)">
<g transform="matrix(1,0,0,1,0,0)">
<text
id="icon"
fill="rgba(0,0,0,1)"
dominant-baseline="central"
paint-order="stroke"
text-anchor="middle"
/>
</g>
</g>
</g>
<g id="g-svg-24" fill="none" r="10" transform="matrix(1,0,0,1,0,0)">
<g transform="matrix(1,0,0,1,125,150)">
<circle
id="key"
fill="rgba(248,248,248,1)"
transform="translate(-10,-10)"
cx="10"
cy="10"
stroke="rgba(139,155,175,1)"
r="10"
/>
</g>
<g id="label" fill="none" transform="matrix(1,0,0,1,125,150)">
<g transform="matrix(1,0,0,1,-5,-5)">
<path
id="background"
fill="none"
d="M 0,0 l 10,0 l 0,10 l-10 0 z"
width="10"
height="10"
/>
</g>
<g transform="matrix(1,0,0,1,0,0)">
<text
id="text"
fill="rgba(0,0,0,1)"
dominant-baseline="central"
paint-order="stroke"
text-anchor="middle"
/>
</g>
</g>
<g transform="matrix(1,0,0,1,125,150)">
<circle
id="halo"
fill="rgba(248,248,248,1)"
cx="0"
cy="0"
stroke="rgba(139,155,175,1)"
r="0"
/>
</g>
<g id="icon" fill="none" transform="matrix(1,0,0,1,125,150)">
<g transform="matrix(1,0,0,1,0,0)">
<text
id="icon"
fill="rgba(0,0,0,1)"
dominant-baseline="central"
paint-order="stroke"
text-anchor="middle"
/>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 9.0 KiB

View File

@ -15,6 +15,7 @@ describe('static', () => {
try {
const { preprocess, postprocess } = testCase;
await preprocess?.();
await canvas.init();
await testCase({ canvas });
await expect(canvas).toMatchSVGSnapshot(`${__dirname}/snapshots/static`, name);
await postprocess?.();

View File

@ -48,13 +48,14 @@ function loadCasesList(select: HTMLSelectElement) {
function onchange(testCase: TestCase, rendererName: string) {
const renderer = getRenderer(rendererName);
testCase({
canvas: new Canvas({
width: 500,
height: 500,
container: document.getElementById('container')!,
renderer,
}),
const canvas = new Canvas({
width: 500,
height: 500,
container: document.getElementById('container')!,
renderer,
});
canvas.init().then(() => {
testCase({ canvas });
});
}

View File

@ -0,0 +1,3 @@
import EventEmitter from '@antv/event-emitter';
export class Graph extends EventEmitter {}

View File

@ -0,0 +1 @@
export * from './graph';

View File

@ -1,33 +1,35 @@
import { BUILT_IN_EDGES, BUILT_IN_NODES } from '../../src/elements';
import { getPlugin, getPlugins, register, registerBuiltInPlugins } from '../../src/registry';
import { BUILT_IN_THEMES } from '../../src/themes';
describe('registry', () => {
it('registerBuiltInPlugins', () => {
registerBuiltInPlugins();
// TODO 在变更内置插件后更新此用例 / update this when we have more built-in plugins
expect(getPlugins('node')).toEqual({});
expect(getPlugins('edge')).toEqual({});
expect(getPlugins('node')).toEqual(BUILT_IN_NODES);
expect(getPlugins('edge')).toEqual(BUILT_IN_EDGES);
expect(getPlugins('combo')).toEqual({});
expect(getPlugins('theme')).toEqual({});
expect(getPlugins('theme')).toEqual(BUILT_IN_THEMES);
});
it('register, getPlugin, getPlugins', () => {
class CircleNode {}
class RectNode {}
class Edge {}
register('node', 'circle-node', CircleNode);
register('node', 'rect-node', RectNode);
register('edge', 'edge', Edge);
register('node', 'circle-node', CircleNode as any);
register('node', 'rect-node', RectNode as any);
register('edge', 'line-edge', Edge);
expect(getPlugin('node', 'circle-node')).toEqual(CircleNode);
expect(getPlugin('node', 'rect-node')).toEqual(RectNode);
expect(getPlugin('node', 'diamond-node')).toEqual(undefined);
expect(getPlugin('edge', 'edge')).toEqual(Edge);
expect(getPlugin('edge', 'line-edge')).toEqual(Edge);
expect(() => {
register('node', 'circle-node', CircleNode);
register('node', 'circle-node', CircleNode as any);
}).toThrow();
expect(getPlugins('node')).toEqual({
...BUILT_IN_NODES,
'circle-node': CircleNode,
'rect-node': RectNode,
});

View File

@ -411,83 +411,67 @@ describe('DataController', () => {
});
});
it('changes', (done) => {
it('changes', () => {
const controller = new DataController();
controller.once('change', (original: any) => {
expect(original).toEqual([
{ value: { id: 'combo-1' }, type: 'ComboAdded' },
{ value: { id: 'node-1', data: { value: 1 }, style: { fill: 'red' } }, type: 'NodeAdded' },
{
value: { id: 'node-2', data: { value: 2 }, style: { fill: 'green', parentId: 'combo-1' } },
type: 'NodeAdded',
},
{
value: { id: 'node-3', data: { value: 3 }, style: { fill: 'blue', parentId: 'combo-1' } },
type: 'NodeAdded',
},
{ value: { id: 'edge-1', source: 'node-1', target: 'node-2', data: { weight: 1 } }, type: 'EdgeAdded' },
{ value: { id: 'edge-2', source: 'node-2', target: 'node-3', data: { weight: 2 } }, type: 'EdgeAdded' },
{ value: { id: 'combo-2' }, type: 'ComboAdded' },
{ value: { id: 'node-4', data: { value: 4 }, style: { fill: 'yellow' } }, type: 'NodeAdded' },
{
value: { id: 'node-3', data: { value: 3 }, style: { fill: 'pink', parentId: 'combo-2' } },
original: { id: 'node-3', data: { value: 3 }, style: { fill: 'blue', parentId: 'combo-1' } },
type: 'NodeUpdated',
},
{ value: { id: 'edge-1', source: 'node-1', target: 'node-2', data: { weight: 1 } }, type: 'EdgeRemoved' },
{ value: { id: 'edge-2', source: 'node-2', target: 'node-3', data: { weight: 2 } }, type: 'EdgeRemoved' },
{ value: { id: 'node-1', data: { value: 1 }, style: { fill: 'red' } }, type: 'NodeRemoved' },
{
value: { id: 'node-2', data: { value: 2 }, style: { fill: 'green', parentId: 'combo-1' } },
type: 'NodeRemoved',
},
{ value: { id: 'combo-1' }, type: 'ComboRemoved' },
]);
controller.addData(clone(data));
expect(reduceDataChanges(original)).toEqual([
{
type: 'NodeAdded',
value: { id: 'node-3', data: { value: 3 }, style: { fill: 'pink', parentId: 'combo-2' } },
},
{ value: { id: 'combo-2' }, type: 'ComboAdded' },
{ value: { id: 'node-4', data: { value: 4 }, style: { fill: 'yellow' } }, type: 'NodeAdded' },
]);
done();
controller.setData({
nodes: [
{ id: 'node-3', data: { value: 3 }, style: { fill: 'pink', parentId: 'combo-2' } },
{ id: 'node-4', data: { value: 4 }, style: { fill: 'yellow' } },
],
combos: [{ id: 'combo-2' }],
});
controller.batch(() => {
controller.addData(clone(data));
const changes = controller.getChanges();
controller.setData({
nodes: [
{ id: 'node-3', data: { value: 3 }, style: { fill: 'pink', parentId: 'combo-2' } },
{ id: 'node-4', data: { value: 4 }, style: { fill: 'yellow' } },
],
combos: [{ id: 'combo-2' }],
});
});
expect(changes).toEqual([
{ value: { id: 'combo-1' }, type: 'ComboAdded' },
{ value: { id: 'node-1', data: { value: 1 }, style: { fill: 'red' } }, type: 'NodeAdded' },
{
value: { id: 'node-2', data: { value: 2 }, style: { fill: 'green', parentId: 'combo-1' } },
type: 'NodeAdded',
},
{
value: { id: 'node-3', data: { value: 3 }, style: { fill: 'blue', parentId: 'combo-1' } },
type: 'NodeAdded',
},
{ value: { id: 'edge-1', source: 'node-1', target: 'node-2', data: { weight: 1 } }, type: 'EdgeAdded' },
{ value: { id: 'edge-2', source: 'node-2', target: 'node-3', data: { weight: 2 } }, type: 'EdgeAdded' },
{ value: { id: 'combo-2' }, type: 'ComboAdded' },
{ value: { id: 'node-4', data: { value: 4 }, style: { fill: 'yellow' } }, type: 'NodeAdded' },
{
value: { id: 'node-3', data: { value: 3 }, style: { fill: 'pink', parentId: 'combo-2' } },
original: { id: 'node-3', data: { value: 3 }, style: { fill: 'blue', parentId: 'combo-1' } },
type: 'NodeUpdated',
},
{ value: { id: 'edge-1', source: 'node-1', target: 'node-2', data: { weight: 1 } }, type: 'EdgeRemoved' },
{ value: { id: 'edge-2', source: 'node-2', target: 'node-3', data: { weight: 2 } }, type: 'EdgeRemoved' },
{ value: { id: 'node-1', data: { value: 1 }, style: { fill: 'red' } }, type: 'NodeRemoved' },
{
value: { id: 'node-2', data: { value: 2 }, style: { fill: 'green', parentId: 'combo-1' } },
type: 'NodeRemoved',
},
{ value: { id: 'combo-1' }, type: 'ComboRemoved' },
]);
expect(reduceDataChanges(changes)).toEqual([
{
type: 'NodeAdded',
value: { id: 'node-3', data: { value: 3 }, style: { fill: 'pink', parentId: 'combo-2' } },
},
{ value: { id: 'combo-2' }, type: 'ComboAdded' },
{ value: { id: 'node-4', data: { value: 4 }, style: { fill: 'yellow' } }, type: 'NodeAdded' },
]);
// re pull
expect(controller.getChanges()).toEqual([]);
});
it('changes add', (done) => {
it('changes add', () => {
const controller = new DataController();
controller.once('change', (original: any) => {
expect(original).toEqual(original);
expect(reduceDataChanges(original)).toEqual([
{ value: { id: 'combo-2' }, type: 'ComboAdded' },
{
type: 'NodeAdded',
value: { id: 'node-3', data: { value: 3 }, style: { fill: 'pink', parentId: 'combo-2' } },
},
{ value: { id: 'node-4', data: { value: 4 }, style: { fill: 'yellow' } }, type: 'NodeAdded' },
]);
done();
});
controller.addData({
nodes: [
{ id: 'node-3', data: { value: 3 }, style: { fill: 'pink', parentId: 'combo-2' } },
@ -495,6 +479,17 @@ describe('DataController', () => {
],
combos: [{ id: 'combo-2' }],
});
const changes = controller.getChanges();
expect(reduceDataChanges(changes)).toEqual([
{ value: { id: 'combo-2' }, type: 'ComboAdded' },
{
type: 'NodeAdded',
value: { id: 'node-3', data: { value: 3 }, style: { fill: 'pink', parentId: 'combo-2' } },
},
{ value: { id: 'node-4', data: { value: 4 }, style: { fill: 'yellow' } }, type: 'NodeAdded' },
]);
});
it('getElementData', () => {
@ -525,16 +520,6 @@ describe('DataController', () => {
expect(controller.getNodeLikeData()).toEqual([...data.combos, ...data.nodes]);
});
it('classifyNodeLikeData', () => {
const controller = new DataController();
controller.addData(clone(data));
expect(controller.classifyNodeLikeData([...data.combos, ...data.nodes])).toEqual({
nodes: data.nodes,
combos: data.combos,
});
});
it('hasNode', () => {
const controller = new DataController();

View File

@ -0,0 +1,241 @@
import type { G6Spec } from '../../../src';
import { BUILT_IN_PALETTES } from '../../../src/palettes';
import '../../../src/preset';
import { DataController } from '../../../src/runtime/data';
import { ElementController } from '../../../src/runtime/element';
import type { RuntimeContext } from '../../../src/runtime/types';
import { LIGHT_THEME } from '../../../src/themes/light';
import { idOf } from '../../../src/utils/id';
import { Graph } from '../../mock';
class Canvas {
init() {
return Promise.resolve();
}
children: unknown[] = [];
appendChild(node: unknown) {
this.children.push(node);
return node;
}
}
const createContext = (options: G6Spec): RuntimeContext => {
const dataController = new DataController();
dataController.setData(options.data || {});
return {
canvas: new Canvas() as any,
graph: new Graph() as any,
options,
dataController,
};
};
describe('ElementController', () => {
it('static', async () => {
const options: G6Spec = {
data: {
nodes: [
{ id: 'node-1', style: { fill: 'red', stroke: 'pink', lineWidth: 1 }, data: { value: 100 } },
{ id: 'node-2', data: { value: 150 } },
{ id: 'node-3', style: { parentId: 'combo-1', states: ['selected'] }, data: { value: 150 } },
],
edges: [
{ source: 'node-1', target: 'node-2', data: { weight: 250 } },
{
source: 'node-2',
target: 'node-3',
style: { lineWidth: 5, states: ['active', 'selected'] },
data: { weight: 300 },
},
],
combos: [{ id: 'combo-1' }],
},
theme: 'light',
node: {
style: {
fill: (datum: any) => (datum?.data?.value > 100 ? 'red' : 'blue'),
border: (datum: any, index: number, data: any) => (index % 2 === 0 ? 0 : 10),
},
state: {
selected: {
fill: (datum: any) => (datum?.data?.value > 100 ? 'purple' : 'cyan'),
},
},
palette: 'spectral',
},
edge: {
style: {},
state: {
selected: {
stroke: 'red',
},
active: {
stroke: 'pink',
lineWidth: 4,
},
},
palette: { type: 'group', color: 'oranges', invert: true },
},
combo: {
style: {},
state: {},
palette: 'blues',
},
};
const context = createContext(options);
const elementController = new ElementController(context);
const edge1Id = idOf(options.data!.edges![0]);
const edge2Id = idOf(options.data!.edges![1]);
// @ts-expect-error computeStyle is private
elementController.computeStyle();
expect(elementController.getDataStyle('node', 'node-1')).toEqual(options.data!.nodes![0].style || {});
// 没有属性 / no style
expect(elementController.getDataStyle('node', 'node-2')).toEqual({});
// 没有样式属性 / No style attribute
expect(elementController.getDataStyle('node', 'node-3')).toEqual({});
expect(elementController.getDataStyle('edge', edge1Id)).toEqual(options.data!.edges![0].style || {});
expect(elementController.getDataStyle('combo', 'combo-1')).toEqual({});
// ref light theme
expect(elementController.getThemeStyle('node')).toEqual(LIGHT_THEME.node!.style);
expect(elementController.getThemeStateStyle('node', [])).toEqual({});
expect(elementController.getThemeStateStyle('node', ['selected'])).toEqual({
...LIGHT_THEME.node!.state!.selected,
});
expect(elementController.getThemeStateStyle('node', ['selected', 'active'])).toEqual({
...LIGHT_THEME.node!.state!.selected,
...LIGHT_THEME.node!.state!.active,
});
const paletteKey = 'keyShapeColor';
expect(elementController.getPaletteStyle('node-1')[paletteKey]).toBe(BUILT_IN_PALETTES.spectral[0]);
expect(elementController.getPaletteStyle('node-2')[paletteKey]).toBe(BUILT_IN_PALETTES.spectral[1]);
expect(elementController.getPaletteStyle('node-3')[paletteKey]).toBe(BUILT_IN_PALETTES.spectral[2]);
// invert
expect(elementController.getPaletteStyle(edge1Id)[paletteKey]).toBe(BUILT_IN_PALETTES.oranges.at(-1));
expect(elementController.getPaletteStyle(edge2Id)[paletteKey]).toBe(BUILT_IN_PALETTES.oranges.at(-2));
expect(elementController.getPaletteStyle('combo-1')[paletteKey]).toBe(BUILT_IN_PALETTES.blues[0]);
expect(elementController.getDefaultStyle('node-1')).toEqual({ fill: 'blue', border: 0 });
expect(elementController.getDefaultStyle('node-2')).toEqual({ fill: 'red', border: 10 });
expect(elementController.getDefaultStyle('node-3')).toEqual({ fill: 'red', border: 0 });
expect(elementController.getDefaultStyle(edge1Id)).toEqual({});
expect(elementController.getDefaultStyle('combo-1')).toEqual({});
expect(elementController.getStateStyle('node-1')).toEqual({});
expect(elementController.getStateStyle('node-2')).toEqual({});
expect(elementController.getStateStyle('node-3')).toEqual({ fill: 'purple' });
expect(elementController.getStateStyle(idOf(options.data!.edges![1]))).toEqual({
stroke: 'red',
lineWidth: 4,
});
expect(elementController.getStateStyle('combo-1')).toEqual({});
expect(Object.keys(elementController.getElementsByState('selected'))).toEqual([
'node-3',
idOf(options.data!.edges![1]),
]);
expect(elementController.getElementStates('node-1')).toEqual([]);
expect(elementController.getElementStates('node-2')).toEqual([]);
expect(elementController.getElementStates('node-3')).toEqual(['selected']);
expect(elementController.getElementStates('edge-1')).toEqual([]);
expect(elementController.getElementStates(idOf(options.data!.edges![1]))).toEqual(['active', 'selected']);
expect(elementController.getElementComputedStyle('node', 'node-1')).toEqual({
...LIGHT_THEME.node?.style,
fill: 'blue',
stroke: 'pink',
lineWidth: 1,
border: 0,
// from palette
keyShapeColor: BUILT_IN_PALETTES.spectral[0],
});
expect(elementController.getElementComputedStyle('node', 'node-2')).toEqual({
...LIGHT_THEME.node?.style,
fill: 'red',
border: 10,
// from palette
keyShapeColor: BUILT_IN_PALETTES.spectral[1],
});
expect(elementController.getElementComputedStyle('node', 'node-3')).toEqual({
...LIGHT_THEME.node?.style,
...LIGHT_THEME.node?.state?.selected,
border: 0,
// from state
fill: 'purple',
// from palette
keyShapeColor: BUILT_IN_PALETTES.spectral[2],
});
expect(elementController.getElementComputedStyle('edge', edge1Id)).toEqual({
...LIGHT_THEME.edge?.style,
sourcePoint: [0, 0, 0],
targetPoint: [0, 0, 0],
keyShapeColor: BUILT_IN_PALETTES.oranges.at(-1),
});
expect(elementController.getElementComputedStyle('edge', edge2Id)).toEqual({
...LIGHT_THEME.edge?.style,
...LIGHT_THEME.edge?.state?.active,
...LIGHT_THEME.edge?.state?.selected,
lineWidth: 4,
stroke: 'red',
// 在运行时环境测试 / Test in runtime environment
sourceNode: undefined,
targetNode: undefined,
// 暂未实现 / Not implemented yet
sourcePoint: [0, 0, 0],
targetPoint: [0, 0, 0],
keyShapeColor: BUILT_IN_PALETTES.oranges.at(-2),
});
expect(elementController.getElementComputedStyle('combo', 'combo-1')).toEqual({
...LIGHT_THEME.combo?.style,
keyShapeColor: BUILT_IN_PALETTES.blues[0],
children: {
// 值为 undefined 是因为在非运行时环境 / The value is undefined because it is not in the runtime environment
'node-3': undefined,
},
});
});
it('mock runtime', async () => {
const options: G6Spec = {
data: {
nodes: [
{ id: 'node-1' },
{ id: 'node-2', style: { parentId: 'combo-1' } },
{ id: 'node-3', style: { parentId: 'combo-1' } },
],
edges: [
{ source: 'node-1', target: 'node-2' },
{ source: 'node-2', target: 'node-3' },
],
combos: [{ id: 'combo-1' }],
},
};
const context = createContext(options);
const elementController = new ElementController(context);
await elementController.render(context);
// @ts-expect-error container is private
const container = elementController.container;
expect(container.node.children.length).toBe(3);
expect(container.edge.children.length).toBe(2);
// TODO 目前暂未提供 combo 图形,因此无法渲染 / Currently, combo graphics are not provided, so they cannot be rendered
expect(container.combo.children.length).toBe(0);
});
});

View File

@ -41,9 +41,7 @@ describe('spec', () => {
},
},
animation: {
enter: {
type: 'fade-in',
},
enter: 'fade',
},
palette: {
type: 'group',

View File

@ -1,9 +1,8 @@
import type { IAnimation } from '@antv/g';
import { register } from '../../../src';
import {
createAnimationsProxy,
executeAnimation,
parseAnimation,
inferDefaultValue,
preprocessKeyframes,
} from '../../../src/utils/animation';
@ -35,25 +34,6 @@ describe('animation', () => {
expect(targetPause).toHaveBeenCalledTimes(2);
});
it('parseAnimation', () => {
expect(parseAnimation('custom')).toEqual([]);
register('animation', 'custom', [{ fields: ['size'] }]);
expect(parseAnimation('custom')).toEqual([{ fields: ['size'] }]);
// built it
expect(parseAnimation('fade')).toEqual([{ fields: ['opacity'] }]);
expect(parseAnimation([{ fields: ['opacity'] }])).toEqual([{ fields: ['opacity'] }]);
expect(
parseAnimation([
{ fields: ['opacity', 'size'], shape: 'key', duration: 500 },
{ fields: ['opacity'], shape: 'halo', duration: 500 },
]),
).toEqual([
{ fields: ['opacity', 'size'], shape: 'key', duration: 500 },
{ fields: ['opacity'], shape: 'halo', duration: 500 },
]);
});
it('preprocessKeyframes', () => {
expect(
preprocessKeyframes([
@ -124,4 +104,9 @@ describe('animation', () => {
},
]);
});
it('inferDefaultValue', () => {
expect(inferDefaultValue('opacity')).toBe(1);
expect(inferDefaultValue('stroke')).toBe(undefined);
});
});

View File

@ -0,0 +1,178 @@
import '../../../src/preset';
import { register } from '../../../src/registry';
import { assignColorByPalette, parsePalette } from '../../../src/utils/palette';
describe('palette', () => {
it('parsePalette', () => {
expect(parsePalette('category3')).toEqual({ type: 'group', color: 'category3' });
expect(parsePalette(['red', 'green', 'blue'])).toEqual({
type: 'group',
color: ['red', 'green', 'blue'],
});
expect(parsePalette({ type: 'value', color: 'custom-blues', field: 'value' })).toEqual({
type: 'value',
color: 'custom-blues',
field: 'value',
});
});
it('assignColorByPalette unset', () => {
const data = [
{ id: 'node-1', data: { value: 100, category: 'A' } },
{ id: 'node-2', data: { value: 200, category: 'B' } },
{ id: 'node-3', data: { value: 300, category: 'C' } },
];
expect(assignColorByPalette(data)).toEqual({});
});
it('assignColorByPalette discrete', () => {
register('palette', 'category3', ['#1f77b4', '#ff7f0e', '#2ca02c']);
const data3 = [
{ id: 'node-1', data: { value: 100, category: 'A' } },
{ id: 'node-2', data: { value: 200, category: 'B' } },
{ id: 'node-3', data: { value: 300, category: 'C' } },
];
const data4 = [...data3, { id: 'node-4', data: { value: 400, category: 'D' } }];
const data5 = [...data4, { id: 'node-5', data: { value: 500, category: 'A' } }];
expect(
assignColorByPalette(data3, {
type: 'group',
color: 'category3',
field: 'category',
}),
).toEqual({
'node-1': '#1f77b4',
'node-2': '#ff7f0e',
'node-3': '#2ca02c',
});
// invert
expect(
assignColorByPalette(data3, {
type: 'group',
color: 'category3',
field: 'category',
invert: true,
}),
).toEqual({
'node-1': '#2ca02c',
'node-2': '#ff7f0e',
'node-3': '#1f77b4',
});
expect(
assignColorByPalette(data4, {
type: 'group',
color: 'category3',
field: 'category',
}),
).toEqual({
'node-1': '#1f77b4',
'node-2': '#ff7f0e',
'node-3': '#2ca02c',
'node-4': '#1f77b4',
});
expect(
assignColorByPalette(data5, {
type: 'group',
color: 'category3',
field: 'category',
}),
).toEqual({
'node-1': '#1f77b4',
'node-2': '#ff7f0e',
'node-3': '#2ca02c',
'node-4': '#1f77b4',
'node-5': '#1f77b4',
});
expect(
assignColorByPalette(data5, {
type: 'group',
color: 'category3',
}),
).toEqual({
'node-1': '#1f77b4',
'node-2': '#ff7f0e',
'node-3': '#2ca02c',
'node-4': '#1f77b4',
'node-5': '#ff7f0e',
});
expect(
assignColorByPalette(data5, {
type: 'group',
color: 'spectral',
}),
).toEqual({
'node-1': 'rgb(158, 1, 66)',
'node-2': 'rgb(213, 62, 79)',
'node-3': 'rgb(244, 109, 67)',
'node-4': 'rgb(253, 174, 97)',
'node-5': 'rgb(254, 224, 139)',
});
});
it('assignColorByPalette continuous', () => {
register('palette', 'custom-blues', (value) => `rgb(0, 0, ${(value * 255).toFixed(0)})`);
const createData = (length: number) => {
return Array.from({ length }, (_, index) => ({ id: `node-${index + 1}`, data: { value: index * 100 + 100 } }));
};
const data3 = createData(3);
expect(
assignColorByPalette(data3, {
type: 'value',
color: 'custom-blues',
field: 'value',
}),
).toEqual({
'node-1': 'rgb(0, 0, 0)',
'node-2': 'rgb(0, 0, 128)',
'node-3': 'rgb(0, 0, 255)',
});
// invert
expect(
assignColorByPalette(data3, {
type: 'value',
color: 'custom-blues',
field: 'value',
invert: true,
}),
).toEqual({
'node-1': 'rgb(0, 0, 255)',
'node-2': 'rgb(0, 0, 128)',
'node-3': 'rgb(0, 0, 0)',
});
const data11 = createData(11);
expect(
assignColorByPalette(data11, {
type: 'value',
color: 'custom-blues',
field: 'value',
}),
).toEqual({
'node-1': 'rgb(0, 0, 0)',
'node-2': 'rgb(0, 0, 26)',
'node-3': 'rgb(0, 0, 51)',
'node-4': 'rgb(0, 0, 77)',
'node-5': 'rgb(0, 0, 102)',
'node-6': 'rgb(0, 0, 128)',
'node-7': 'rgb(0, 0, 153)',
'node-8': 'rgb(0, 0, 179)',
'node-9': 'rgb(0, 0, 204)',
'node-10': 'rgb(0, 0, 230)',
'node-11': 'rgb(0, 0, 255)',
});
});
});

View File

@ -0,0 +1,31 @@
import { computeElementCallbackStyle } from '../../../src/utils/style';
describe('style', () => {
it('computeElementCallbackStyle', () => {
const datum = {
id: 'node-1',
data: {
value: 100,
},
type: 'A',
style: {
fill: 'pink',
lineWidth: 5,
},
};
const style = {
stroke: 'blue',
size: (data: any) => data.data.value / 2,
fill: (data: any) => (data.data.type === 'B' ? 'green' : 'red'),
};
const computedStyle = computeElementCallbackStyle(style, { datum, index: 0, elementData: [datum] });
expect(computedStyle).toEqual({
stroke: 'blue',
size: 50,
fill: 'red',
});
});
});

View File

@ -1,7 +1,7 @@
import type { STDAnimation } from './types';
export const DEFAULT_ANIMATION_OPTIONS: KeyframeAnimationOptions = {
duration: 2000,
duration: 1000,
easing: 'cubic-bezier(0.250, 0.460, 0.450, 0.940)',
iterations: 1,
fill: 'both',

View File

@ -1,8 +1,9 @@
import type { DisplayObject, IAnimation } from '@antv/g';
import { upperFirst } from '@antv/util';
import { createAnimationsProxy, executeAnimation, parseAnimation, preprocessKeyframes } from '../utils/animation';
import { isString, upperFirst } from '@antv/util';
import { getPlugin } from '../registry';
import { createAnimationsProxy, executeAnimation, inferDefaultValue, preprocessKeyframes } from '../utils/animation';
import { DEFAULT_ANIMATION_OPTIONS } from './constants';
import type { Animation, AnimationContext, AnimationEffectTiming } from './types';
import type { AnimationExecutor } from './types';
/**
* <zh/> Spec
@ -14,16 +15,12 @@ import type { Animation, AnimationContext, AnimationEffectTiming } from './types
* @param context - <zh/> | <en/> animation execution context
* @returns <zh/> | <en/> animation instance
*/
export function executor(
shape: DisplayObject,
animation: Animation,
effectTiming: AnimationEffectTiming,
context: AnimationContext,
): IAnimation | null {
const animations = parseAnimation(animation);
export const executor: AnimationExecutor = (shape, animation, effectTiming, context) => {
if (!animation) return null;
const animations = isString(animation) ? getPlugin('animation', animation) || [] : animation;
if (animations.length === 0) return null;
const { originalStyle, states } = context;
const { originalStyle, modifiedStyle, states } = context;
/**
* <zh/>
@ -47,7 +44,7 @@ export function executor(
} else {
const target = shape;
const fromStyle = originalStyle;
const toStyle = { ...target.attributes };
const toStyle = { ...target.attributes, ...modifiedStyle };
return { target, fromStyle, toStyle };
}
};
@ -64,8 +61,8 @@ export function executor(
const keyframes: Keyframe[] = [{}, {}];
fields.forEach((attr) => {
Object.assign(keyframes[0], { [attr]: fromStyle[attr] });
Object.assign(keyframes[1], { [attr]: toStyle[attr] });
Object.assign(keyframes[0], { [attr]: fromStyle[attr] ?? inferDefaultValue(attr) });
Object.assign(keyframes[1], { [attr]: toStyle[attr] ?? inferDefaultValue(attr) });
});
const result = executeAnimation(target, preprocessKeyframes(keyframes), {
@ -90,4 +87,4 @@ export function executor(
mainResult,
results.filter((result) => result !== mainResult),
);
}
};

View File

@ -1,4 +1,4 @@
import type { IAnimationEffectTiming } from '@antv/g';
import type { DisplayObject, IAnimation, IAnimationEffectTiming } from '@antv/g';
import type { State } from '../types';
/**
@ -7,7 +7,7 @@ import type { State } from '../types';
*
* <en/> When it is a string, it will be obtained from the registered animation
*/
export type Animation = string | STDAnimation;
export type Animation = false | string | STDAnimation;
export type STDAnimation = ConfigurableAnimationOptions[];
@ -28,6 +28,16 @@ export interface AnimationContext {
* <en/> Used to set the style of shape to the source style before the animation is executed. For example, the move-to animation needs to set the x and y of shape to the source style
*/
originalStyle: Record<string, unknown>;
/**
* <zh/>
*
* <en/> Additional animation final state style
* @description
* <zh/> 0
*
* <en/> For example, before the element is destroyed, the final state opacity of the element needs to be set to 0
*/
modifiedStyle?: Record<string, unknown>;
/**
* <zh/>
*
@ -39,3 +49,10 @@ export interface AnimationContext {
export type AnimationEffectTiming = Partial<
Pick<IAnimationEffectTiming, 'duration' | 'delay' | 'easing' | 'iterations' | 'direction' | 'fill'>
>;
export type AnimationExecutor = (
shape: DisplayObject,
animation: Animation | false,
effectTiming: AnimationEffectTiming,
context: AnimationContext,
) => IAnimation | null;

View File

@ -0,0 +1,6 @@
export const enum GraphEvent {
/** 开始渲染 */
BEFORE_RENDER = 'beforerender',
/** 结束渲染 */
AFTER_RENDER = 'afterrender',
}

View File

@ -1 +1,2 @@
export * from './change';
export * from './events';

View File

@ -0,0 +1,11 @@
import { Line, Quadratic } from './edges';
import { Circle } from './nodes';
export const BUILT_IN_NODES = {
circle: Circle,
};
export const BUILT_IN_EDGES = {
line: Line,
quadratic: Quadratic,
};

View File

@ -8,12 +8,11 @@ import type {
} from '@antv/g';
import { Path } from '@antv/g';
import { deepMix, isFunction } from '@antv/util';
import type { PrefixObject } from '../../types';
import type { EdgeKey, EdgeLabelStyleProps } from '../../types/edge';
import type { EdgeKey, EdgeLabelStyleProps, PrefixObject } from '../../types';
import { getLabelPositionStyle } from '../../utils/edge';
import { omitStyleProps, subStyleProps } from '../../utils/prefix';
import type { SymbolFactor } from '../../utils/symbol';
import * as Symbol from '../../utils/symbol';
import { SymbolFactor } from '../../utils/symbol';
import type { LabelStyleProps } from '../shapes';
import { Label } from '../shapes';
import type { BaseShapeStyleProps } from '../shapes/base-shape';

View File

@ -1,7 +1 @@
/**
* <zh/>
*
* <en/> Built-in elements
*/
export {};
export { BUILT_IN_EDGES, BUILT_IN_NODES } from './constants';

View File

@ -1,7 +1,6 @@
import type { DisplayObjectConfig, CircleStyleProps as GCircleStyleProps, Group } from '@antv/g';
import { Circle as GCircle } from '@antv/g';
import type { PrefixObject } from '../../types';
import type { AnchorPosition, BadgePosition, LabelPosition } from '../../types/node';
import type { AnchorPosition, BadgePosition, LabelPosition, PrefixObject } from '../../types';
import { getAnchorPosition, getTextStyleByPosition, getXYByPosition } from '../../utils/element';
import { omitStyleProps, subStyleProps } from '../../utils/prefix';
import type { BadgeStyleProps, BaseShapeStyleProps, IconStyleProps, LabelStyleProps } from '../shapes';

View File

@ -0,0 +1,53 @@
/**
* <zh/>
*
* <en/> Built-in palettes
*/
export const BUILT_IN_PALETTES = {
spectral: [
'rgb(158, 1, 66)',
'rgb(213, 62, 79)',
'rgb(244, 109, 67)',
'rgb(253, 174, 97)',
'rgb(254, 224, 139)',
'rgb(255, 255, 191)',
'rgb(230, 245, 152)',
'rgb(171, 221, 164)',
'rgb(102, 194, 165)',
'rgb(50, 136, 189)',
'rgb(94, 79, 162)',
],
oranges: [
'rgb(255, 245, 235)',
'rgb(254, 230, 206)',
'rgb(253, 208, 162)',
'rgb(253, 174, 107)',
'rgb(253, 141, 60)',
'rgb(241, 105, 19)',
'rgb(217, 72, 1)',
'rgb(166, 54, 3)',
'rgb(127, 39, 4)',
],
greens: [
'rgb(247, 252, 245)',
'rgb(229, 245, 224)',
'rgb(199, 233, 192)',
'rgb(161, 217, 155)',
'rgb(116, 196, 118)',
'rgb(65, 171, 93)',
'rgb(35, 139, 69)',
'rgb(0, 109, 44)',
'rgb(0, 68, 27)',
],
blues: [
'rgb(247, 251, 255)',
'rgb(222, 235, 247)',
'rgb(198, 219, 239)',
'rgb(158, 202, 225)',
'rgb(107, 174, 214)',
'rgb(66, 146, 198)',
'rgb(33, 113, 181)',
'rgb(8, 81, 156)',
'rgb(8, 48, 107)',
],
};

View File

@ -1,7 +1 @@
/**
* <zh/>
*
* <en/> Built-in palettes
*/
export {};
export { BUILT_IN_PALETTES } from './constants';

View File

@ -1,6 +1,10 @@
import type { BUILT_IN_PALETTES } from './constants';
export type Palette = string | BuiltInPalette | CategoricalPalette | ContinuousPalette;
export type BuiltInPalette = 'category10' | 'category20';
export type STDPalette = CategoricalPalette | ContinuousPalette;
export type BuiltInPalette = keyof typeof BUILT_IN_PALETTES;
export type CategoricalPalette = string[];

View File

@ -1,12 +1,16 @@
import { BUILT_IN_ANIMATIONS } from '../animations';
import { BUILT_IN_EDGES, BUILT_IN_NODES } from '../elements';
import { BUILT_IN_PALETTES } from '../palettes';
import { BUILT_IN_THEMES } from '../themes';
export const BUILT_IN_PLUGINS = {
animation: BUILT_IN_ANIMATIONS,
behavior: {},
combo: {},
edge: {},
edge: BUILT_IN_EDGES,
layout: {},
node: {},
theme: {},
node: BUILT_IN_NODES,
palette: BUILT_IN_PALETTES,
theme: BUILT_IN_THEMES,
widget: {},
};

View File

@ -7,14 +7,15 @@ import type { PluginCategory, PluginRegistry } from './types';
* <en/> Plugin registry
*/
const PLUGIN_REGISTRY: PluginRegistry = {
node: {},
edge: {},
combo: {},
theme: {},
layout: {},
behavior: {},
widget: {},
animation: {},
behavior: {},
combo: {},
edge: {},
layout: {},
node: {},
palette: {},
theme: {},
widget: {},
};
/**

View File

@ -1,12 +1,13 @@
import type { DisplayObject } from '@antv/g';
import type { STDAnimation } from '../animations/types';
import type { BaseNode, BaseNodeStyleProps } from '../elements/nodes';
import type { STDPalette } from '../palettes/types';
import type { Theme } from '../themes/types';
// TODO 待使用正式类型定义 / To be used formal type definition
declare type Node = BaseNode<BaseNodeStyleProps<any>, DisplayObject>;
declare type Edge = unknown;
declare type Combo = unknown;
declare type Theme = unknown;
declare type Layout = unknown;
declare type Behavior = unknown;
declare type Widget = unknown;
@ -21,6 +22,7 @@ export interface PluginRegistry {
edge: Record<string, { new (...args: any[]): Edge }>;
combo: Record<string, { new (...args: any[]): Combo }>;
theme: Record<string, Theme>; // theme is a object options
palette: Record<string, STDPalette>;
layout: Record<string, { new (...args: any[]): Layout }>;
behavior: Record<string, { new (...args: any[]): Behavior }>;
widget: Record<string, { new (...args: any[]): Widget }>;

View File

@ -40,42 +40,57 @@ export class Canvas {
};
}
public renderers: Record<CanvasLayer, IRenderer>;
public renderers!: Record<CanvasLayer, IRenderer>;
constructor(config: CanvasConfig) {
this.config = config;
const { renderer: getRenderer, ...restConfig } = config;
const names: CanvasLayer[] = ['main', 'label', 'transient', 'transientLabel', 'background'];
}
const renderers = names.map((name) => {
const renderer = isFunction(getRenderer) ? getRenderer?.(name) : new CanvasRenderer();
public async init() {
const allCanvas = Object.entries(this.canvas);
renderer.registerPlugin(
new DragNDropPlugin({
isDocumentDraggable: true,
isDocumentDroppable: true,
dragstartDistanceThreshold: 10,
dragstartTimeThreshold: 100,
}),
if (allCanvas.every(([, canvas]) => !canvas)) {
const { renderer: getRenderer, ...restConfig } = this.config;
const names: CanvasLayer[] = ['main', 'label', 'transient', 'transientLabel', 'background'];
const { renderers, canvas } = names.reduce(
(acc, name) => {
const renderer = isFunction(getRenderer) ? getRenderer?.(name) : new CanvasRenderer();
renderer.registerPlugin(
new DragNDropPlugin({
isDocumentDraggable: true,
isDocumentDroppable: true,
dragstartDistanceThreshold: 10,
dragstartTimeThreshold: 100,
}),
);
if (name !== 'main') {
renderer.unregisterPlugin(renderer.getPlugin('dom-interaction'));
}
const canvas = new GCanvas({
renderer,
supportsMutipleCanvasesInOneContainer: true,
...restConfig,
});
acc.renderers[name] = renderer;
acc.canvas[name] = canvas;
this[name] = canvas;
return acc;
},
{ renderers: {}, canvas: {} } as {
renderers: Record<CanvasLayer, IRenderer>;
canvas: Record<CanvasLayer, GCanvas>;
},
);
if (name !== 'main') {
renderer.unregisterPlugin(renderer.getPlugin('dom-interaction'));
}
this.renderers = renderers;
this[name] = new GCanvas({
renderer,
supportsMutipleCanvasesInOneContainer: true,
...restConfig,
});
return [name, renderer];
});
this.renderers = Object.fromEntries(renderers);
this.init().then(() => {
Object.entries(this.canvas).forEach(([name, canvas]) => {
Object.entries(canvas).forEach(([name, canvas]) => {
const domElement = canvas.getContextService().getDomElement() as unknown as HTMLElement;
domElement.style.position = 'absolute';
@ -84,10 +99,8 @@ export class Canvas {
if (name !== 'main') domElement.style.pointerEvents = 'none';
});
});
}
}
public init() {
return Promise.all(Object.values(this.canvas).map((canvas) => canvas.ready));
}

View File

@ -1,7 +1,6 @@
import EventEmitter from '@antv/event-emitter';
import { Graph as GraphLib, ID } from '@antv/graphlib';
import { isEqual } from '@antv/util';
import { ChangeEvent, ChangeTypeEnum } from '../constants';
import { ChangeTypeEnum } from '../constants';
import type { ComboData, DataOptions, EdgeData, NodeData } from '../spec';
import type {
DataAdded,
@ -26,7 +25,9 @@ import { dfs } from '../utils/traverse';
const COMBO_KEY = 'combo';
export class DataController extends EventEmitter {
const TREE_KEY = 'tree';
export class DataController {
public model: GraphlibData;
/**
@ -59,7 +60,6 @@ export class DataController extends EventEmitter {
private batchCount = 0;
constructor() {
super();
this.model = new GraphLib();
}
@ -78,14 +78,22 @@ export class DataController extends EventEmitter {
}
}
/**
* <zh/> [] API Element Controller
*
* <en/> [WARNING] This API is only for Element Controller
* @returns <zh/> | <en/> data changes
*/
public getChanges(): DataChange[] {
const changes = this.changes;
this.changes = [];
return changes;
}
public batch(callback: () => void) {
this.batchCount++;
this.model.batch(callback);
this.batchCount--;
if (this.batchCount === 0) {
this.emit(ChangeEvent.CHANGE, [...this.changes]);
this.changes = [];
}
}
public isCombo(id: ID) {
@ -128,6 +136,22 @@ export class DataController extends EventEmitter {
}, [] as ComboData[]);
}
/**
* <zh/>
*
* <en/> Get the child node data
* @param id - <zh/> ID | <en/> node ID
* @returns <zh/> | <en/> child node data
* @description
* <zh/>
*
* <en/> Only valid in tree graph
*/
public getChildrenData(id: ID): NodeData[] {
if (!this.model.hasNode(id)) return [];
return this.model.getChildren(id, TREE_KEY).map((node) => node.data);
}
/**
* <zh/> ID
*
@ -166,24 +190,6 @@ export class DataController extends EventEmitter {
}, [] as NodeLikeData[]);
}
/**
* <zh/> combo
*
* <en/> Classify node and combo data
* @param data - <zh/> | <en/> data to be classified
* @returns <zh/> combo | <en/> node and combo data
*/
public classifyNodeLikeData(data: NodeLikeData[]) {
return data.reduce(
(acc, item) => {
if (this.isCombo(idOf(item))) acc.combos.push(item);
else acc.nodes.push(item);
return acc;
},
{ nodes: [] as NodeData[], combos: [] as ComboData[] },
);
}
public hasNode(id: ID) {
return this.model.hasNode(id) && !this.isCombo(id);
}
@ -467,6 +473,11 @@ export class DataController extends EventEmitter {
if (!ids.length) return;
this.batch(() => {
ids.forEach((id) => {
// 移除关联边、子节点
// remove related edges and child nodes
this.removeEdgeData(this.getRelatedEdgesData(id).map(idOf));
// TODO 树图情况下移除子节点
this.pushChange({ value: this.getNodeData([id])[0], type: ChangeTypeEnum.NodeRemoved });
this.removeNodeLikeHierarchy(id);
});

View File

@ -0,0 +1,668 @@
/* eslint-disable jsdoc/require-returns */
/* eslint-disable jsdoc/require-param */
import type { DisplayObject, IAnimation } from '@antv/g';
import { Group } from '@antv/g';
import type { ID } from '@antv/graphlib';
import { groupBy } from '@antv/util';
import { executor as animationExecutor } from '../animations';
import { ChangeTypeEnum, GraphEvent } from '../constants';
import { BaseNode } from '../elements/nodes';
import type { BaseShape } from '../elements/shapes';
import { getPlugin } from '../registry';
import type { ComboData, DataOptions, EdgeData, G6Spec, NodeData } from '../spec';
import type { AnimationStage } from '../spec/element/animation';
import type { EdgeStyle } from '../spec/element/edge';
import type { NodeLikeStyle } from '../spec/element/node';
import type { DataChange, ElementData, ElementDatum, ElementType, State, StyleIterationContext } from '../types';
import { createAnimationsProxy } from '../utils/animation';
import { reduceDataChanges } from '../utils/change';
import { idOf } from '../utils/id';
import { assignColorByPalette, parsePalette } from '../utils/palette';
import { computeElementCallbackStyle } from '../utils/style';
import type { RuntimeContext } from './types';
type AnimationExecutor = (
id: ID,
shape: DisplayObject,
originalStyle: Record<string, unknown>,
modifiedStyle?: Record<string, unknown>,
) => IAnimation | null;
type RenderContext = {
taskId: TaskID;
animate?: AnimationExecutor;
};
type TaskID = number;
export class ElementController {
private context: RuntimeContext;
private container!: {
node: Group;
edge: Group;
combo: Group;
};
private elementMap: Record<ID, DisplayObject> = {};
private shapeTypeMap: Record<ID, string> = {};
private taskIdCounter = 0;
/**
* <zh/> id
*
* <en/> Get render task id
*/
private getTaskId() {
return this.taskIdCounter++;
}
private postRenderTasks: Record<TaskID, (() => Promise<void>)[]> = {};
private getTasks(taskId: TaskID) {
return this.postRenderTasks[taskId] || [];
}
private animationMap: Record<TaskID, Record<ID, IAnimation>> = {};
constructor(context: RuntimeContext) {
this.context = context;
this.setElementStates(context.options.data);
}
public async init() {
const { canvas } = this.context;
if (!this.container) {
await canvas.init();
this.container = {
node: canvas.appendChild(new Group({ style: { zIndex: 2 } })),
edge: canvas.appendChild(new Group({ style: { zIndex: 1 } })),
combo: canvas.appendChild(new Group({ style: { zIndex: 0 } })),
};
}
}
private emit(event: GraphEvent, payload?: any) {
const { graph } = this.context;
graph.emit(event, payload);
}
private getElementData(elementType: ElementType, ids?: ID[]) {
const { dataController } = this.context;
switch (elementType) {
case 'node':
return dataController.getNodeData(ids);
case 'edge':
return dataController.getEdgeData(ids);
case 'combo':
return dataController.getComboData(ids);
default:
return [];
}
}
private forEachElementData(callback: (elementType: ElementType, elementData: ElementData) => void) {
const elementTypes: ElementType[] = ['node', 'edge', 'combo'];
elementTypes.forEach((elementType) => {
const elementData = this.getElementData(elementType);
callback(elementType, elementData);
});
}
private getTheme(elementType: ElementType) {
const { theme } = this.context.options;
if (!theme) return {};
const themeConfig = getPlugin('theme', theme);
return themeConfig?.[elementType] || {};
}
public getThemeStyle(elementType: ElementType) {
return this.getTheme(elementType).style || {};
}
public getThemeStateStyle(elementType: ElementType, states: State[]) {
const { state = {} } = this.getTheme(elementType);
return Object.assign({}, ...states.map((name) => state[name] || {}));
}
private paletteStyle: Record<ID, string> = {};
private computePaletteStyle() {
const { options } = this.context;
this.paletteStyle = {};
this.forEachElementData((elementType, elementData) => {
const palette = parsePalette(this.getTheme(elementType)?.palette || options[elementType]?.palette);
if (palette) {
Object.assign(this.paletteStyle, assignColorByPalette(elementData, palette));
}
});
}
public getPaletteStyle(id: ID) {
return {
keyShapeColor: this.paletteStyle[id],
};
}
public getDataStyle(elementType: ElementType, id: ID): NodeLikeStyle | EdgeStyle {
const datum = this.getElementData(elementType, [id])?.[0];
if (!datum) return {};
// `data.style` 中一些样式例如 parentId, collapsed, type 并非直接给元素使用,因此需要过滤掉这些字段
// Some styles in `data.style`, such as parentId, collapsed, type, are not directly used by the element, so these fields need to be filtered out
const { parentId, collapsed, type, states, ...style } = datum.style || {};
return style;
}
private defaultStyle: Record<ID, Record<string, unknown>> = {};
/**
* <zh/>
*
* <en/> compute default style of single element
*/
private computedElementDefaultStyle(elementType: ElementType, context: StyleIterationContext) {
const { options } = this.context;
const defaultStyle = options[elementType]?.style || {};
this.defaultStyle[idOf(context.datum)] = computeElementCallbackStyle(defaultStyle, context);
}
private computeElementsDefaultStyle() {
this.forEachElementData((elementType, elementData) => {
elementData.forEach((datum, index) => {
this.computedElementDefaultStyle(elementType, { datum, index, elementData });
});
});
}
public getDefaultStyle(id: ID) {
return this.defaultStyle[id] || {};
}
private elementState: Record<ID, State[]> = {};
/**
* <zh/>
*
* <en/> Initialize element state from data
*/
private setElementStates(data: G6Spec['data']) {
const { nodes = [], edges = [], combos = [] } = data || {};
[...nodes, ...edges, ...combos].forEach((elementData) => {
const states = elementData.style?.states || [];
const id = idOf(elementData);
this.elementState[id] = states;
});
}
/**
* <zh/>
*
* <en/> Get the state of the specified element
* @param id - <zh/> id | <en/> element id
* @returns <zh/> | <en/> element state array
*/
public getElementStates(id: ID): State[] {
return this.elementState[id] || [];
}
private stateStyle: Record<ID, Record<string, unknown>> = {};
/**
* <zh/>
*
* <en/> get single state style of single element
*/
private getElementStateStyle(elementType: ElementType, state: State, context: StyleIterationContext) {
const { options } = this.context;
const stateStyle = options[elementType]?.state?.[state] || {};
return computeElementCallbackStyle(stateStyle, context);
}
/**
* <zh/>
*
* <en/> compute merged state style of single element
*/
private computeElementStatesStyle(elementType: ElementType, states: State[], context: StyleIterationContext) {
this.stateStyle[idOf(context.datum)] = Object.assign(
{},
...states.map((state) => this.getElementStateStyle(elementType, state, context)),
);
}
/**
* <zh/>
*
* <en/> compute state style of all elements
* @param ids - <zh/> | <en/> compute state style of specified elements
*/
private computeElementsStatesStyle(ids?: ID[]) {
this.forEachElementData((elementType, elementData) => {
elementData.forEach((datum, index) => {
const id = idOf(datum);
if ((ids && ids.includes(id)) || ids === undefined) {
const states = this.getElementStates(id);
this.computeElementStatesStyle(elementType, states, { datum, index, elementData });
}
});
});
}
public getStateStyle(id: ID) {
return this.stateStyle[id] || {};
}
private computeStyle() {
this.computePaletteStyle();
this.computeElementsDefaultStyle();
this.computeElementsStatesStyle();
}
private getElement<T extends DisplayObject = BaseShape<any>>(id: ID): T | undefined {
return this.elementMap[id] as T;
}
private getAnimationExecutor(elementType: ElementType, stage: AnimationStage): AnimationExecutor {
const { options } = this.context;
const getAnimation = () => {
const userDefined = options?.[elementType]?.animation;
if (userDefined === false) return false;
const userDefinedStage = userDefined?.[stage];
if (userDefinedStage) return userDefinedStage;
const themeDefined = this.getTheme(elementType)?.animation;
if (themeDefined === false) return false;
const themeDefinedStage = themeDefined?.[stage];
return themeDefinedStage ?? false;
};
return (
id: ID,
shape: DisplayObject,
originalStyle: Record<string, unknown>,
modifiedStyle?: Record<string, unknown>,
) => {
return animationExecutor(
shape,
getAnimation(),
{},
{
originalStyle,
modifiedStyle,
states: this.getElementStates(id),
},
);
};
}
/**
* <zh/>
*
* <en/> Get elements with the specified state
* @param state - <zh/> | <en/> state or state array
* @returns <zh/> id | <en/> key-value pairs of element id and element instance
*/
public getElementsByState(state: State | State[]): Record<string, DisplayObject> {
return Object.fromEntries(
Object.entries(this.elementState)
.filter(([, states]) => {
return (Array.isArray(state) ? state : [state]).every((s) => states.includes(s));
})
.map(([id]) => [id, this.elementMap[id]]),
);
}
/**
* <zh/>
*
* <en/> Get edge end context
* @param id - <zh/> id | <en/> edge id
* @returns <zh/> | <en/> edge end context
* @description
* <zh/>
*
* <en/> Only the most basic node instances and connection point position information are provided, and more context information needs to be calculated in the edge element
*/
private getEdgeEndsContext(id: ID) {
const { dataController } = this.context;
const data = dataController.getEdgeData([id])?.[0];
if (!data) return {};
const { source, target } = data;
const sourceNode = this.getElement<BaseNode<any, any>>(source);
const targetNode = this.getElement<BaseNode<any, any>>(target);
const sourcePoint = sourceNode?.getBounds().center || [0, 0, 0];
const targetPoint = targetNode?.getBounds().center || [0, 0, 0];
return {
sourcePoint,
targetPoint,
sourceNode,
targetNode,
};
}
private getComboChildren(id: ID) {
const { dataController } = this.context;
return Object.fromEntries(
dataController.getComboChildrenData(id).map((datum) => [idOf(datum), this.getElement(idOf(datum))]),
);
}
public getElementComputedStyle(elementType: ElementType, id: ID) {
// 优先级(从低到高) Priority (from low to high):
const themeStyle = this.getThemeStyle(elementType);
const paletteStyle = this.getPaletteStyle(id);
const dataStyle = this.getDataStyle(elementType, id);
const defaultStyle = this.getDefaultStyle(id);
const themeStateStyle = this.getThemeStateStyle(elementType, this.getElementStates(id));
const stateStyle = this.getStateStyle(id);
const style = Object.assign({}, themeStyle, paletteStyle, dataStyle, defaultStyle, themeStateStyle, stateStyle);
if (elementType === 'edge') {
Object.assign(style, this.getEdgeEndsContext(id));
} else if (elementType === 'combo') {
Object.assign(style, {
children: this.getComboChildren(id),
});
}
return style;
}
// ---------- Render API ----------
/**
* <zh/>
*
* <en/> start render process
*/
public async render(context: RuntimeContext): Promise<IAnimation | null> {
this.context = context;
const { dataController } = context;
const tasks = reduceDataChanges(dataController.getChanges());
if (tasks.length === 0) return null;
this.emit(GraphEvent.BEFORE_RENDER);
await this.init();
const {
NodeAdded = [],
NodeUpdated = [],
NodeRemoved = [],
EdgeAdded = [],
EdgeUpdated = [],
EdgeRemoved = [],
ComboAdded = [],
ComboUpdated = [],
ComboRemoved = [],
} = groupBy(tasks, (change) => change.type) as unknown as Record<`${ChangeTypeEnum}`, DataChange[]>;
const dataOf = <T extends DataChange['value']>(data: DataChange[]) => data.map((datum) => datum.value) as T[];
// 计算要新增的元素 / compute elements to add
const nodesToAdd = dataOf<NodeData>(NodeAdded);
const edgesToAdd = dataOf<EdgeData>(EdgeAdded);
const combosToAdd = dataOf<ComboData>(ComboAdded);
// 计算要更新的元素 / compute elements to update
const nodesToUpdate = dataOf<NodeData>(NodeUpdated);
const edgesToUpdate = dataOf<EdgeData>(EdgeUpdated);
const combosToUpdate = dataOf<ComboData>(ComboUpdated);
this.setElementStates({ nodes: nodesToUpdate, edges: edgesToUpdate, combos: combosToUpdate });
// 计算要删除的元素 / compute elements to remove
const nodesToRemove = dataOf<NodeData>(NodeRemoved);
const edgesToRemove = dataOf<EdgeData>(EdgeRemoved);
const combosToRemove = dataOf<ComboData>(ComboRemoved);
// 如果更新了节点,需要更新连接的边和所处的 combo
// If the node is updated, the connected edge and the combo it is in need to be updated
// TODO 待优化,仅考虑影响边更新的属性,如 x, y, size 等
nodesToUpdate
.map((node) => dataController.getRelatedEdgesData(idOf(node)))
.flat()
.forEach((edge) => {
if (!edgesToUpdate.find((item) => idOf(item) === idOf(edge))) edgesToUpdate.push(edge);
});
dataController
.getComboData(
[...nodesToUpdate, ...nodesToRemove, ...combosToUpdate, ...combosToRemove].reduce((acc, curr) => {
const parentId = curr?.style?.parentId;
if (parentId) acc.push(parentId);
return acc;
}, [] as ID[]),
)
.forEach((combo) => {
if (!combosToUpdate.find((item) => item.id === combo.id)) combosToUpdate.push(combo);
});
// 重新计算样式 / Recalculate style
this.computeStyle();
// 创建渲染任务 / Create render task
const taskId = this.getTaskId();
this.postRenderTasks[taskId] = [];
this.animationMap[taskId] = {};
this.destroyElements({ nodes: nodesToRemove, edges: edgesToRemove, combos: combosToRemove }, { taskId });
this.createElements({ nodes: nodesToAdd, edges: edgesToAdd, combos: combosToAdd }, { taskId });
this.updateElements({ nodes: nodesToUpdate, edges: edgesToUpdate, combos: combosToUpdate }, { taskId });
return this.postRender(taskId);
}
private postRender(taskId: TaskID) {
const tasks = this.getTasks(taskId);
// 执行后续任务 / Execute subsequent tasks
Promise.all(tasks.map((task) => task())).then(() => {
delete this.postRenderTasks[taskId];
delete this.animationMap[taskId];
});
const getRenderResult = (taskId: TaskID): IAnimation | null => {
const [source, ...target] = Object.values(this.animationMap[taskId]);
if (source) return createAnimationsProxy(source, target);
return null;
};
const result = getRenderResult(taskId);
// 触发成事件 / Trigger event
if (result) result.onfinish = () => this.emit(GraphEvent.AFTER_RENDER);
else this.emit(GraphEvent.AFTER_RENDER);
return result;
}
private getShapeType(elementType: ElementType, datum: ElementDatum) {
const type = datum?.style?.type;
if (type) return type;
// 推断默认类型 / Infer default type
return {
node: 'circle',
edge: 'line',
combo: 'circle',
}[elementType];
}
private createElement(elementType: ElementType, datum: ElementDatum, context: RenderContext) {
const { animate, taskId } = context;
const id = idOf(datum);
const currentShape = this.getElement(id);
if (currentShape) return;
// get shape constructor
const shapeType = this.getShapeType(elementType, datum);
const Ctor = getPlugin(elementType, shapeType);
if (!Ctor) return;
const shape = this.container[elementType].appendChild(
// @ts-expect-error TODO fix type
new Ctor({
style: {
context: this.context,
...this.getElementComputedStyle(elementType, id),
},
}),
) as DisplayObject;
this.shapeTypeMap[id] = shapeType;
const tasks = this.getTasks(taskId);
tasks.push(async () => {
const result = animate?.(id, shape, { ...shape.attributes, opacity: 0 });
if (result) {
this.animationMap[taskId][id] = result;
await result.finished;
}
});
this.elementMap[id] = shape;
}
private createElements(data: DataOptions, context: RenderContext) {
// 新增相应的元素数据
// 重新计算色板样式
const { nodes = [], edges = [], combos = [] } = data;
const iteration: [ElementType, ElementData][] = [
['node', nodes],
['edge', edges],
['combo', combos],
];
iteration.forEach(([elementType, elementData]) => {
if (elementData.length === 0) return;
const animate = this.getAnimationExecutor(elementType, 'enter');
elementData.forEach((datum) => this.createElement(elementType, datum, { ...context, animate }));
});
}
private async updateElement(elementType: ElementType, datum: ElementDatum, context: RenderContext) {
const { animate, taskId } = context;
this.handleTypeChange(elementType, datum, context);
const id = idOf(datum);
const shape = this.getElement(id);
if (!shape) return;
const style = this.getElementComputedStyle(elementType, id);
const originalStyle = { ...shape.attributes };
if ('update' in shape) shape.update(style);
else (shape as DisplayObject).attr(style);
const tasks = this.getTasks(taskId);
tasks.push(async () => {
const result = animate?.(id, shape, originalStyle);
if (result) {
this.animationMap[taskId][id] = result;
await result?.finished;
}
});
}
private updateElements(data: DataOptions, context: RenderContext) {
const { nodes = [], edges = [], combos = [] } = data;
const iteration: [ElementType, ElementData][] = [
['node', nodes],
['edge', edges],
['combo', combos],
];
iteration.forEach(([elementType, elementData]) => {
if (elementData.length === 0) return;
const animate = this.getAnimationExecutor(elementType, 'update');
elementData.forEach((datum) => this.updateElement(elementType, datum, { ...context, animate }));
});
}
/**
* <zh/>
*
* <en/> handle element type change
* @description
* <zh/>
*
* <en/> Destroy the original shape instance and recreate the shape instance
*/
private handleTypeChange(elementType: ElementType, datum: ElementDatum, context: RenderContext) {
const id = idOf(datum);
const originalShapeType = this.shapeTypeMap[id];
const modifiedShapeType = this.getShapeType(elementType, datum);
if (originalShapeType && originalShapeType !== modifiedShapeType) {
this.destroyElement(datum, context);
this.createElement(elementType, datum, context);
}
}
protected destroyElement(datum: ElementDatum, context: RenderContext) {
const { animate, taskId } = context;
const id = idOf(datum);
const element = this.elementMap[id];
if (!element) return;
const tasks = this.getTasks(taskId);
tasks.push(async () => {
const result = animate?.(id, element, { ...element.attributes }, { opacity: 0 });
if (result) {
this.animationMap[taskId][id] = result;
result.onfinish = () => element.destroy();
} else {
element.destroy();
}
});
}
protected destroyElements(data: DataOptions, context: RenderContext) {
const { nodes = [], edges = [], combos = [] } = data;
const iteration: [ElementType, ElementData][] = [
['combo', combos],
['edge', edges],
['node', nodes],
];
// 移除相应的元素数据
// 重新计算色板样式,如果是分组色板,则不需要重新计算
iteration.forEach(([elementType, elementData]) => {
if (elementData.length === 0) return;
const animate = this.getAnimationExecutor(elementType, 'exit');
elementData.forEach((datum) => this.destroyElement(datum, { ...context, animate }));
this.clearElement(elementData.map(idOf));
});
}
private clearElement(ids: ID[]) {
ids.forEach((id) => {
delete this.paletteStyle[id];
delete this.defaultStyle[id];
delete this.stateStyle[id];
delete this.elementState[id];
delete this.elementMap[id];
delete this.shapeTypeMap[id];
});
}
}

View File

@ -0,0 +1,11 @@
import type { G6Spec } from '../spec';
import type { Canvas } from './canvas';
import type { DataController } from './data';
import type { Graph } from './graph';
export interface RuntimeContext {
canvas: Canvas;
graph: Graph;
options: G6Spec;
dataController: DataController;
}

View File

@ -48,7 +48,20 @@ interface NodeLikeDataStyle extends BaseElementStyle, NodeLikeStyle {
parentId?: ID;
}
interface EdgeDataStyle extends BaseElementStyle, EdgeStyle {}
interface EdgeDataStyle extends BaseElementStyle, EdgeStyle {
/**
* <zh/> id
*
* <en/> source port id
*/
sourceAnchor?: string;
/**
* <zh/> id
*
* <en/> target port id
*/
targetAnchor?: string;
}
interface BaseElementStyle {
/**

View File

@ -1,10 +1,12 @@
import type { Animation } from '../../animations/types';
export type AnimationOptions = {
[STAGE in AnimationStage]?: Animation;
} & {
[key: string]: Animation;
};
export type AnimationOptions =
| false
| ({
[STAGE in AnimationStage]?: Animation;
} & {
[key: string]: Animation;
});
/**
* <zh/>

View File

@ -39,7 +39,7 @@ export type EdgeOptions = {
export type StaticEdgeOptions = {
style?: EdgeStyle;
state?: Record<string, EdgeStyle>;
animation?: PaletteOptions;
animation?: AnimationOptions;
palette?: PaletteOptions;
};

View File

@ -0,0 +1,36 @@
import { Theme } from './types';
export const DARK_THEME: Theme = {
node: {
style: {
fill: '#444',
stroke: '#f8f8f8',
},
state: {},
animation: {
enter: 'fade',
update: [{ fields: ['cx', 'cy'] }],
exit: 'fade',
},
},
edge: {
style: {
lineWidth: 1,
stroke: '#8b9baf',
},
state: {},
animation: {
enter: 'fade',
update: [{ fields: ['sourcePoint', 'targetPoint'] }],
exit: 'fade',
},
},
combo: {
style: {
fill: 'rgba(170, 174, 178, 0.2)',
stroke: '#aaaeb2',
lineWidth: 1,
},
state: {},
},
};

View File

@ -1,7 +1,12 @@
import { DARK_THEME } from './dark';
import { LIGHT_THEME } from './light';
/**
* <zh/>
*
* <en/> Built-in themes
*/
export {};
export const BUILT_IN_THEMES = {
light: LIGHT_THEME,
dark: DARK_THEME,
};

View File

@ -0,0 +1,36 @@
import { Theme } from './types';
export const LIGHT_THEME: Theme = {
node: {
style: {
fill: '#f8f8f8',
stroke: '#8b9baf',
},
state: {},
animation: {
enter: 'fade',
update: [{ fields: ['cx', 'cy'] }],
exit: 'fade',
},
},
edge: {
style: {
lineWidth: 1,
stroke: '#8b9baf',
},
state: {},
animation: {
enter: 'fade',
update: [{ fields: ['sourcePoint', 'targetPoint'] }],
exit: 'fade',
},
},
combo: {
style: {
fill: 'rgba(170, 174, 178, 0.2)',
stroke: '#aaaeb2',
lineWidth: 1,
},
state: {},
},
};

View File

@ -1 +1,11 @@
import type { StaticComboOptions } from '../spec/element/combo';
import type { StaticEdgeOptions } from '../spec/element/edge';
import type { StaticNodeOptions } from '../spec/element/node';
export type BuiltInTheme = 'light' | 'dark';
export type Theme = {
node?: StaticNodeOptions;
edge?: StaticEdgeOptions;
combo?: StaticComboOptions;
};

View File

@ -11,7 +11,9 @@ export type DataID = {
export type NodeLikeData = NodeData | ComboData;
export type ElementData = NodeData | EdgeData | ComboData;
export type ElementDatum = NodeData | EdgeData | ComboData;
export type ElementData = NodeData[] | EdgeData[] | ComboData[];
/**
* <zh/>

View File

@ -1 +1,5 @@
import type { ComboOptions, EdgeOptions, NodeOptions } from '../spec';
export type ElementType = 'node' | 'edge' | 'combo';
export type ElementOptions = NodeOptions | EdgeOptions | ComboOptions;

View File

@ -0,0 +1,28 @@
import type {
EdgeAdded,
EdgeDataUpdated,
EdgeRemoved,
EdgeUpdated,
NodeAdded,
NodeDataUpdated,
NodeRemoved,
TreeStructureAttached,
TreeStructureChanged,
TreeStructureDetached,
} from '@antv/graphlib';
import type { EdgeData } from '../spec';
import type { NodeLikeData } from './data';
export type GraphLibGroupedChanges = {
NodeRemoved: NodeRemoved<NodeLikeData>[];
EdgeRemoved: EdgeRemoved<EdgeData>[];
NodeAdded: NodeAdded<NodeLikeData>[];
EdgeAdded: EdgeAdded<EdgeData>[];
NodeDataUpdated: NodeDataUpdated<NodeLikeData>[];
EdgeUpdated: EdgeUpdated<EdgeData>[];
EdgeDataUpdated: EdgeDataUpdated<EdgeData>[];
TreeStructureChanged: TreeStructureChanged[];
ComboStructureChanged: TreeStructureChanged[];
TreeStructureAttached: TreeStructureAttached[];
TreeStructureDetached: TreeStructureDetached[];
};

View File

@ -2,8 +2,13 @@ export type * from './callable';
export type * from './canvas';
export type * from './change';
export type * from './data';
export type * from './edge';
export type * from './element';
export type * from './graphlib';
export type * from './node';
export type * from './padding';
export type * from './point';
export type * from './prefix';
export type * from './state';
export type * from './style';
export type * from './vector';

View File

@ -0,0 +1,12 @@
import type { ElementData, ElementDatum } from './data';
/**
* <zh/>
*
* <en/> Style iteration context
*/
export type StyleIterationContext = {
datum: ElementDatum;
index: number;
elementData: ElementData;
};

View File

@ -1,7 +1,5 @@
import type { DisplayObject, IAnimation } from '@antv/g';
import { isNil, isString } from '@antv/util';
import type { Animation, STDAnimation } from '../animations/types';
import { getPlugin } from '../registry';
import { isNil } from '@antv/util';
import { getDescendantShapes } from './shape';
/**
@ -24,26 +22,18 @@ export function createAnimationsProxy(sourceAnimation: IAnimation, targetAnimati
return Reflect.get(target, propKey);
},
set(target, propKey: keyof IAnimation, value) {
targetAnimations.forEach((animation) => ((animation[propKey] as any) = value));
// onframe 和 onfinish 特殊处理,不用同步到所有动画实例上
// onframe and onfinish are specially processed and do not need to be synchronized to all animation instances
if (!['onframe', 'onfinish'].includes(propKey)) {
targetAnimations.forEach((animation) => {
(animation[propKey] as any) = value;
});
}
return Reflect.set(target, propKey, value);
},
});
}
/**
* <zh/>
*
* <en/> parse animation options
* @param animation - <zh/> | <en/> animation options
* @returns <zh/> | <en/> animation options
*/
export function parseAnimation(animation: Animation): STDAnimation {
if (isString(animation)) {
return getPlugin('animation', animation) || [];
}
return animation;
}
/**
* <zh/>
*
@ -132,3 +122,23 @@ export function executeAnimation<T extends DisplayObject>(
const descendantAnimations = descendants.map((descendant) => descendant.animate(inheritAttrsKeyframes, options)!);
return createAnimationsProxy(keyShapeAnimation!, descendantAnimations);
}
/**
* <zh/>
*
* <en/> Get default value of attribute
* @param name - <zh/> | <en/> Attribute name
* @returns <zh/> | <en/> Attribute default value
* @description
* <zh/> G
*
* <en/> During the animation, some attributes do not explicitly specify the attribute value, but in fact there is an attribute value in G, so use this method to get the actual default value
*/
export function inferDefaultValue(name: string) {
switch (name) {
case 'opacity':
return 1;
default:
return undefined;
}
}

View File

@ -1,6 +1,7 @@
import type { Edge, Node } from '@antv/graphlib';
import type { ComboData, EdgeData, NodeData } from '../spec';
import { NodeLikeData } from '../types/data';
import { idOf } from './id';
import { isEdgeData } from './is';
export function toGraphlibData(datums: EdgeData): Edge<EdgeData>;
@ -18,9 +19,10 @@ export function toGraphlibData(data: NodeData | EdgeData | ComboData): Node<Node
return {
...rest,
data,
id: idOf(data),
} as Edge<EdgeData>;
}
return { id: data.id, data } as Node<NodeLikeData>;
return { id: idOf(data), data } as Node<NodeLikeData>;
}
export function toG6Data<T extends EdgeData>(data: Edge<T>): T;

View File

@ -1,5 +1,5 @@
import type { EdgeData } from '../spec';
import type { ElementData } from '../types/data';
import type { ElementDatum } from '../types';
/**
* <zh/>
@ -8,7 +8,7 @@ import type { ElementData } from '../types/data';
* @param data - <zh/> | <en/> element data
* @returns - <zh/> | <en/> whether the data is edge data
*/
export function isEdgeData(data: Partial<ElementData>): data is EdgeData {
export function isEdgeData(data: Partial<ElementDatum>): data is EdgeData {
if ('source' in data && 'target' in data) return true;
return false;
}

View File

@ -0,0 +1,101 @@
import type { ID } from '@antv/graphlib';
import { groupBy, isFunction, isNumber, isString } from '@antv/util';
import { getPlugin } from '../registry';
import type { PaletteOptions, STDPaletteOptions } from '../spec/element/palette';
import type { ElementData, ElementDatum } from '../types/data';
import { idOf } from './id';
/**
* <zh/>
*
* <en/> Parse palette options
* @param palette - <zh/> | <en/> PaletteOptions options
* @returns <zh/> | <en/> Standard palette options
*/
export function parsePalette(palette?: PaletteOptions): STDPaletteOptions | undefined {
if (!palette) return undefined;
if (
// 色板名 palette name
typeof palette === 'string' ||
// 插值函数 interpolate function
typeof palette === 'function' ||
// 颜色数组 color array
Array.isArray(palette)
) {
// 默认为离散色板,默认分组字段为 id
// Default to discrete palette, default group field is id
return {
type: 'group',
color: palette,
};
}
return palette;
}
/**
* <zh/>
*
* <en/> Assign colors according to the palette
* @param data - <zh/> | <en/> Element data
* @param palette - <zh/> | <en/> PaletteOptions options
* @returns <zh/> | <en/> Element color
* @description
* <zh/> id key value
*
* <en/> The return value is an object with element id as key and color value as value
*/
export function assignColorByPalette(data: ElementData, palette?: STDPaletteOptions) {
if (!palette) return {};
const { type, color: colorPalette, field, invert } = palette;
const assignColor = (args: [ID, number][]): Record<ID, string> => {
const palette = isString(colorPalette) ? getPlugin('palette', colorPalette) : colorPalette;
if (isFunction(palette)) {
// assign by continuous
return Object.fromEntries(args.map(([groupKey, value]) => [groupKey, palette(invert ? 1 - value : value)]));
} else if (Array.isArray(palette)) {
// assign by discrete
const colors = invert ? [...palette].reverse() : palette;
return Object.fromEntries(args.map(([id, index]) => [id, colors[index % palette.length]]));
}
return {};
};
if (type === 'group') {
// @ts-expect-error @antv/util groupBy condition 参数应当支持返回 string 或者 number / groupBy condition parameter should support return string or number
const groupData = groupBy<ElementDatum>(data, (datum) => {
if (!datum.data || !field) {
return idOf(datum);
}
return String(datum.data[field]);
});
const groupKeys = Object.keys(groupData);
const assignResult = assignColor(groupKeys.map((key, index) => [key, index]));
const result: Record<ID, string> = {};
Object.entries(groupData).forEach(([groupKey, groupData]) => {
groupData.forEach((datum) => {
result[idOf(datum)] = assignResult[groupKey];
});
});
return result;
} else {
const [min, max] = data.reduce(
([min, max], datum) => {
const value = datum?.data?.[field];
if (!isNumber(value)) throw new Error(`Palette field ${field} is not a number`);
return [Math.min(min, value), Math.max(max, value)];
},
[Infinity, -Infinity],
);
const range = max - min;
return assignColor(
data.map((datum) => [datum.id, ((datum?.data?.[field] as number) - min) / range]) as [ID, number][],
);
}
}

View File

@ -0,0 +1,23 @@
import { isFunction } from '@antv/util';
import type { CallableObject, ElementData, ElementDatum, StyleIterationContext } from '../types';
/**
* <zh/>
*
* <en/> compute dynamic style that supports callback
* @param callableStyle - <zh/> | <en/> dynamic style
* @param context - <zh/> | <en/> style iteration context
* @returns <zh/> | <en/> static style
*/
export function computeElementCallbackStyle(
callableStyle: CallableObject<Record<string, unknown>, [ElementDatum, number, ElementData]>,
context: StyleIterationContext,
) {
const { datum, index, elementData } = context;
return Object.fromEntries(
Object.entries(callableStyle).map(([key, style]) => {
if (isFunction(style)) return [key, style(datum, index, elementData)];
return [key, style];
}),
);
}