feat: v5 donut (#4846)

Co-authored-by: yvonneyx <banxuan.zyx@antgroup.com>
This commit is contained in:
yvonneyx 2023-08-24 12:37:13 +08:00 committed by GitHub
parent fcd25b36ed
commit c09593aa2c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 792 additions and 11 deletions

View File

@ -6,6 +6,7 @@ export const RESERVED_SHAPE_IDS = [
'haloShape',
'anchorShapes',
'badgeShapes',
'donutShapes',
];
export const OTHER_SHAPES_FIELD_NAME = 'otherShapes';

View File

@ -277,17 +277,19 @@ export class ItemController {
);
}
// collapse the sub tree which has 'collapsed' in initial data
const collapseNodes = [];
graphCoreTreeDfs(
graphCore,
graphCore.getRoots('tree'),
(child) => {
if (child.data.collapsed) collapseNodes.push(child);
},
'BT',
'tree',
);
this.collapseSubTree(collapseNodes, graphCore, false);
if (graphCore.hasTreeStructure('tree')) {
const collapseNodes = [];
graphCoreTreeDfs(
graphCore,
graphCore.getRoots('tree'),
(child) => {
if (child.data.collapsed) collapseNodes.push(child);
},
'BT',
'tree',
);
this.collapseSubTree(collapseNodes, graphCore, false);
}
}
/**

View File

@ -17,6 +17,7 @@ import {
SphereNode,
TriangleNode,
HexagonNode,
DonutNode,
} from './item/node';
import DarkTheme from './theme/dark';
import LightTheme from './theme/light';
@ -96,6 +97,7 @@ const stdLib = {
'hexagon-node': HexagonNode,
'triangle-node': TriangleNode,
'ellipse-node': EllipseNode,
'donut-node': DonutNode,
},
edges: {
'line-edge': LineEdge,

View File

@ -0,0 +1,368 @@
import { DisplayObject } from '@antv/g';
import { each } from '@antv/util';
import { NodeDisplayModel } from '../../../types';
import { ShapeStyle, State } from '../../../types/item';
import {
NodeModelData,
NodeShapeMap,
NodeShapeStyles,
} from '../../../types/node';
import { BaseNode } from './base';
const defaultDonutPalette = [
'#61DDAA',
'#65789B',
'#F6BD16',
'#7262FD',
'#78D3F8',
'#9661BC',
'#F6903D',
'#008685',
'#F08BB4',
];
type DonutAttrs = {
[propKey: string]: number;
};
type DonutColorMap = {
[propKey: string]: string;
};
type DonutNodeDisplayModel = NodeDisplayModel & {
donutShapes: ShapeStyle & {
innerSize: number;
attrs: DonutAttrs;
colorMap: DonutColorMap;
};
};
type DonutSegmentValue = {
key: string; // key of the fan, came from the key of corresponding property of donutAttrs
value: number; // format number value of the single fan
color: string; // color from corresponding position of donutColorMap
};
type DonutSegmentConfig = {
arcR: number; // the radius of the fan
beginAngle: number; // the beginning angle of the arc
config: DonutSegmentValue; // value and color of the fan
index: number; // the index of the fan at the donut fans array
lineWidth: number; // width of the segment determining the inner size
zIndex: number; // shape zIndex
totalValue: number; // the total value of the donut configs
drawWhole?: boolean; // whether draw a arc with radius 2*PI to represent a circle
};
export class DonutNode extends BaseNode {
override defaultStyles = {
keyShape: {
r: 16,
x: 0,
y: 0,
},
donutShapes: {
innerSize: 0.6,
attrs: {},
colorMap: {},
zIndex: 1,
},
};
mergedStyles: NodeShapeStyles;
constructor(props) {
super(props);
// suggest to merge default styles like this to avoid style value missing
// this.defaultStyles = mergeStyles([this.baseDefaultStyles, this.defaultStyles]);
}
public draw(
model: DonutNodeDisplayModel,
shapeMap: NodeShapeMap,
diffData?: { previous: NodeModelData; current: NodeModelData },
diffState?: { previous: State[]; current: State[] },
): NodeShapeMap {
const { data = {} } = model;
let shapes: NodeShapeMap = { keyShape: undefined };
// keyShape
shapes.keyShape = this.drawKeyShape(model, shapeMap, diffData);
// haloShape
if (data.haloShape && this.drawHaloShape) {
shapes.haloShape = this.drawHaloShape(model, shapeMap, diffData);
}
// labelShape
if (data.labelShape) {
shapes.labelShape = this.drawLabelShape(model, shapeMap, diffData);
}
// labelBackgroundShape
if (data.labelBackgroundShape) {
shapes.labelBackgroundShape = this.drawLabelBackgroundShape(
model,
shapeMap,
diffData,
);
}
// anchor shapes
if (data.anchorShapes) {
const anchorShapes = this.drawAnchorShapes(
model,
shapeMap,
diffData,
diffState,
);
shapes = {
...shapes,
...anchorShapes,
};
}
// iconShape
if (data.iconShape) {
shapes.iconShape = this.drawIconShape(model, shapeMap, diffData);
}
// badgeShape
if (data.badgeShapes) {
const badgeShapes = this.drawBadgeShapes(
model,
shapeMap,
diffData,
diffState,
);
shapes = {
...shapes,
...badgeShapes,
};
}
// donutShapes
if (data.donutShapes) {
const donutShapes = this.drawDonutShapes(
model,
shapeMap,
diffData,
diffState,
);
shapes = {
...shapes,
...donutShapes,
};
}
// otherShapes
if (data.otherShapes && this.drawOtherShapes) {
shapes = {
...shapes,
...this.drawOtherShapes(model, shapeMap, diffData),
};
}
return shapes;
}
/**
* Draw a complete donut composed of several segments
* @param model
* @param shapeMap
* @param diffData
* @param diffState
* @returns
*/
private drawDonutShapes(
model: DonutNodeDisplayModel,
shapeMap: NodeShapeMap,
diffData?: { previous: NodeModelData; current: NodeModelData },
diffState?: { previous: State[]; current: State[] },
): {
[shapeId: string]: DisplayObject;
} {
const {
donutShapes: { innerSize, attrs, colorMap, zIndex },
} = this.mergedStyles as DonutNodeDisplayModel;
const attrNum = Object.keys(attrs).length;
if (!attrNum) return;
const { configs, totalValue } = getDonutConfig(attrs, colorMap);
if (!totalValue) return;
const { lineWidth, arcR } = getDonutSize(shapeMap.keyShape, innerSize);
let beginAngle = 0;
const shapes = {};
each(configs, (config, index) => {
const result = this.drawDonutSegment(
{
arcR,
beginAngle,
config,
index,
lineWidth,
zIndex,
totalValue,
drawWhole: attrNum === 1,
},
shapes,
model,
shapeMap,
diffData,
diffState,
);
if (result.shouldEnd) return;
beginAngle = result.beginAngle;
});
return shapes;
}
/**
* Draw a single donut segment
* @param cfg The configurations of donut segments
* @param shapes The collections of donut segment shapes
* @param model
* @param shapeMap
* @param diffData
* @param diffState
* @returns
*/
private drawDonutSegment = (
cfg: DonutSegmentConfig,
shapes: { [shapeId: string]: DisplayObject },
model: DonutNodeDisplayModel,
shapeMap: NodeShapeMap,
diffData?: { previous: NodeModelData; current: NodeModelData },
diffState?: { previous: State[]; current: State[] },
): {
beginAngle: number; // next begin iangle
shouldEnd: boolean; // finish fans drawing
} => {
const {
arcR,
beginAngle,
config,
index,
lineWidth,
zIndex,
totalValue,
drawWhole = false,
} = cfg;
const id = `donutShape${index}`;
const percent = config.value / totalValue;
if (percent < 0.001) {
// too small to add a fan
return {
beginAngle,
shouldEnd: false,
};
}
let arcEnd, endAngle, isLargeArc;
const arcBegin = calculateArcEndpoint(arcR, beginAngle);
// draw a path represents the whole circle, or the percentage is close to 1
if (drawWhole || percent > 0.999) {
arcEnd = [arcR, 0.0001]; // [arcR * cos(2 * PI), -arcR * sin(2 * PI)]
isLargeArc = 1;
} else {
const angle = percent * Math.PI * 2;
endAngle = beginAngle + angle;
arcEnd = calculateArcEndpoint(arcR, endAngle);
isLargeArc = angle > Math.PI ? 1 : 0;
}
const style = {
path: [
['M', arcBegin[0], arcBegin[1]],
['A', arcR, arcR, 0, isLargeArc, 0, arcEnd[0], arcEnd[1]],
],
stroke:
config.color || defaultDonutPalette[index % defaultDonutPalette.length],
lineWidth,
zIndex,
} as ShapeStyle;
shapes[id] = this.upsertShape(
'path',
id,
style,
shapeMap,
model,
) as DisplayObject;
return {
beginAngle: endAngle,
shouldEnd: drawWhole || percent > 0.999,
};
};
public drawKeyShape(
model: DonutNodeDisplayModel,
shapeMap: NodeShapeMap,
diffData?: { previous: NodeModelData; current: NodeModelData },
diffState?: { previous: State[]; current: State[] },
): DisplayObject {
return this.upsertShape(
'circle',
'keyShape',
this.mergedStyles.keyShape,
shapeMap,
model,
);
}
}
/**
* Calculate the endpoint of an arc segment.
* @param arcR Radius of the arc.
* @param angle angle in degrees subtended by arc.
*/
const calculateArcEndpoint = (arcR: number, angle: number): number[] => [
arcR * Math.cos(angle),
-arcR * Math.sin(angle),
];
/**
* calculate the total value and format single value for each fan
* @param donutAttrs
* @param donutColorMap
* @returns
*/
const getDonutConfig = (
donutAttrs: DonutAttrs,
donutColorMap: DonutColorMap,
): {
totalValue: number;
configs: DonutSegmentValue[];
} => {
let totalValue = 0;
const configs = [];
Object.keys(donutAttrs).forEach((name) => {
const value = +donutAttrs[name];
if (isNaN(value)) return;
configs.push({
key: name,
value,
color: donutColorMap[name],
});
totalValue += value;
});
return { totalValue, configs };
};
/**
* calculate the lineWidth and radius for fan shapes according to the keyShape's radius
* @param keyShape
* @returns
*/
const getDonutSize = (
keyShape,
innerSize: number,
): {
lineWidth: number;
arcR: number;
} => {
const keyShapeR = keyShape.attr('r');
const innerR = innerSize * keyShapeR; // The radius of the inner ring of the donut
const arcR = (keyShapeR + innerR) / 2; // The average of the radius of the inner ring and the radius of the outer ring
const lineWidth = keyShapeR - innerR;
return { lineWidth, arcR };
};

View File

@ -4,3 +4,4 @@ export * from './rect';
export * from './hexagon';
export * from './triangle';
export * from './ellipse';
export * from './donut';

View File

@ -11,6 +11,7 @@ import menu from './demo/menu';
import quadratic from './demo/quadratic';
import rect from './demo/rect';
import tooltip from './demo/tooltip';
import donut_node from './item/node/donut-node';
import cubic_edge from './item/edge/cubic-edge';
import cubic_horizon_edge from './item/edge/cubic-horizon-edge';
import cubic_vertical_edge from './item/edge/cubic-vertical-edge';
@ -66,6 +67,7 @@ export {
layouts_fruchterman_gpu,
layouts_fruchterman_wasm,
layouts_grid,
donut_node,
line_edge,
menu,
performance,

View File

@ -0,0 +1,243 @@
import { Graph, IGraph } from '../../../../src/index';
let outerTop = 0;
let graph: IGraph;
const createLabelCheckbox = (
container: HTMLElement,
labelText: string,
checkedCallback: () => void,
uncheckedCallback: () => void,
top?: number,
) => {
if (!container) return;
let innerTop = top;
if (!top) {
innerTop = outerTop;
outerTop += 30;
}
const label = document.createElement('span');
label.textContent = labelText;
label.style.position = 'absolute';
label.style.top = `${innerTop}px`;
label.style.left = '16px';
label.style.zIndex = '100';
const cb = document.createElement('input');
cb.type = 'checkbox';
cb.value = 'highlight';
cb.style.position = 'absolute';
cb.style.width = '20px';
cb.style.height = '20px';
cb.style.top = `${innerTop}px`;
cb.style.left = '400px';
cb.style.zIndex = '100';
cb.addEventListener('click', (e) => {
cb.checked ? checkedCallback() : uncheckedCallback();
});
container.appendChild(label);
container.appendChild(cb);
};
const createOperationContainer = (container: HTMLElement) => {
const operationContainer = document.createElement('div');
operationContainer.id = 'ctrl-container';
operationContainer.style.width = '100%';
operationContainer.style.height = '150px';
operationContainer.style.lineHeight = '50px';
operationContainer.style.backgroundColor = '#eee';
container.appendChild(operationContainer);
};
const createOperations = (): any => {
const parentEle = document.getElementById('ctrl-container');
if (!parentEle) return;
// Custom Donut Colors
createLabelCheckbox(
parentEle,
'custom donut colors',
() => {
graph.updateData('node', {
id: 'node1',
data: {
donutShapes: {
colorMap: {
income: '#78D3F8',
outcome: '#F08BB4',
unknown: '#65789B',
},
},
},
});
},
() => {
graph.updateData('node', {
id: 'node1',
data: {
donutShapes: {
colorMap: {},
},
},
});
},
);
// Custom Donut innerSize
createLabelCheckbox(
parentEle,
'update donut innerSize',
() => {
graph.updateData('node', {
id: 'node1',
data: {
donutShapes: {
innerSize: 0.8,
},
},
});
},
() => {
graph.updateData('node', {
id: 'node1',
data: {
donutShapes: {
innerSize: 0.6,
},
},
});
},
);
// Custom Donut attrs
createLabelCheckbox(
parentEle,
'update donut attrs',
() => {
graph.updateData('node', {
id: 'node1',
data: {
donutShapes: {
attrs: {
income: 280,
},
},
},
});
},
() => {
graph.updateData('node', {
id: 'node1',
data: {
donutShapes: {
attrs: {
income: 80,
outcome: 40,
unknown: 45,
},
},
},
});
},
);
// select
createLabelCheckbox(
parentEle,
'custom selected style',
() => {
graph.setItemState('node1', 'selected', true);
},
() => {
graph.setItemState('node1', 'selected', false);
},
);
};
export default (context) => {
const { container } = context;
// 1.create operation container
createOperationContainer(container!);
const data = {
nodes: [
{
id: 'node1',
data: {
x: 100,
y: 100,
type: 'donut-node',
keyShape: {
r: 30,
},
labelShape: {
text: 'label',
position: 'bottom',
},
labelBackgroundShape: {
fill: 'red',
},
anchorShapes: [
{
position: [0, 0.5],
r: 2,
fill: 'red',
},
],
iconShape: {
img: 'https://gw.alipayobjects.com/zos/basement_prod/012bcf4f-423b-4922-8c24-32a89f8c41ce.svg',
width: 20,
height: 20,
},
badgeShapes: [
{
text: '1',
position: 'rightTop',
color: 'blue',
},
],
donutShapes: {
attrs: {
income: 80,
outcome: 40,
unknown: 45,
},
},
},
},
],
};
// @ts-ignore
graph = new Graph({
...context,
data,
type: 'graph',
modes: {
default: ['drag-canvas', 'drag-node', 'click-select', 'hover-activate'],
},
node: (nodeInnerModel: any) => {
const { id, data } = nodeInnerModel;
return {
id,
data: {
keyShape: {
r: 16,
},
...data,
},
};
},
});
// 2.create operations
createOperations();
return graph;
};

View File

@ -0,0 +1,157 @@
import { resetEntityCounter } from '@antv/g';
import donutNode from '../demo/item/node/donut-node';
import './utils/useSnapshotMatchers';
import { createContext } from './utils';
describe('Items node donut', () => {
beforeEach(() => {
/**
* SVG Snapshot testing will generate a unique id for each element.
* Reset to 0 to keep snapshot consistent.
*/
resetEntityCounter();
});
it('should be rendered correctly with Canvas2D', (done) => {
const dir = `${__dirname}/snapshots/canvas/items/node/donut`;
const { backgroundCanvas, canvas, transientCanvas, container } =
createContext('canvas', 500, 500);
const graph = donutNode({
container,
backgroundCanvas,
canvas,
transientCanvas,
width: 500,
height: 500,
});
graph.on('afterlayout', async () => {
await expect(canvas).toMatchCanvasSnapshot(dir, 'items-node-donut');
/**
* Click the checkbox to set custom colors.
*/
const $customColors = document.querySelectorAll(
'input',
)[0] as HTMLInputElement;
$customColors.click();
await expect(canvas).toMatchCanvasSnapshot(
dir,
'items-node-donut-custom-colors',
);
$customColors.click();
/**
* Click the checkbox to set custom inner size.
*/
const $innerSize = document.querySelectorAll(
'input',
)[1] as HTMLInputElement;
$innerSize.click();
await expect(canvas).toMatchCanvasSnapshot(
dir,
'items-node-donut-custom-innersize',
);
$innerSize.click();
/**
* Click the checkbox to update attrs.
*/
const $attrs = document.querySelectorAll('input')[2] as HTMLInputElement;
$attrs.click();
await expect(canvas).toMatchCanvasSnapshot(
dir,
'items-node-donut-custom-attrs',
);
$attrs.click();
/**
* Click the checkbox to set selected style.
*/
const $selected = document.querySelectorAll(
'input',
)[3] as HTMLInputElement;
$selected.click();
await expect(canvas).toMatchCanvasSnapshot(
dir,
'items-node-donut-selected-style',
);
$selected.click();
graph.destroy();
done();
});
});
it('should be rendered correctly with SVG', (done) => {
const dir = `${__dirname}/snapshots/svg/items/node/donut`;
const { backgroundCanvas, canvas, transientCanvas, container } =
createContext('svg', 500, 500);
const graph = donutNode({
container,
backgroundCanvas,
canvas,
transientCanvas,
width: 500,
height: 500,
});
graph.on('afterlayout', async () => {
await expect(canvas).toMatchSVGSnapshot(dir, 'items-node-donut');
/**
* Click the checkbox to set custom colors.
*/
const $customColors = document.querySelectorAll(
'input',
)[0] as HTMLInputElement;
$customColors.click();
await expect(canvas).toMatchSVGSnapshot(
dir,
'items-node-donut-custom-colors',
);
$customColors.click();
/**
* Click the checkbox to set custom inner size.
*/
const $innerSize = document.querySelectorAll(
'input',
)[1] as HTMLInputElement;
$innerSize.click();
await expect(canvas).toMatchSVGSnapshot(
dir,
'items-node-donut-custom-innersize',
);
$innerSize.click();
/**
* Click the checkbox to update attrs.
*/
const $attrs = document.querySelectorAll('input')[2] as HTMLInputElement;
$attrs.click();
await expect(canvas).toMatchSVGSnapshot(
dir,
'items-node-donut-custom-attrs',
);
$attrs.click();
/**
* Click the checkbox to set selected style.
*/
const $selected = document.querySelectorAll(
'input',
)[3] as HTMLInputElement;
$selected.click();
await expect(canvas).toMatchSVGSnapshot(
dir,
'items-node-donut-selected-style',
);
$selected.click();
graph.destroy();
done();
});
});
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="500" height="500" style="background: transparent; position: fixed; outline: none;" color-interpolation-filters="sRGB" tabindex="1"><defs/><g transform="matrix(1,0,0,1,0,0)"><g fill="none" transform="matrix(1,0,0,1,0,0)"><g fill="none" stroke="transparent" stroke-width="01"/><g fill="none" transform="matrix(1,0,0,1,0,0)"/><g fill="none" transform="matrix(1,0,0,1,0,0)"/><g fill="none" transform="matrix(1,0,0,1,0,0)"><g fill="none" transform="matrix(1,0,0,1,100,100)"><g transform="matrix(1,0,0,1,0,0)"><circle fill="rgba(34,126,255,1)" transform="translate(-30,-30)" cx="30" cy="30" r="30" stroke-width="0"/></g><g transform="matrix(0.945946,0,0,1,-16.499999,30)"><path fill="rgba(255,0,0,1)" d="M 0,0 l 37,0 l 0,19 l-37 0 z" stroke-width="0" opacity="0.75" width="37" height="19"/></g><g transform="matrix(1,0,0,1,-10,-10)"><image fill="rgba(255,255,255,1)" preserveAspectRatio="none" x="0" y="0" href="https://gw.alipayobjects.com/zos/basement_prod/012bcf4f-423b-4922-8c24-32a89f8c41ce.svg" font-size="16" font-family="sans-serif" font-weight="normal" font-variant="normal" font-style="normal" stroke-width="0" width="20" height="20"/></g><g transform="matrix(1,0,0,1,-24,-23.999950)"><path fill="none" d="M 47.99999999994792,23.99995 A 24 24 0 1 0 47.99999999994792 24.000049999999998" stroke="rgba(97,221,170,1)" stroke-width="12"/><path fill="none" d="M 47.891326141754035,24.00000000000001 A 24 24 0 0 0 3.552713678800501e-15 21.71865496069962" stroke="transparent" stroke-width="12"/></g><g transform="matrix(1,0,0,1,0,32)"><text fill="rgba(0,0,0,1)" dominant-baseline="central" paint-order="stroke" dx="0" dy="7.5px" text-anchor="middle" font-size="12" font-family="sans-serif" font-weight="normal" font-variant="normal" font-style="normal" stroke-width="0">label</text></g><g transform="matrix(1,0,0,1,-30,0)"><circle fill="rgba(255,0,0,1)" transform="translate(-2,-2)" cx="2" cy="2" stroke-width="1" stroke="rgba(0,0,0,0.65)" r="2"/></g><g transform="matrix(1,0,0,1,15,-33.200001)"><path fill="rgba(0,0,255,1)" d="M 10,0 l 0,0 a 10,10,0,0,1,10,10 l 0,0 a 10,10,0,0,1,-10,10 l 0,0 a 10,10,0,0,1,-10,-10 l 0,0 a 10,10,0,0,1,10,-10 z" height="20" width="20"/></g><g transform="matrix(1,0,0,1,17,-23.200001)"><text fill="rgba(255,255,255,1)" dominant-baseline="central" paint-order="stroke" dx="0.5" font-size="17" text-anchor="left">1</text></g></g></g></g></g></svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="500" height="500" style="background: transparent; position: fixed; outline: none;" color-interpolation-filters="sRGB" tabindex="1"><defs/><g transform="matrix(1,0,0,1,0,0)"><g fill="none" transform="matrix(1,0,0,1,0,0)"><g fill="none" stroke="transparent" stroke-width="01"/><g fill="none" transform="matrix(1,0,0,1,0,0)"/><g fill="none" transform="matrix(1,0,0,1,0,0)"/><g fill="none" transform="matrix(1,0,0,1,0,0)"><g fill="none" transform="matrix(1,0,0,1,100,100)"><g transform="matrix(1,0,0,1,0,0)"><circle fill="rgba(34,126,255,1)" transform="translate(-30,-30)" cx="30" cy="30" r="30" stroke-width="0"/></g><g transform="matrix(0.945946,0,0,1,-16.499999,30)"><path fill="rgba(255,0,0,1)" d="M 0,0 l 37,0 l 0,19 l-37 0 z" stroke-width="0" opacity="0.75" width="37" height="19"/></g><g transform="matrix(1,0,0,1,-10,-10)"><image fill="rgba(255,255,255,1)" preserveAspectRatio="none" x="0" y="0" href="https://gw.alipayobjects.com/zos/basement_prod/012bcf4f-423b-4922-8c24-32a89f8c41ce.svg" font-size="16" font-family="sans-serif" font-weight="normal" font-variant="normal" font-style="normal" stroke-width="0" width="20" height="20"/></g><g transform="matrix(1,0,0,1,-23.891327,-24)"><path fill="none" d="M 47.891326141754035,24.00000000000001 A 24 24 0 0 0 3.552713678800501e-15 21.71865496069962" stroke="rgba(120,211,248,1)" stroke-width="12"/><path fill="none" d="M 47.891326141754035,24.00000000000001 A 24 24 0 0 0 3.552713678800501e-15 21.71865496069962" stroke="transparent" stroke-width="24"/></g><g transform="matrix(1,0,0,1,-24,-2.281345)"><path fill="none" d="M 0.10867385824597164,1.7763568394002505e-15 A 24 24 0 0 0 20.584443881441153 26.037059644442774" stroke="rgba(240,139,180,1)" stroke-width="12"/><path fill="none" d="M 0.10867385824597164,1.7763568394002505e-15 A 24 24 0 0 0 20.584443881441153 26.037059644442774" stroke="transparent" stroke-width="24"/></g><g transform="matrix(1,0,0,1,-3.415556,0)"><path fill="none" d="M 3.1086244689504383e-15,23.755714605142373 A 24 24 0 0 0 27.415556118558847 -4.779836400494208e-15" stroke="rgba(101,120,155,1)" stroke-width="12"/><path fill="none" d="M 3.1086244689504383e-15,23.755714605142373 A 24 24 0 0 0 27.415556118558847 -4.779836400494208e-15" stroke="transparent" stroke-width="24"/></g><g transform="matrix(1,0,0,1,0,32)"><text fill="rgba(0,0,0,1)" dominant-baseline="central" paint-order="stroke" dx="0" dy="7.5px" text-anchor="middle" font-size="12" font-family="sans-serif" font-weight="normal" font-variant="normal" font-style="normal" stroke-width="0">label</text></g><g transform="matrix(1,0,0,1,-30,0)"><circle fill="rgba(255,0,0,1)" transform="translate(-2,-2)" cx="2" cy="2" stroke-width="1" stroke="rgba(0,0,0,0.65)" r="2"/></g><g transform="matrix(1,0,0,1,15,-33.200001)"><path fill="rgba(0,0,255,1)" d="M 10,0 l 0,0 a 10,10,0,0,1,10,10 l 0,0 a 10,10,0,0,1,-10,10 l 0,0 a 10,10,0,0,1,-10,-10 l 0,0 a 10,10,0,0,1,10,-10 z" height="20" width="20"/></g><g transform="matrix(1,0,0,1,17,-23.200001)"><text fill="rgba(255,255,255,1)" dominant-baseline="central" paint-order="stroke" dx="0.5" font-size="17" text-anchor="left">1</text></g></g></g></g></g></svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="500" height="500" style="background: transparent; position: fixed; outline: none;" color-interpolation-filters="sRGB" tabindex="1"><defs/><g transform="matrix(1,0,0,1,0,0)"><g fill="none" transform="matrix(1,0,0,1,0,0)"><g fill="none" stroke="transparent" stroke-width="01"/><g fill="none" transform="matrix(1,0,0,1,0,0)"/><g fill="none" transform="matrix(1,0,0,1,0,0)"/><g fill="none" transform="matrix(1,0,0,1,0,0)"><g fill="none" transform="matrix(1,0,0,1,100,100)"><g transform="matrix(1,0,0,1,0,0)"><circle fill="rgba(34,126,255,1)" transform="translate(-30,-30)" cx="30" cy="30" r="30" stroke-width="0"/></g><g transform="matrix(0.945946,0,0,1,-16.499999,30)"><path fill="rgba(255,0,0,1)" d="M 0,0 l 37,0 l 0,19 l-37 0 z" stroke-width="0" opacity="0.75" width="37" height="19"/></g><g transform="matrix(1,0,0,1,-10,-10)"><image fill="rgba(255,255,255,1)" preserveAspectRatio="none" x="0" y="0" href="https://gw.alipayobjects.com/zos/basement_prod/012bcf4f-423b-4922-8c24-32a89f8c41ce.svg" font-size="16" font-family="sans-serif" font-weight="normal" font-variant="normal" font-style="normal" stroke-width="0" width="20" height="20"/></g><g transform="matrix(1,0,0,1,-26.877743,-27)"><path fill="none" d="M 53.877741909473286,27.000000000000046 A 27 27 0 0 0 7.105427357601002e-15 24.433486830787107" stroke="rgba(97,221,170,1)" stroke-width="6"/><path fill="none" d="M 47.891326141754035,24.00000000000001 A 24 24 0 0 0 3.552713678800501e-15 21.71865496069962" stroke="transparent" stroke-width="6"/></g><g transform="matrix(1,0,0,1,-27,-2.566513)"><path fill="none" d="M 0.12225809052671721,0 A 27 27 0 0 0 23.1574993666213 29.291692099998123" stroke="rgba(101,120,155,1)" stroke-width="6"/><path fill="none" d="M 0.10867385824597164,1.7763568394002505e-15 A 24 24 0 0 0 20.584443881441153 26.037059644442774" stroke="transparent" stroke-width="6"/></g><g transform="matrix(1,0,0,1,-3.842501,0)"><path fill="none" d="M 0,26.725178930785177 A 27 27 0 0 0 30.8425006333787 6.178883824198621e-16" stroke="rgba(246,189,22,1)" stroke-width="6"/><path fill="none" d="M 3.1086244689504383e-15,23.755714605142373 A 24 24 0 0 0 27.415556118558847 -4.779836400494208e-15" stroke="transparent" stroke-width="6"/></g><g transform="matrix(1,0,0,1,0,32)"><text fill="rgba(0,0,0,1)" dominant-baseline="central" paint-order="stroke" dx="0" dy="7.5px" text-anchor="middle" font-size="12" font-family="sans-serif" font-weight="normal" font-variant="normal" font-style="normal" stroke-width="0">label</text></g><g transform="matrix(1,0,0,1,-30,0)"><circle fill="rgba(255,0,0,1)" transform="translate(-2,-2)" cx="2" cy="2" stroke-width="1" stroke="rgba(0,0,0,0.65)" r="2"/></g><g transform="matrix(1,0,0,1,15,-33.200001)"><path fill="rgba(0,0,255,1)" d="M 10,0 l 0,0 a 10,10,0,0,1,10,10 l 0,0 a 10,10,0,0,1,-10,10 l 0,0 a 10,10,0,0,1,-10,-10 l 0,0 a 10,10,0,0,1,10,-10 z" height="20" width="20"/></g><g transform="matrix(1,0,0,1,17,-23.200001)"><text fill="rgba(255,255,255,1)" dominant-baseline="central" paint-order="stroke" dx="0.5" font-size="17" text-anchor="left">1</text></g></g></g></g></g></svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="500" height="500" style="background: transparent; position: fixed; outline: none;" color-interpolation-filters="sRGB" tabindex="1"><defs/><g transform="matrix(1,0,0,1,0,0)"><g fill="none" transform="matrix(1,0,0,1,0,0)"><g fill="none" stroke="transparent" stroke-width="01"/><g fill="none" transform="matrix(1,0,0,1,0,0)"/><g fill="none" transform="matrix(1,0,0,1,0,0)"/><g fill="none" transform="matrix(1,0,0,1,0,0)"><g fill="none" transform="matrix(1,0,0,1,100,100)"><g transform="matrix(1,0,0,1,0,0)"><circle fill="rgba(34,126,255,1)" transform="translate(-30,-30)" cx="30" cy="30" opacity="0.25" r="30" stroke-width="20" stroke="rgba(34,126,255,1)" pointer-events="none"/></g><g transform="matrix(1,0,0,1,0,0)"><circle fill="rgba(34,126,255,1)" transform="translate(-30,-30)" cx="30" cy="30" r="30" stroke-width="3" stroke="rgba(0,0,0,1)"/></g><g transform="matrix(0.945946,0,0,1,-16.499999,30)"><path fill="rgba(255,0,0,1)" d="M 0,0 l 37,0 l 0,19 l-37 0 z" stroke-width="0" opacity="0.75" width="37" height="19"/></g><g transform="matrix(1,0,0,1,-10,-10)"><image fill="rgba(255,255,255,1)" preserveAspectRatio="none" x="0" y="0" href="https://gw.alipayobjects.com/zos/basement_prod/012bcf4f-423b-4922-8c24-32a89f8c41ce.svg" font-size="16" font-family="sans-serif" font-weight="normal" font-variant="normal" font-style="normal" stroke-width="0" width="20" height="20"/></g><g transform="matrix(1,0,0,1,-23.891327,-24)"><path fill="none" d="M 47.891326141754035,24.00000000000001 A 24 24 0 0 0 3.552713678800501e-15 21.71865496069962" stroke="rgba(97,221,170,1)" stroke-width="12"/><path fill="none" d="M 47.891326141754035,24.00000000000001 A 24 24 0 0 0 3.552713678800501e-15 21.71865496069962" stroke="transparent" stroke-width="12"/></g><g transform="matrix(1,0,0,1,-24,-2.281345)"><path fill="none" d="M 0.10867385824597164,1.7763568394002505e-15 A 24 24 0 0 0 20.584443881441153 26.037059644442774" stroke="rgba(101,120,155,1)" stroke-width="12"/><path fill="none" d="M 0.10867385824597164,1.7763568394002505e-15 A 24 24 0 0 0 20.584443881441153 26.037059644442774" stroke="transparent" stroke-width="24"/></g><g transform="matrix(1,0,0,1,-3.415556,0)"><path fill="none" d="M 3.1086244689504383e-15,23.755714605142373 A 24 24 0 0 0 27.415556118558847 -4.779836400494208e-15" stroke="rgba(246,189,22,1)" stroke-width="12"/><path fill="none" d="M 3.1086244689504383e-15,23.755714605142373 A 24 24 0 0 0 27.415556118558847 -4.779836400494208e-15" stroke="transparent" stroke-width="24"/></g><g transform="matrix(1,0,0,1,0,32)"><text fill="rgba(0,0,0,1)" dominant-baseline="central" paint-order="stroke" dx="0" dy="7.5px" text-anchor="middle" font-size="12" font-family="sans-serif" font-weight="700" font-variant="normal" font-style="normal" stroke-width="0">label</text></g><g transform="matrix(1,0,0,1,-30,0)"><circle fill="rgba(255,0,0,1)" transform="translate(-2,-2)" cx="2" cy="2" stroke-width="1" stroke="rgba(0,0,0,0.65)" r="2"/></g><g transform="matrix(1,0,0,1,15,-33.200001)"><path fill="rgba(0,0,255,1)" d="M 10,0 l 0,0 a 10,10,0,0,1,10,10 l 0,0 a 10,10,0,0,1,-10,10 l 0,0 a 10,10,0,0,1,-10,-10 l 0,0 a 10,10,0,0,1,10,-10 z" height="20" width="20"/></g><g transform="matrix(1,0,0,1,17,-23.200001)"><text fill="rgba(255,255,255,1)" dominant-baseline="central" paint-order="stroke" dx="0.5" font-size="17" text-anchor="left">1</text></g></g></g></g></g></svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="500" height="500" style="background: transparent; position: fixed; outline: none;" color-interpolation-filters="sRGB" tabindex="1"><defs/><g transform="matrix(1,0,0,1,0,0)"><g fill="none" transform="matrix(1,0,0,1,0,0)"><g fill="none" stroke="transparent" stroke-width="01"/><g fill="none" transform="matrix(1,0,0,1,0,0)"/><g fill="none" transform="matrix(1,0,0,1,0,0)"/><g fill="none" transform="matrix(1,0,0,1,0,0)"><g fill="none" transform="matrix(1,0,0,1,100,100)"><g transform="matrix(1,0,0,1,0,0)"><circle fill="rgba(34,126,255,1)" transform="translate(-30,-30)" cx="30" cy="30" r="30" stroke-width="0"/></g><g transform="matrix(0.945946,0,0,1,-16.499999,30)"><path fill="rgba(255,0,0,1)" d="M 0,0 l 37,0 l 0,19 l-37 0 z" stroke-width="0" opacity="0.75" width="37" height="19"/></g><g transform="matrix(1,0,0,1,-10,-10)"><image fill="rgba(255,255,255,1)" preserveAspectRatio="none" x="0" y="0" href="https://gw.alipayobjects.com/zos/basement_prod/012bcf4f-423b-4922-8c24-32a89f8c41ce.svg" font-size="16" font-family="sans-serif" font-weight="normal" font-variant="normal" font-style="normal" stroke-width="0" width="20" height="20"/></g><g transform="matrix(1,0,0,1,-23.891327,-24)"><path fill="none" d="M 47.891326141754035,24.00000000000001 A 24 24 0 0 0 3.552713678800501e-15 21.71865496069962" stroke="rgba(97,221,170,1)" stroke-width="12"/><path fill="none" d="M 47.891326141754035,24.00000000000001 A 24 24 0 0 0 3.552713678800501e-15 21.71865496069962" stroke="transparent" stroke-width="24"/></g><g transform="matrix(1,0,0,1,-24,-2.281345)"><path fill="none" d="M 0.10867385824597164,1.7763568394002505e-15 A 24 24 0 0 0 20.584443881441153 26.037059644442774" stroke="rgba(101,120,155,1)" stroke-width="12"/><path fill="none" d="M 0.10867385824597164,1.7763568394002505e-15 A 24 24 0 0 0 20.584443881441153 26.037059644442774" stroke="transparent" stroke-width="24"/></g><g transform="matrix(1,0,0,1,-3.415556,0)"><path fill="none" d="M 3.1086244689504383e-15,23.755714605142373 A 24 24 0 0 0 27.415556118558847 -4.779836400494208e-15" stroke="rgba(246,189,22,1)" stroke-width="12"/><path fill="none" d="M 3.1086244689504383e-15,23.755714605142373 A 24 24 0 0 0 27.415556118558847 -4.779836400494208e-15" stroke="transparent" stroke-width="24"/></g><g transform="matrix(1,0,0,1,0,32)"><text fill="rgba(0,0,0,1)" dominant-baseline="central" paint-order="stroke" dx="0" dy="7.5px" text-anchor="middle" font-size="12" font-family="sans-serif" font-weight="normal" font-variant="normal" font-style="normal" stroke-width="0">label</text></g><g transform="matrix(1,0,0,1,-30,0)"><circle fill="rgba(255,0,0,1)" transform="translate(-2,-2)" cx="2" cy="2" stroke-width="1" stroke="rgba(0,0,0,0.65)" r="2"/></g><g transform="matrix(1,0,0,1,15,-33.200001)"><path fill="rgba(0,0,255,1)" d="M 10,0 l 0,0 a 10,10,0,0,1,10,10 l 0,0 a 10,10,0,0,1,-10,10 l 0,0 a 10,10,0,0,1,-10,-10 l 0,0 a 10,10,0,0,1,10,-10 z" height="20" width="20"/></g><g transform="matrix(1,0,0,1,17,-23.200001)"><text fill="rgba(255,255,255,1)" dominant-baseline="central" paint-order="stroke" dx="0.5" font-size="17" text-anchor="left">1</text></g></g></g></g></g></svg>

After

Width:  |  Height:  |  Size: 3.1 KiB