feat: style standards; perf: type refine (#4470)

* feat: style standards; perf: type refine

* chore: refine
This commit is contained in:
Yanyan Wang 2023-04-27 18:05:16 +08:00 committed by GitHub
parent 9c8db01fe1
commit 05948c5e7d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 2706 additions and 1417 deletions

View File

@ -1,4 +1,12 @@
export const RESERVED_SHAPE_IDS = ['keyShape', 'labelShape', 'iconShape'];
export const RESERVED_SHAPE_IDS = [
'keyShape',
'labelShape',
'labelBackgroundShape',
'iconShape',
'haloShape',
'anchorShapes',
'badgeShapes',
];
export const OTHER_SHAPES_FIELD_NAME = 'otherShapes';
export const DEFAULT_LABEL_BG_PADDING = [4, 4, 4, 4];
@ -9,6 +17,7 @@ export const DEFAULT_SHAPE_STYLE = {
shadowColor: undefined,
shadowBlur: 0,
lineDash: undefined,
zIndex: 0,
};
/** Default text style to avoid shape value missing */
export const DEFAULT_TEXT_STYLE = {

View File

@ -1,16 +1,12 @@
import { Group } from '@antv/g';
import { clone } from '@antv/util';
import { EdgeDisplayModel, EdgeModel } from '../types';
import { EdgeDisplayModel, EdgeModel, NodeModelData } from '../types';
import { EdgeModelData } from '../types/edge';
import {
DisplayMapper,
ItemShapeStyles,
ITEM_TYPE,
State,
} from '../types/item';
import { DisplayMapper, State } from '../types/item';
import { updateShapes } from '../util/shape';
import Item from './item';
import Node from './node';
import { EdgeStyleSet } from 'types/theme';
interface IProps {
model: EdgeModel;
@ -22,7 +18,7 @@ interface IProps {
};
sourceItem: Node;
targetItem: Node;
themeStyles: ItemShapeStyles;
themeStyles: EdgeStyleSet;
}
export default class Edge extends Item {
@ -51,18 +47,10 @@ export default class Edge extends Item {
diffState?: { previous: State[]; current: State[] },
) {
// get the end points
const sourceBBox = this.sourceItem.getKeyBBox();
const targetBBox = this.targetItem.getKeyBBox();
const sourcePoint = {
x: sourceBBox.center[0],
y: sourceBBox.center[1],
z: sourceBBox.center[2],
};
const targetPoint = {
x: targetBBox.center[0],
y: targetBBox.center[1],
z: targetBBox.center[2],
};
const { x: sx, y: sy, z: sz } = this.sourceItem.model.data as NodeModelData;
const { x: tx, y: ty, z: tz } = this.targetItem.model.data as NodeModelData;
const sourcePoint = this.sourceItem.getAnchorPoint({ x: tx, y: ty, z: tz });
const targetPoint = this.targetItem.getAnchorPoint({ x: sx, y: sy, z: sz });
this.renderExt.mergeStyles(displayModel);
const shapeMap = this.renderExt.draw(
displayModel,
@ -75,8 +63,8 @@ export default class Edge extends Item {
// add shapes to group, and update shapeMap
this.shapeMap = updateShapes(this.shapeMap, shapeMap, this.group);
const { labelShape } = this.shapeMap;
const { haloShape, labelShape, labelBackgroundShape } = this.shapeMap;
haloShape?.toBack();
labelShape?.toFront();
super.draw(displayModel, diffData, diffState);

View File

@ -13,10 +13,11 @@ import {
State,
} from '../types/item';
import { NodeShapeMap } from '../types/node';
import { ItemStyleSet } from '../types/theme';
import { EdgeStyleSet, NodeStyleSet } from '../types/theme';
import { isArrayOverlap } from '../util/array';
import { mergeStyles, updateShapes } from '../util/shape';
import { isEncode } from '../util/type';
import { DEFAULT_MAPPER } from '../util/mapper';
export default abstract class Item implements IItem {
public destroyed = false;
@ -116,7 +117,7 @@ export default abstract class Item implements IItem {
model: ItemModel,
diffData?: { previous: ItemModelData; current: ItemModelData },
isReplace?: boolean,
themeStyles?: ItemStyleSet,
themeStyles?: NodeStyleSet | EdgeStyleSet,
) {
// 1. merge model into this model
this.model = model;
@ -170,26 +171,34 @@ export default abstract class Item implements IItem {
model: ItemDisplayModel;
typeChange?: boolean;
} {
const { mapper } = this;
const { mapper, type } = this;
const defaultMapper = DEFAULT_MAPPER[type];
const { data: innerModelData, ...otherFields } = innerModel;
const { current = innerModelData, previous } = diffData || {};
// === no mapper, displayModel = model ===
if (!mapper) {
this.displayModel = innerModel; // TODO: need clone?
this.displayModel = defaultMapper(innerModel);
// compare the previous data and current data to find shape changes
let typeChange = false;
if (current) {
typeChange = Boolean(current.type);
}
return {
model: innerModel,
model: this.displayModel,
typeChange,
};
}
// === mapper is function, displayModel is mapper(model), cannot diff the displayModel, so all the shapes need to be updated ===
if (isFunction(mapper)) return { model: (mapper as Function)(innerModel) };
if (isFunction(mapper))
return {
model: {
...defaultMapper(innerModel),
...(mapper as Function)(innerModel),
},
};
// === fields' values in mapper are final value or Encode ===
const dataChangedFields = isReplace
@ -199,20 +208,49 @@ export default abstract class Item implements IItem {
let typeChange = false;
const { data, ...otherProps } = innerModel;
const displayModelData = clone(data);
const displayModelData = defaultMapper(innerModel).data; //clone(data);
// const defaultMappedModel = defaultMapper(innerModel);
Object.keys(mapper).forEach((fieldName) => {
const subMapper = mapper[fieldName];
if (RESERVED_SHAPE_IDS.includes(fieldName)) {
// reserved shapes, fieldName is shapeId
debugger;
let subMapper = mapper[fieldName];
const isReservedShapeId = RESERVED_SHAPE_IDS.includes(fieldName);
const isShapeId =
RESERVED_SHAPE_IDS.includes(fieldName) ||
fieldName === OTHER_SHAPES_FIELD_NAME;
if ((isShapeId && isEncode(subMapper)) || !isShapeId) {
// fields not about shape
if (!displayModelData.hasOwnProperty(fieldName)) {
displayModelData[fieldName] = {};
updateShapeChange({
const { changed, value: mappedValue } = updateChange({
innerModel,
mapper: subMapper,
mapper,
fieldName,
dataChangedFields,
shapeConfig: displayModelData[fieldName],
});
if (isShapeId) {
if (!mappedValue) return;
subMapper = mappedValue;
} else {
displayModelData[fieldName] = mappedValue;
}
if (changed && fieldName === 'type') typeChange = true;
} else if (
fieldName === 'type' &&
(!dataChangedFields || dataChangedFields.includes('type'))
) {
typeChange = true;
}
}
if (isReservedShapeId) {
// reserved shapes, fieldName is shapeId
displayModelData[fieldName] = displayModelData[fieldName] || {};
updateShapeChange({
innerModel,
mapper: subMapper,
dataChangedFields,
shapeConfig: displayModelData[fieldName],
});
} else if (fieldName === OTHER_SHAPES_FIELD_NAME) {
// other shapes
displayModelData[fieldName] = displayModelData[fieldName] || {};
@ -229,23 +267,6 @@ export default abstract class Item implements IItem {
});
}
});
} else {
// fields not about shape
if (!displayModelData.hasOwnProperty(fieldName)) {
const { changed, value: mappedValue } = updateChange({
innerModel,
mapper,
fieldName,
dataChangedFields,
});
displayModelData[fieldName] = mappedValue;
if (changed && fieldName === 'type') typeChange = true;
} else if (
fieldName === 'type' &&
(!dataChangedFields || dataChangedFields.includes('type'))
) {
typeChange = true;
}
}
});
const displayModel = {

View File

@ -1,11 +1,18 @@
import { Group } from '@antv/g';
import { clone } from '@antv/util';
import { Point } from '../types/common';
import { NodeModel } from '../types';
import { DisplayMapper, ItemShapeStyles, State } from '../types/item';
import { DisplayMapper, State } from '../types/item';
import { NodeDisplayModel, NodeModelData } from '../types/node';
import { ItemStyleSet } from '../types/theme';
import { NodeStyleSet } from '../types/theme';
import { updateShapes } from '../util/shape';
import Item from './item';
import {
getCircleIntersectByPoint,
getEllipseIntersectByPoint,
getNearestPoint,
getRectIntersectByPoint,
} from 'util/point';
interface IProps {
model: NodeModel;
@ -15,11 +22,12 @@ interface IProps {
stateMapper: {
[stateName: string]: DisplayMapper;
};
themeStyles: ItemShapeStyles;
themeStyles: NodeStyleSet;
device?: any; // for 3d shapes
}
export default class Node extends Item {
public type: 'node';
private anchorPointsCache: Point[];
constructor(props: IProps) {
super(props);
@ -48,17 +56,20 @@ export default class Node extends Item {
// add shapes to group, and update shapeMap
this.shapeMap = updateShapes(prevShapeMap, shapeMap, group);
this.shapeMap.labelShape?.toFront();
const { haloShape, labelShape, labelBackgroundShape } = this.shapeMap;
haloShape?.toBack();
labelShape?.toFront();
labelBackgroundShape?.toBack();
super.draw(displayModel, diffData, diffState);
this.anchorPointsCache = undefined;
}
public update(
model: NodeModel,
diffData?: { previous: NodeModelData; current: NodeModelData },
isReplace?: boolean,
themeStyles?: ItemStyleSet,
themeStyles?: NodeStyleSet,
) {
super.update(model, diffData, isReplace, themeStyles);
const { data } = this.displayModel;
@ -86,4 +97,83 @@ export default class Node extends Item {
themeStyles: clone(this.themeStyles),
});
}
public getAnchorPoint(point: Point) {
const { keyShape } = this.shapeMap;
const shapeType = keyShape.nodeName;
const { x, y, anchorPoints = [] } = this.model.data as NodeModelData;
let intersectPoint: Point | null;
switch (shapeType) {
case 'circle':
intersectPoint = getCircleIntersectByPoint(
{
x,
y,
r: keyShape.attributes.r,
},
point,
);
break;
case 'ellipse':
intersectPoint = getEllipseIntersectByPoint(
{
x,
y,
rx: keyShape.attributes.rx,
ry: keyShape.attributes.ry,
},
point,
);
break;
default:
const bbox =
this.renderExt.boundsCache?.keyShapeLocal ||
keyShape.getLocalBounds();
intersectPoint = getRectIntersectByPoint(
{
x: bbox.halfExtents[0],
y: bbox.halfExtents[1],
width: bbox.max[0] - bbox.min[0],
height: bbox.max[1] - bbox.min[1],
},
point,
);
}
let anchorPointsPositions = this.anchorPointsCache;
if (!anchorPointsPositions) {
const keyShapeBBox =
this.renderExt.boundsCache?.keyShapeLocal ||
this.shapeMap.keyShape.getLocalBounds();
const keyShapeWidth = keyShapeBBox.max[0] - keyShapeBBox.min[0];
const keyShapeHeight = keyShapeBBox.max[1] - keyShapeBBox.min[1];
anchorPointsPositions = anchorPoints.map((pointRatio) => {
const [xRatio, yRatio] = pointRatio;
return {
x: keyShapeWidth * (xRatio - 0.5) + x,
y: keyShapeHeight * (yRatio - 0.5) + y,
};
});
this.anchorPointsCache = anchorPointsPositions;
}
let linkPoint = intersectPoint;
// If the node has anchorPoints in the data, find the nearest anchor point.
if (anchorPoints.length) {
if (!linkPoint) {
// If the linkPoint is failed to calculate.
linkPoint = point;
}
linkPoint = getNearestPoint(
anchorPointsPositions,
linkPoint,
).nearestPoint;
}
if (!linkPoint) {
// If the calculations above are all failed, return the data's position
return { x, y };
}
return linkPoint;
}
}

View File

@ -551,7 +551,12 @@ const mergeOneLevelData = (
const { data: prevData } = prevModel;
const mergedData = {};
Object.keys(newData).forEach((key) => {
if (isObject(prevData[key]) && isObject(newData[key])) {
if (isArray(prevData[key]) || isArray(newData[key])) {
mergedData[key] = newData[key];
} else if (
typeof prevData[key] === 'object' &&
typeof newData[key] === 'object'
) {
mergedData[key] = {
...(prevData[key] as object),
...(newData[key] as object),

View File

@ -26,8 +26,10 @@ import { upsertTransientItem } from '../../util/item';
import { ITEM_TYPE, ShapeStyle, SHAPE_TYPE } from '../../types/item';
import {
ThemeSpecification,
ItemThemeSpecifications,
ItemStyleSet,
NodeThemeSpecifications,
EdgeThemeSpecifications,
NodeStyleSet,
EdgeStyleSet,
} from '../../types/theme';
import { isArray, isObject } from '@antv/util';
import { DirectionalLight, AmbientLight } from '@antv/g-plugin-3d';
@ -277,7 +279,10 @@ export class ItemController {
const { isReplace, previous, current } = nodeUpdate[id];
// update the theme if the dataType value is changed
let themeStyles;
if (previous[nodeDataTypeField] !== current[nodeDataTypeField]) {
if (
nodeDataTypeField &&
previous[nodeDataTypeField] !== current[nodeDataTypeField]
) {
themeStyles = getThemeStyles(
this.nodeDataTypeSet,
nodeDataTypeField,
@ -488,7 +493,7 @@ export class ItemController {
return;
}
const shape = upsertShape(type, String(id), style, transientObjectMap);
const { shape } = upsertShape(type, String(id), style, transientObjectMap);
shape.style.pointerEvents = capture ? 'auto' : 'none';
canvas.getRoot().appendChild(shape);
}
@ -502,7 +507,7 @@ export class ItemController {
*/
private renderNodes(
models: NodeModel[],
nodeTheme: ItemThemeSpecifications = {},
nodeTheme: NodeThemeSpecifications = {},
) {
const { nodeExtensions, nodeGroup, nodeDataTypeSet, graph } = this;
const { dataTypeField } = nodeTheme;
@ -523,7 +528,7 @@ export class ItemController {
containerGroup: nodeGroup,
mapper: this.nodeMapper,
stateMapper: this.nodeStateMapper,
themeStyles: themeStyle,
themeStyles: themeStyle as NodeStyleSet,
device:
graph.rendererType === 'webgl-3d'
? // TODO: G type
@ -539,7 +544,7 @@ export class ItemController {
*/
private renderEdges(
models: EdgeModel[],
edgeTheme: ItemThemeSpecifications = {},
edgeTheme: EdgeThemeSpecifications = {},
) {
const { edgeExtensions, edgeGroup, itemMap, edgeDataTypeSet } = this;
const { dataTypeField } = edgeTheme;
@ -575,7 +580,7 @@ export class ItemController {
stateMapper: this.edgeStateMapper,
sourceItem,
targetItem,
themeStyles: themeStyle,
themeStyles: themeStyle as EdgeStyleSet,
});
});
}
@ -648,8 +653,8 @@ const getThemeStyles = (
dataTypeSet: Set<string>,
dataTypeField: string,
dataType: string,
itemTheme: ItemThemeSpecifications,
): ItemStyleSet => {
itemTheme: NodeThemeSpecifications | EdgeThemeSpecifications,
): NodeStyleSet | EdgeStyleSet => {
const { styles: themeStyles } = itemTheme;
if (!dataTypeField) {
// dataType field is not assigned

View File

@ -1,4 +1,4 @@
import { DisplayObject, Line, Polyline } from '@antv/g';
import { AABB, DisplayObject, Line, Polyline } from '@antv/g';
import { isNumber } from '@antv/util';
import {
DEFAULT_LABEL_BG_PADDING,
@ -10,31 +10,47 @@ import {
EdgeDisplayModel,
EdgeModelData,
EdgeShapeMap,
EdgeShapeStyles,
} from '../../../types/edge';
import {
GShapeStyle,
ItemShapeStyles,
SHAPE_TYPE,
ShapeStyle,
State,
} from '../../../types/item';
import { formatPadding, mergeStyles, upsertShape } from '../../../util/shape';
import {
formatPadding,
isStyleAffectBBox,
mergeStyles,
upsertShape,
} from '../../../util/shape';
export abstract class BaseEdge {
type: string;
defaultStyles: ItemShapeStyles = {};
themeStyles: ItemShapeStyles;
mergedStyles: ItemShapeStyles;
defaultStyles: EdgeShapeStyles = {};
themeStyles: EdgeShapeStyles;
mergedStyles: EdgeShapeStyles;
labelPosition: {
x: number;
y: number;
transform: string;
isRevert: boolean;
};
boundsCache: {
labelShapeGeometry?: AABB;
labelBackgroundShapeGeometry?: AABB;
};
constructor(props) {
const { themeStyles } = props;
if (themeStyles) this.themeStyles = themeStyles;
this.boundsCache = {};
}
private mergeStyles(model: EdgeDisplayModel) {
public mergeStyles(model: EdgeDisplayModel) {
this.mergedStyles = this.getMergedStyles(model);
}
public getMergedStyles(model: EdgeDisplayModel) {
const { data } = model;
const dataStyles = {} as ItemShapeStyles;
const dataStyles = {} as EdgeShapeStyles;
Object.keys(data).forEach((fieldName) => {
if (RESERVED_SHAPE_IDS.includes(fieldName))
dataStyles[fieldName] = data[fieldName] as ShapeStyle;
@ -79,16 +95,12 @@ export abstract class BaseEdge {
shapeMap: EdgeShapeMap,
diffData?: { previous: EdgeModelData; current: EdgeModelData },
diffState?: { previous: State[]; current: State[] },
): {
labelShape: DisplayObject;
[id: string]: DisplayObject;
} {
): DisplayObject {
const { keyShape } = shapeMap;
const { labelShape: shapeStyle } = this.mergedStyles;
const {
position,
background,
offsetX: propsOffsetX,
offsetY: propsOffsetY,
autoRotate = true,
@ -123,6 +135,7 @@ export abstract class BaseEdge {
positionPreset.pointRatio[0],
);
let positionStyle: any = { x: point.x, y: point.y };
let isRevert = false;
if (autoRotate) {
const pointOffset = (keyShape as Line | Polyline).getPoint(
positionPreset.pointRatio[1],
@ -130,6 +143,18 @@ export abstract class BaseEdge {
const angle = Math.atan(
(point.y - pointOffset.y) / (point.x - pointOffset.x),
); // TODO: NaN
// revert
isRevert = pointOffset.x < point.x;
if (isRevert) {
if (position === 'start') {
positionPreset.textAlign = 'right';
positionPreset.offsetX = -4;
} else if (position === 'end') {
positionPreset.textAlign = 'left';
positionPreset.offsetX = 4;
}
}
const offsetX = (
propsOffsetX === undefined ? positionPreset.offsetX : propsOffsetX
) as number;
@ -148,51 +173,88 @@ export abstract class BaseEdge {
transform: `rotate(${(angle / Math.PI) * 180})`,
};
}
this.labelPosition = {
...positionStyle,
isRevert,
};
const style = {
...this.defaultStyles.labelShape,
textAlign: positionPreset.textAlign,
...positionStyle,
...otherStyle,
};
const labelShape = upsertShape('text', 'labelShape', style, shapeMap);
const shapes = { labelShape };
if (background) {
const textBBox = labelShape.getGeometryBounds();
// TODO: update type define.
// @ts-ignore
const { padding: propsPadding, ...backgroundStyle } = background;
const padding = formatPadding(propsPadding, DEFAULT_LABEL_BG_PADDING);
const bgStyle = {
fill: '#fff',
radius: 4,
...backgroundStyle,
x: textBBox.min[0] - padding[3] + style.x,
y: textBBox.min[1] - padding[0] + style.y,
width: textBBox.max[0] - textBBox.min[0] + padding[1] + padding[3],
height: textBBox.max[1] - textBBox.min[1] + padding[0] + padding[2],
transform: positionStyle.transform,
transformOrigin: 'center',
};
if (position === 'start') {
bgStyle.transformOrigin = `${padding[3]} ${
padding[0] + bgStyle.height / 2
}`;
}
if (position === 'end') {
bgStyle.transformOrigin = `${padding[3] + bgStyle.width} ${
padding[0] + bgStyle.height / 2
}`;
}
shapes['labelBgShape'] = upsertShape(
'rect',
'labelBgShape',
bgStyle,
shapeMap,
);
const { shape, updateStyles } = this.upsertShape(
'text',
'labelShape',
style,
shapeMap,
);
if (isStyleAffectBBox('text', updateStyles)) {
this.boundsCache.labelShapeGeometry = shape.getGeometryBounds();
}
return shapes;
return shape;
}
public drawLabelBackgroundShape(
model: EdgeDisplayModel,
shapeMap: EdgeShapeMap,
diffData?: { previous: EdgeModelData; current: EdgeModelData },
diffState?: { previous: State[]; current: State[] },
): DisplayObject {
const { labelShape } = shapeMap;
if (!labelShape || !model.data.labelShape) return;
const { labelBackgroundShape, labelShape: labelShapeStyle } =
this.mergedStyles;
const textBBox =
this.boundsCache.labelShapeGeometry || labelShape.getGeometryBounds();
const { x, y, transform, isRevert } = this.labelPosition;
const { padding: propsPadding, ...backgroundStyle } = labelBackgroundShape;
const padding = formatPadding(propsPadding, DEFAULT_LABEL_BG_PADDING);
const textWidth = textBBox.max[0] - textBBox.min[0];
const textHeight = textBBox.max[1] - textBBox.min[1];
const bgStyle = {
fill: '#fff',
...backgroundStyle,
x: textBBox.min[0] - padding[3] + x,
y: textBBox.min[1] - padding[0] + y,
width: textWidth + padding[1] + padding[3],
height: textHeight + padding[0] + padding[2],
transform: transform,
};
if (labelShapeStyle.position === 'start') {
if (isRevert) {
bgStyle.transformOrigin = `${bgStyle.width - padding[1]} ${
bgStyle.height / 2
}`;
} else {
bgStyle.transformOrigin = `${padding[3]} ${bgStyle.height / 2}`;
}
} else if (labelShapeStyle.position === 'end') {
if (isRevert) {
bgStyle.transformOrigin = `${padding[3]} ${bgStyle.height / 2}`;
} else {
bgStyle.transformOrigin = `${bgStyle.width - padding[1]} ${
bgStyle.height / 2
}`;
}
} else {
bgStyle.transformOrigin = `${textWidth / 2 + padding[3]} ${
textHeight / 2 + padding[0]
}`;
}
const { shape, updateStyles } = this.upsertShape(
'rect',
'labelBackgroundShape',
bgStyle,
shapeMap,
);
if (isStyleAffectBBox('rect', updateStyles)) {
this.boundsCache.labelBackgroundShapeGeometry = shape.getGeometryBounds();
}
return shape;
}
public drawIconShape(
@ -201,69 +263,112 @@ export abstract class BaseEdge {
diffData?: { previous: EdgeModelData; current: EdgeModelData },
diffState?: { previous: State[]; current: State[] },
): DisplayObject {
const { labelShape, labelBgShape, keyShape } = shapeMap;
const { labelShape, labelBackgroundShape, keyShape } = shapeMap;
const { iconShape: shapeStyle, labelShape: labelShapeProps } =
this.mergedStyles;
const iconShapeType = shapeStyle.text ? 'text' : 'image';
if (iconShapeType === 'text') {
shapeStyle.textAlign = 'left';
shapeStyle.textBaseline = 'top';
}
const { width, height, fontSize } = shapeStyle;
const {
width,
height,
fontSize,
text,
offsetX = 0,
offsetY = 0,
} = shapeStyle;
const w = (width || fontSize) as number;
const h = (height || fontSize) as number;
const iconShapeType = text ? 'text' : 'image';
if (iconShapeType === 'text') {
shapeStyle.textAlign = 'left';
shapeStyle.textBaseline = 'top';
shapeStyle.fontSize = w;
} else {
shapeStyle.width = w;
shapeStyle.height = h;
}
if (labelShapeProps) {
const referShape = labelBgShape || labelShape;
const { min: referMin, halfExtents: referHalExtents } =
const referShape = labelBackgroundShape || labelShape;
const referBounds =
this.boundsCache.labelBackgroundShapeGeometry ||
this.boundsCache.labelShapeGeometry ||
referShape.getGeometryBounds();
const {
min: referMin,
max: referMax,
halfExtents: referHalExtents,
} = referBounds;
const referHeight = referMax[1] - referMin[1];
const referWidth = referMax[0] - referMin[0];
const {
x: referX,
y: referY,
transform: referTransform,
textAlign: labelAlign,
} = referShape.attributes;
shapeStyle.x = referMin[0] - w - 4 + referX;
shapeStyle.y = referMin[1] + 2 + referY;
const { textAlign: labelAlign } = labelShape.attributes;
shapeStyle.x = referMin[0] - w + 4 + referX + offsetX;
shapeStyle.y = referMin[1] + (referHeight - h) / 2 + referY + offsetY;
if (referTransform) {
shapeStyle.transform = referTransform;
if (labelAlign === 'right') {
shapeStyle.transformOrigin = `${w + 4 + referHalExtents[0] * 2}px ${
h / 2
}px`;
shapeStyle.transformOrigin = `${
referWidth / 2 - w / 2 + 4 + referHalExtents[0] - offsetX
} ${h / 2 - offsetY}`;
} else if (labelAlign === 'left') {
shapeStyle.transformOrigin = `${w + 4}px ${h / 2}px`;
shapeStyle.transformOrigin = `${w + 4 - offsetX} ${h / 2 - offsetY}`;
} else {
// labelShape align 'center'
shapeStyle.transformOrigin = `${w + 4 + referHalExtents[0]}px ${
h / 2
}px`;
shapeStyle.transformOrigin = `${(w + referWidth) / 2 - offsetX} ${
h / 2 - offsetY
}`;
}
}
} else {
const midPoint = (keyShape as Line | Polyline).getPoint(0.5);
shapeStyle.x = midPoint.x;
shapeStyle.y = midPoint.y;
shapeStyle.x = midPoint.x + offsetX;
shapeStyle.y = midPoint.y + offsetY;
// TODO: rotate
}
// TODO: update type define.
return upsertShape(
return this.upsertShape(
iconShapeType,
'iconShape',
shapeStyle as unknown as GShapeStyle,
shapeStyle as GShapeStyle,
shapeMap,
);
).shape;
}
public drawHaloShape(
model: EdgeDisplayModel,
shapeMap: EdgeShapeMap,
diffData?: { previous: EdgeModelData; current: EdgeModelData },
diffState?: { previous: State[]; current: State[] },
): DisplayObject {
const { keyShape } = shapeMap;
const { haloShape: haloShapeStyle } = this.mergedStyles;
const { nodeName, attributes } = keyShape;
return this.upsertShape(
nodeName as SHAPE_TYPE,
'haloShape',
{
...attributes,
...haloShapeStyle,
isBillboard: true,
},
shapeMap,
).shape;
}
public upsertShape(
type: SHAPE_TYPE,
id: string,
style: { [shapeAttr: string]: unknown },
style: ShapeStyle,
shapeMap: { [shapeId: string]: DisplayObject },
): DisplayObject {
// TODO: update type define.
return upsertShape(type, id, style as unknown as GShapeStyle, shapeMap);
): {
updateStyles: ShapeStyle;
shape: DisplayObject;
} {
return upsertShape(type, id, style as GShapeStyle, shapeMap);
}
}

View File

@ -1,3 +1,4 @@
import { isStyleAffectBBox } from 'util/shape';
import { Point } from '../../../types/common';
import {
EdgeDisplayModel,
@ -35,6 +36,7 @@ export class LineEdge extends BaseEdge {
const { data = {} } = model;
let shapes: EdgeShapeMap = { keyShape: undefined };
shapes.keyShape = this.drawKeyShape(
model,
sourcePoint,
@ -42,13 +44,27 @@ export class LineEdge extends BaseEdge {
shapeMap,
diffData,
);
if (data.labelShape)
shapes = {
...shapes,
...this.drawLabelShape(model, shapeMap, diffData),
};
if (data.iconShape)
if (data.haloShape) {
shapes.haloShape = this.drawHaloShape(model, shapeMap, diffData);
}
if (data.labelShape) {
shapes.labelShape = this.drawLabelShape(model, shapeMap, diffData);
}
// labelBackgroundShape
if (data.labelBackgroundShape) {
shapes.labelBackgroundShape = this.drawLabelBackgroundShape(
model,
shapeMap,
diffData,
);
}
if (data.iconShape) {
shapes.iconShape = this.drawIconShape(model, shapeMap, diffData);
}
// TODO: other shapes
@ -63,11 +79,9 @@ export class LineEdge extends BaseEdge {
diffState?: { previous: State[]; current: State[] },
) {
const { keyShape: keyShapeStyle } = this.mergedStyles;
const keyShape = this.upsertShape(
return this.upsertShape(
'line',
'keyShape',
// TODO: update type define.
// @ts-ignore
{
...keyShapeStyle,
x1: sourcePoint.x,
@ -79,7 +93,6 @@ export class LineEdge extends BaseEdge {
isBillboard: true,
},
shapeMap,
);
return keyShape;
).shape;
}
}

View File

@ -1,4 +1,4 @@
import { DisplayObject } from '@antv/g';
import { AABB, DisplayObject, ImageStyleProps, TextStyleProps } from '@antv/g';
import {
DEFAULT_LABEL_BG_PADDING,
OTHER_SHAPES_FIELD_NAME,
@ -7,34 +7,54 @@ import {
import { NodeDisplayModel } from '../../../types';
import {
GShapeStyle,
ItemShapeStyles,
SHAPE_TYPE,
SHAPE_TYPE_3D,
ShapeStyle,
State,
} from '../../../types/item';
import { NodeModelData, NodeShapeMap } from '../../../types/node';
import { formatPadding, mergeStyles, upsertShape } from '../../../util/shape';
import {
NodeModelData,
NodeShapeMap,
NodeShapeStyles,
} from '../../../types/node';
import {
formatPadding,
isStyleAffectBBox,
mergeStyles,
upsertShape,
} from '../../../util/shape';
export abstract class BaseNode {
type: string;
defaultStyles: ItemShapeStyles;
themeStyles: ItemShapeStyles;
mergedStyles: ItemShapeStyles;
defaultStyles: NodeShapeStyles;
themeStyles: NodeShapeStyles;
mergedStyles: NodeShapeStyles;
boundsCache: {
keyShapeLocal?: AABB;
labelShapeLocal?: AABB;
};
constructor(props) {
const { themeStyles } = props;
if (themeStyles) this.themeStyles = themeStyles;
this.boundsCache = {};
}
public mergeStyles(model: NodeDisplayModel) {
this.mergedStyles = this.getMergedStyles(model);
}
public getMergedStyles(model: NodeDisplayModel) {
const { data } = model;
const dataStyles = {} as ItemShapeStyles;
const dataStyles = {} as NodeShapeStyles;
Object.keys(data).forEach((fieldName) => {
if (RESERVED_SHAPE_IDS.includes(fieldName))
dataStyles[fieldName] = data[fieldName] as ShapeStyle;
else if (fieldName === OTHER_SHAPES_FIELD_NAME) {
if (RESERVED_SHAPE_IDS.includes(fieldName)) {
if (fieldName === 'BadgeShapes') {
Object.keys(data[fieldName]).forEach(
(badgeShapeId) =>
(dataStyles[badgeShapeId] = data[fieldName][badgeShapeId]),
);
} else {
dataStyles[fieldName] = data[fieldName] as ShapeStyle;
}
} else if (fieldName === OTHER_SHAPES_FIELD_NAME) {
Object.keys(data[fieldName]).forEach(
(otherShapeId) =>
(dataStyles[otherShapeId] = data[fieldName][otherShapeId]),
@ -81,17 +101,15 @@ export abstract class BaseNode {
shapeMap: NodeShapeMap,
diffData?: { previous: NodeModelData; current: NodeModelData },
diffState?: { oldState: State[]; newState: State[] },
): {
labelShape: DisplayObject;
[id: string]: DisplayObject;
} {
): DisplayObject {
const { keyShape } = shapeMap;
const keyShapeBox = keyShape.getGeometryBounds();
this.boundsCache.keyShapeLocal =
this.boundsCache.keyShapeLocal || keyShape.getLocalBounds();
const keyShapeBox = this.boundsCache.keyShapeLocal;
const { labelShape: shapeStyle } = this.mergedStyles;
const {
position,
background,
offsetX: propsOffsetX,
offsetY: propsOffsetY,
...otherStyle
@ -145,46 +163,60 @@ export abstract class BaseNode {
...positionPreset,
...otherStyle,
};
const labelShape = upsertShape('text', 'labelShape', style, shapeMap);
const shapes = { labelShape };
if (background) {
const textBBox = labelShape.getGeometryBounds();
// TODO: update type define.
// @ts-ignore
const { padding: propsPadding, ...backgroundStyle } = background;
const padding = formatPadding(propsPadding, DEFAULT_LABEL_BG_PADDING);
const bgStyle: any = {
fill: '#fff',
radius: 4,
...backgroundStyle,
x: textBBox.min[0] - padding[3] + style.x,
y: textBBox.min[1] - padding[0] + style.y,
width: textBBox.max[0] - textBBox.min[0] + padding[1] + padding[3],
height: textBBox.max[1] - textBBox.min[1] + padding[0] + padding[2],
};
if (style.stransform) {
bgStyle.transform = style.transform;
bgStyle.transformOrigin = 'center';
if (style.textAlign === 'left') {
bgStyle.transformOrigin = `${padding[3]} ${
padding[0] + bgStyle.height / 2
}`;
}
if (style.textAlign === 'right') {
bgStyle.transformOrigin = `${padding[3] + bgStyle.width} ${
padding[0] + bgStyle.height / 2
}`;
}
}
const { shape, updateStyles } = this.upsertShape(
'text',
'labelShape',
style,
shapeMap,
);
shapes['labelBgShape'] = upsertShape(
'rect',
'labelBgShape',
bgStyle,
shapeMap,
);
if (isStyleAffectBBox('text', updateStyles)) {
this.boundsCache.labelShapeLocal = undefined;
}
return shapes;
return shape;
}
public drawLabelBackgroundShape(
model: NodeDisplayModel,
shapeMap: NodeShapeMap,
diffData?: { previous: NodeModelData; current: NodeModelData },
diffState?: { oldState: State[]; newState: State[] },
): DisplayObject {
const { labelShape } = shapeMap;
if (!labelShape || !model.data.labelShape) return;
this.boundsCache.labelShapeLocal =
this.boundsCache.labelShapeLocal || labelShape.getLocalBounds();
const { labelShapeLocal: textBBox } = this.boundsCache;
const { padding: propsPadding, ...backgroundStyle } =
this.mergedStyles.labelBackgroundShape;
const padding = formatPadding(propsPadding, DEFAULT_LABEL_BG_PADDING);
const bgStyle: any = {
fill: '#fff',
...backgroundStyle,
x: textBBox.min[0] - padding[3],
y: textBBox.min[1] - padding[0],
width: textBBox.max[0] - textBBox.min[0] + padding[1] + padding[3],
height: textBBox.max[1] - textBBox.min[1] + padding[0] + padding[2],
};
const labelShapeAttr = labelShape.attributes;
if (labelShapeAttr.transform) {
bgStyle.transform = labelShapeAttr.transform;
bgStyle.transformOrigin = 'center';
if (labelShapeAttr.textAlign === 'left') {
bgStyle.transformOrigin = `${padding[3]} ${
padding[0] + bgStyle.height / 2
}`;
}
if (labelShapeAttr.textAlign === 'right') {
bgStyle.transformOrigin = `${padding[3] + bgStyle.width} ${
padding[0] + bgStyle.height / 2
}`;
}
}
return this.upsertShape('rect', 'labelBackgroundShape', bgStyle, shapeMap)
.shape;
}
public drawIconShape(
@ -193,26 +225,237 @@ export abstract class BaseNode {
diffData?: { previous: NodeModelData; current: NodeModelData },
diffState?: { oldState: State[]; newState: State[] },
): DisplayObject {
const { iconShape } = model.data || {};
const { iconShape: shapeStyle } = this.mergedStyles;
const iconShapeType = shapeStyle.text ? 'text' : 'image';
const {
width,
height,
fontSize,
text,
offsetX = 0,
offsetY = 0,
} = shapeStyle;
const w = (width || fontSize) as number;
const h = (height || fontSize) as number;
const iconShapeType = text ? 'text' : 'image';
if (iconShapeType === 'image') {
const { width, height } = shapeStyle;
if (!Object.prototype.hasOwnProperty.call(iconShape, 'x'))
shapeStyle.x = -width / 2;
if (!Object.prototype.hasOwnProperty.call(iconShape, 'y'))
shapeStyle.y = -height / 2;
shapeStyle.x = -w / 2 + offsetX;
shapeStyle.y = -h / 2 + offsetY;
shapeStyle.width = w;
shapeStyle.height = h;
} else {
shapeStyle.textAlign = 'center';
shapeStyle.textBaseline = 'middle';
shapeStyle.x = offsetX;
shapeStyle.y = offsetY;
shapeStyle.fontSize = w;
}
// TODO: update type define.
return upsertShape(
return this.upsertShape(
iconShapeType,
'iconShape',
shapeStyle as unknown as GShapeStyle,
shapeStyle as GShapeStyle,
shapeMap,
);
).shape;
}
public drawHaloShape(
model: NodeDisplayModel,
shapeMap: NodeShapeMap,
diffData?: { previous: NodeModelData; current: NodeModelData },
diffState?: { previous: State[]; current: State[] },
): DisplayObject {
const { keyShape } = shapeMap;
const { haloShape: haloShapeStyle } = this.mergedStyles;
const { nodeName, attributes } = keyShape;
return this.upsertShape(
nodeName as SHAPE_TYPE,
'haloShape',
{
...attributes,
...haloShapeStyle,
isBillboard: true,
},
shapeMap,
).shape;
}
public drawAnchorShapes(
model: NodeDisplayModel,
shapeMap: NodeShapeMap,
diffData?: { previous: NodeModelData; current: NodeModelData },
diffState?: { previous: State[]; current: State[] },
): {
[shapeId: string]: DisplayObject;
} {
const { anchorShapes: configs, keyShape: keyShapeStyle } =
this.mergedStyles;
const commonStyle = {};
const individualConfigs = [];
Object.keys(configs).forEach((key) => {
const value = configs[key];
if (typeof value === 'object' && value.position) {
individualConfigs.push(value);
} else {
commonStyle[key] = value;
}
});
if (!individualConfigs.length) return;
this.boundsCache.keyShapeLocal =
this.boundsCache.keyShapeLocal || shapeMap.keyShape.getLocalBounds();
const keyShapeBBox = this.boundsCache.keyShapeLocal;
const keyShapeWidth = keyShapeBBox.max[0] - keyShapeBBox.min[0];
const keyShapeHeight = keyShapeBBox.max[1] - keyShapeBBox.min[1];
const shapes = {};
individualConfigs.forEach((config, i) => {
const { position, fill = keyShapeStyle.fill, ...style } = config;
const id = `anchorShape${i}`;
shapes[id] = this.upsertShape(
'circle',
id,
{
cx: keyShapeWidth * (position[0] - 0.5),
cy: keyShapeHeight * (position[1] - 0.5),
fill,
...commonStyle,
...style,
} as GShapeStyle,
shapeMap,
).shape;
});
return shapes;
}
public drawBadgeShapes(
model: NodeDisplayModel,
shapeMap: NodeShapeMap,
diffData?: { previous: NodeModelData; current: NodeModelData },
diffState?: { previous: State[]; current: State[] },
): {
[shapeId: string]: DisplayObject;
} {
const configs = this.mergedStyles.badgeShapes;
const commonStyle = {};
const individualConfigs = [];
Object.keys(configs).forEach((key) => {
const value = configs[key];
if (typeof value === 'object' && value.position) {
individualConfigs.push(value);
} else {
commonStyle[key] = value;
}
});
if (!individualConfigs.length) return {};
this.boundsCache.keyShapeLocal =
this.boundsCache.keyShapeLocal || shapeMap.keyShape.getLocalBounds();
const { keyShapeLocal: keyShapeBBox } = this.boundsCache;
const keyShapeWidth = keyShapeBBox.max[0] - keyShapeBBox.min[0];
const shapes = {};
individualConfigs.forEach((config) => {
const { position, ...individualStyle } = config;
const id = `${position}BadgeShape`;
const style = {
...commonStyle,
...individualStyle,
};
const {
text = '',
type,
color,
size = keyShapeWidth / 3,
textColor,
zIndex = 2,
offsetX = 0,
offsetY = 0,
...otherStyles
} = style;
const bgHeight = size as number;
let pos = { x: 0, y: 0 };
switch (position) {
case 'rightTop':
pos.x = keyShapeBBox.max[0] - bgHeight / 2 + offsetX;
pos.y = keyShapeBBox.min[1] + size / 4 + offsetY;
break;
case 'right':
pos.x = keyShapeBBox.max[0] - bgHeight / 2 + offsetX;
pos.y = offsetY;
break;
case 'rightBottom':
case 'bottomRight':
pos.x = keyShapeBBox.max[0] - bgHeight / 2 + offsetX;
pos.y = keyShapeBBox.max[1] - size / 4 + offsetY;
break;
case 'leftTop':
case 'topLeft':
pos.x = keyShapeBBox.min[0] + bgHeight / 2 + offsetX;
pos.y = keyShapeBBox.min[1] + size / 4 + offsetY;
break;
case 'left':
pos.x = keyShapeBBox.min[0] + bgHeight / 2 + offsetX;
pos.y = offsetY;
break;
case 'leftBottom':
case 'bottomLeft':
pos.x = keyShapeBBox.min[0] + bgHeight / 2 + offsetX;
pos.y = keyShapeBBox.max[1] - size / 4 + offsetY;
break;
case 'top':
pos.x = offsetX;
pos.y = keyShapeBBox.min[1] + size / 4;
break;
default:
// bottom
pos.x = offsetX;
pos.y = keyShapeBBox.max[1] - size / 4;
break;
}
// a radius rect (as container) + a text / icon
shapes[id] = this.upsertShape(
'text',
id,
{
text,
fill: textColor,
fontSize: bgHeight - 2,
x: pos.x,
y: pos.y,
...otherStyles,
textAlign: position.includes('right') ? 'left' : 'right',
textBaseline: 'middle',
zIndex: (zIndex as number) + 1,
} as GShapeStyle,
shapeMap,
).shape;
const bbox = shapes[id].getLocalBounds();
const bgShapeId = `${position}BadgeBackgroundShape`;
const bgWidth =
(text as string).length <= 1
? bgHeight
: Math.max(bgHeight, bbox.max[0] - bbox.min[0]) + 8;
shapes[bgShapeId] = this.upsertShape(
'rect',
bgShapeId,
{
text,
fill: color,
height: bgHeight,
width: bgWidth,
x: bbox.min[0] - 3, // begin at the border, minus half height
y: bbox.min[1],
radius: bgHeight / 2,
zIndex,
...otherStyles,
} as GShapeStyle,
shapeMap,
).shape;
});
return shapes;
}
public drawOtherShapes(
@ -227,15 +470,12 @@ export abstract class BaseNode {
public upsertShape(
type: SHAPE_TYPE | SHAPE_TYPE_3D,
id: string,
style: { [shapeAttr: string]: unknown },
style: ShapeStyle,
shapeMap: { [shapeId: string]: DisplayObject },
): DisplayObject {
// TODO: update type define.
return upsertShape(
type as SHAPE_TYPE,
id,
style as unknown as GShapeStyle,
shapeMap,
);
): {
updateStyles: ShapeStyle;
shape: DisplayObject;
} {
return upsertShape(type as SHAPE_TYPE, id, style as GShapeStyle, shapeMap);
}
}

View File

@ -6,18 +6,22 @@ import {
ItemShapeStyles,
SHAPE_TYPE,
SHAPE_TYPE_3D,
ShapeStyle,
State,
} from '../../../types/item';
import { NodeModelData, NodeShapeMap } from '../../../types/node';
import { formatPadding, mergeStyles, upsertShape } from '../../../util/shape';
import {
NodeModelData,
NodeShapeMap,
NodeShapeStyles,
} from '../../../types/node';
import { upsertShape3D } from '../../../util/shape3d';
import { BaseNode } from './base';
export abstract class BaseNode3D extends BaseNode {
type: string;
defaultStyles: ItemShapeStyles;
themeStyles: ItemShapeStyles;
mergedStyles: ItemShapeStyles;
themeStyles: NodeShapeStyles;
mergedStyles: NodeShapeStyles;
device: any; // for 3d renderer
constructor(props) {
super(props);
@ -30,110 +34,8 @@ export abstract class BaseNode3D extends BaseNode {
shapeMap: NodeShapeMap,
diffData?: { previous: NodeModelData; current: NodeModelData },
diffState?: { oldState: State[]; newState: State[] },
): {
labelShape: DisplayObject;
[id: string]: DisplayObject;
} {
const { keyShape } = shapeMap;
const keyShapeBox = keyShape.getGeometryBounds();
const { labelShape: shapeStyle } = this.mergedStyles;
const {
position,
background,
offsetX: propsOffsetX,
offsetY: propsOffsetY,
...otherStyle
} = shapeStyle;
const positionPreset = {
x: keyShapeBox.center[0],
y: keyShapeBox.max[1],
textBaseline: 'top',
textAlign: 'center',
offsetX: 0,
offsetY: 0,
};
switch (position) {
case 'center':
positionPreset.y = keyShapeBox.center[1];
break;
case 'top':
positionPreset.y = keyShapeBox.min[1];
positionPreset.textBaseline = 'bottom';
positionPreset.offsetY = -4;
break;
case 'left':
positionPreset.x = keyShapeBox.min[0];
positionPreset.y = keyShapeBox.center[1];
positionPreset.textAlign = 'right';
positionPreset.textBaseline = 'middle';
positionPreset.offsetX = -4;
break;
case 'right':
positionPreset.x = keyShapeBox.max[0];
positionPreset.y = keyShapeBox.center[1];
positionPreset.textAlign = 'left';
positionPreset.textBaseline = 'middle';
positionPreset.offsetX = 4;
break;
default: // at bottom by default
positionPreset.offsetY = 4;
break;
}
const offsetX = (
propsOffsetX === undefined ? positionPreset.offsetX : propsOffsetX
) as number;
const offsetY = (
propsOffsetY === undefined ? positionPreset.offsetY : propsOffsetY
) as number;
positionPreset.x += offsetX;
positionPreset.y += offsetY;
const style: any = {
...this.defaultStyles.labelShape,
...positionPreset,
...otherStyle,
};
const labelShape = upsertShape('text', 'labelShape', style, shapeMap);
const shapes = { labelShape };
if (background) {
const textBBox = labelShape.getGeometryBounds();
// TODO: update type define.
// @ts-ignore
const { padding: propsPadding, ...backgroundStyle } = background;
const padding = formatPadding(propsPadding, DEFAULT_LABEL_BG_PADDING);
const bgStyle: any = {
fill: '#fff',
radius: 4,
...backgroundStyle,
x: textBBox.min[0] - padding[3] + style.x,
y: textBBox.min[1] - padding[0] + style.y,
width: textBBox.max[0] - textBBox.min[0] + padding[1] + padding[3],
height: textBBox.max[1] - textBBox.min[1] + padding[0] + padding[2],
};
if (style.stransform) {
bgStyle.transform = style.transform;
bgStyle.transformOrigin = 'center';
if (style.textAlign === 'left') {
bgStyle.transformOrigin = `${padding[3]} ${
padding[0] + bgStyle.height / 2
}`;
}
if (style.textAlign === 'right') {
bgStyle.transformOrigin = `${padding[3] + bgStyle.width} ${
padding[0] + bgStyle.height / 2
}`;
}
}
shapes['labelBgShape'] = upsertShape(
'rect',
'labelBgShape',
bgStyle,
shapeMap,
);
}
return shapes;
): DisplayObject {
return super.drawLabelShape(model, shapeMap, diffData, diffState);
}
// TODO: 3d icon? - billboard image or text for alpha
@ -143,18 +45,59 @@ export abstract class BaseNode3D extends BaseNode {
diffData?: { previous: NodeModelData; current: NodeModelData },
diffState?: { oldState: State[]; newState: State[] },
): DisplayObject {
const { iconShape } = model.data || {};
const { iconShape: shapeStyle } = this.mergedStyles;
const iconShapeType = shapeStyle.text ? 'text' : 'image';
if (iconShapeType === 'image') {
const { width, height } = shapeStyle;
if (!iconShape.hasOwnProperty('x')) shapeStyle.x = -width / 2;
if (!iconShape.hasOwnProperty('y')) shapeStyle.y = -height / 2;
} else {
shapeStyle.textAlign = 'center';
shapeStyle.textBaseline = 'middle';
}
return this.upsertShape(iconShapeType, 'iconShape', shapeStyle, shapeMap);
return super.drawIconShape(model, shapeMap, diffData, diffState);
}
// TODO: 3d billboard
public drawHaloShape(
model: NodeDisplayModel,
shapeMap: NodeShapeMap,
diffData?: { previous: NodeModelData; current: NodeModelData },
diffState?: { previous: State[]; current: State[] },
): DisplayObject {
const { keyShape } = shapeMap;
const { haloShape: haloShapeStyle } = this.mergedStyles;
const { nodeName, attributes } = keyShape;
return this.upsertShape(
nodeName as SHAPE_TYPE,
'haloShape',
{
...attributes,
...haloShapeStyle,
isBillboard: true,
},
shapeMap,
).shape;
}
/**
* 3D node does not support anchor shapes.
* @param model
* @param shapeMap
* @param diffData
* @param diffState
* @returns
*/
public drawAnchorShapes(
model: NodeDisplayModel,
shapeMap: NodeShapeMap,
diffData?: { previous: NodeModelData; current: NodeModelData },
diffState?: { previous: State[]; current: State[] },
): {
[shapeId: string]: DisplayObject;
} {
return {};
}
public drawBadgeShapes(
model: NodeDisplayModel,
shapeMap: NodeShapeMap,
diffData?: { previous: NodeModelData; current: NodeModelData },
diffState?: { previous: State[]; current: State[] },
): {
[shapeId: string]: DisplayObject;
} {
return super.drawBadgeShapes(model, shapeMap, diffData, diffState);
}
// TODO: 3d shapes?
@ -171,15 +114,12 @@ export abstract class BaseNode3D extends BaseNode {
public upsertShape(
type: SHAPE_TYPE_3D | SHAPE_TYPE,
id: string,
style: { [shapeAttr: string]: unknown },
style: ShapeStyle,
shapeMap: { [shapeId: string]: DisplayObject },
): DisplayObject {
return upsertShape3D(
type,
id,
style as unknown as GShapeStyle,
shapeMap,
this.device,
);
): {
shape: DisplayObject;
updateStyles: ShapeStyle;
} {
return upsertShape3D(type, id, style as GShapeStyle, shapeMap, this.device);
}
}

View File

@ -1,18 +1,23 @@
import { DisplayObject } from '@antv/g';
import { NodeDisplayModel } from '../../../types';
import { GShapeStyle, ItemShapeStyles, State } from '../../../types/item';
import { NodeModelData, NodeShapeMap } from '../../../types/node';
import { State } from '../../../types/item';
import {
NodeModelData,
NodeShapeMap,
NodeShapeStyles,
} from '../../../types/node';
import { BaseNode } from './base';
import { isStyleAffectBBox } from 'util/shape';
export class CircleNode extends BaseNode {
override defaultStyles = {
keyShape: {
r: 15,
r: 16,
x: 0,
y: 0,
},
};
mergedStyles: ItemShapeStyles;
mergedStyles: NodeShapeStyles;
constructor(props) {
super(props);
// suggest to merge default styles like this to avoid style value missing
@ -27,16 +32,62 @@ export class CircleNode extends BaseNode {
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,
...this.drawLabelShape(model, shapeMap, diffData),
...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,
};
}
// otherShapes
if (data.otherShapes && this.drawOtherShapes) {
shapes = {
...shapes,
@ -52,11 +103,15 @@ export class CircleNode extends BaseNode {
diffData?: { previous: NodeModelData; current: NodeModelData },
diffState?: { previous: State[]; current: State[] },
): DisplayObject {
return this.upsertShape(
const { shape, updateStyles } = this.upsertShape(
'circle',
'keyShape',
this.mergedStyles.keyShape,
shapeMap,
);
if (isStyleAffectBBox('circle', updateStyles)) {
this.boundsCache.keyShapeLocal = shape.getLocalBounds();
}
return shape;
}
}

View File

@ -1,7 +1,11 @@
import { DisplayObject } from '@antv/g';
import { NodeDisplayModel } from '../../../types';
import { ItemShapeStyles, State } from '../../../types/item';
import { NodeModelData, NodeShapeMap } from '../../../types/node';
import { State } from '../../../types/item';
import {
NodeModelData,
NodeShapeMap,
NodeShapeStyles,
} from '../../../types/node';
import { BaseNode3D } from './base3d';
export class SphereNode extends BaseNode3D {
@ -15,7 +19,7 @@ export class SphereNode extends BaseNode3D {
z: 0,
},
};
mergedStyles: ItemShapeStyles;
mergedStyles: NodeShapeStyles;
constructor(props) {
super(props);
}
@ -28,16 +32,48 @@ export class SphereNode extends BaseNode3D {
const { data = {} } = model;
let shapes: NodeShapeMap = { keyShape: undefined };
// keyShape
shapes.keyShape = this.drawKeyShape(model, shapeMap, diffData);
if (data.labelShape) {
shapes = {
...shapes,
...this.drawLabelShape(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,
);
}
// 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,
};
}
// otherShapes
if (data.otherShapes && this.drawOtherShapes) {
shapes = {
...shapes,
@ -58,6 +94,6 @@ export class SphereNode extends BaseNode3D {
'keyShape',
this.mergedStyles.keyShape,
shapeMap,
);
).shape;
}
}

View File

@ -102,7 +102,7 @@ export default {
...DEFAULT_SHAPE_STYLE,
lineWidth: 1,
stroke: edgeMainStroke,
lineAppendWidth: 2,
increasedLineWidthForHitTesting: 2,
},
labelShape: {
...DEFAULT_TEXT_STYLE,

View File

@ -1,75 +1,107 @@
import { DEFAULT_SHAPE_STYLE, DEFAULT_TEXT_STYLE } from '../../constant';
import { ThemeSpecification } from '../../types/theme';
const subjectColor = 'rgb(95, 149, 255)';
const textColor = 'rgb(0, 0, 0)';
const subjectColor = 'rgb(34,126,255)';
const textColor = 'rgba(0,0,0,0.85)';
const activeFill = 'rgb(247, 250, 255)';
const nodeMainFill = 'rgb(239, 244, 255)';
const nodeColor = 'rgb(34,126,255)';
const edgeColor = 'rgb(153, 173, 209)';
const comboFill = 'rgb(253, 253, 253)';
const disabledFill = 'rgb(250, 250, 250)';
const disabledFill = 'rgb(240, 240, 240)';
const edgeMainStroke = 'rgb(224, 224, 224)';
const edgeInactiveStroke = 'rgb(234, 234, 234)';
const edgeDisablesStroke = 'rgb(245, 245, 245)';
const inactiveStroke = 'rgb(191, 213, 255)';
const edgeMainStroke = 'rgb(153, 173, 209)';
const edgeDisableStroke = 'rgb(217, 217, 217)';
const edgeInactiveStroke = 'rgb(210, 218, 233)';
const highlightStroke = '#4572d9';
const highlightFill = 'rgb(223, 234, 255)';
const nodeStroke = 'rgba(0,0,0,0.85)';
const haloStroke = 'rgb(0, 0, 0)';
export default {
node: {
palette: [],
palette: [
'#227EFF',
'#AD5CFF',
'#00B8B8',
'#FA822D',
'#F252AF',
'#1EB8F5',
'#108A44',
'#F4B106',
'#5241A8',
'#95CF21',
],
styles: [
{
default: {
keyShape: {
...DEFAULT_SHAPE_STYLE,
r: 10,
fill: nodeMainFill,
stroke: subjectColor,
lineWidth: 1,
r: 16,
fill: nodeColor,
lineWidth: 0,
zIndex: 0,
},
labelShape: {
...DEFAULT_TEXT_STYLE,
fill: '#000',
position: 'bottom',
offsetY: 4,
zIndex: 2,
},
labelBackgroundShape: {
padding: [4, 4, 4, 4],
lineWidth: 0,
fill: '#fff',
opacity: 0.75,
zIndex: -1,
},
iconShape: {
...DEFAULT_TEXT_STYLE,
fill: '#333',
img: 'https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*wAmHQJbNVdwAAAAAAAAAAABkARQnAQ',
width: 15,
height: 15,
fill: '#fff',
fontSize: 16,
zIndex: 1,
},
anchorShapes: {
lineWidth: 1,
stroke: 'rgba(0, 0, 0, 0.65)',
zIndex: 2,
r: 3,
},
badgeShapes: {
color: 'rgb(140, 140, 140)',
textColor: '#fff',
zIndex: 2,
},
},
selected: {
keyShape: {
fill: nodeMainFill,
stroke: subjectColor,
lineWidth: 4,
shadowColor: subjectColor,
shadowBlur: 10,
stroke: nodeStroke,
lineWidth: 3,
},
labelShape: {
fontWeight: 500,
},
haloShape: {
stroke: haloStroke,
opacity: 0.06,
lineWidth: 20,
},
},
active: {
keyShape: {
fill: activeFill,
stroke: subjectColor,
shadowColor: subjectColor,
stroke: nodeStroke,
lineWidth: 2,
shadowBlur: 10,
},
haloShape: {
stroke: haloStroke,
opacity: 0.06,
lineWidth: 4,
zIndex: -1,
},
},
highlight: {
keyShape: {
fill: highlightFill,
stroke: highlightStroke,
lineWidth: 2,
stroke: nodeStroke,
lineWidth: 3,
},
labelShape: {
fontWeight: 500,
@ -77,23 +109,37 @@ export default {
},
inactive: {
keyShape: {
fill: activeFill,
stroke: inactiveStroke,
lineWidth: 1,
opacity: 0.25,
},
labelShape: {
opacity: 0.25,
},
iconShape: {
opacity: 0.25,
},
},
disable: {
keyShape: {
fill: disabledFill,
stroke: edgeMainStroke,
lineWidth: 1,
lineWidth: 0,
},
},
},
],
},
edge: {
palette: [],
palette: [
'#63A4FF',
'#CD9CFF',
'#2DEFEF',
'#FFBDA1',
'#F49FD0',
'#80DBFF',
'#41CB7C',
'#FFD362',
'#A192E8',
'#CEFB75',
],
styles: [
{
default: {
@ -101,42 +147,57 @@ export default {
...DEFAULT_SHAPE_STYLE,
lineWidth: 1,
stroke: edgeMainStroke,
lineAppendWidth: 2,
increasedLineWidthForHitTesting: 2,
},
labelShape: {
...DEFAULT_TEXT_STYLE,
fill: textColor,
position: 'middle',
textBaseline: 'middle',
zIndex: 2,
},
labelBackgroundShape: {
padding: [4, 4, 4, 4],
lineWidth: 0,
fill: '#fff',
opacity: 0.75,
zIndex: 1,
},
iconShape: {
...DEFAULT_TEXT_STYLE,
fill: '#333',
img: 'https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*wAmHQJbNVdwAAAAAAAAAAABkARQnAQ',
width: 15,
height: 15,
fill: 'rgb(140, 140, 140)',
fontSize: 16,
zIndex: 2,
offsetX: -10,
},
},
selected: {
keyShape: {
stroke: subjectColor,
lineWidth: 2,
shadowColor: subjectColor,
shadowBlur: 10,
},
labelShape: {
fontWeight: 500,
},
haloShape: {
stroke: haloStroke,
opacity: 0.06,
lineWidth: 12,
zIndex: -1,
},
},
active: {
keyShape: {
stroke: subjectColor,
lineWidth: 1,
},
haloShape: {
stroke: haloStroke,
opacity: 0.06,
lineWidth: 12,
zIndex: -1,
},
},
highlight: {
keyShape: {
stroke: subjectColor,
lineWidth: 2,
},
labelShape: {
@ -151,7 +212,7 @@ export default {
},
disable: {
keyShape: {
stroke: edgeDisablesStroke,
stroke: edgeDisableStroke,
lineWidth: 1,
},
},
@ -176,11 +237,9 @@ export default {
},
selected: {
keyShape: {
stroke: subjectColor,
stroke: nodeStroke,
fill: comboFill,
shadowColor: subjectColor,
lineWidth: 2,
shadowBlur: 10,
},
labelShape: {
fontWeight: 500,
@ -188,14 +247,13 @@ export default {
},
active: {
keyShape: {
stroke: subjectColor,
stroke: nodeStroke,
lineWidth: 1,
fill: activeFill,
},
},
highlight: {
keyShape: {
stroke: highlightStroke,
stroke: nodeStroke,
fill: comboFill,
lineWidth: 2,
},
@ -212,7 +270,6 @@ export default {
},
disable: {
keyShape: {
stroke: edgeInactiveStroke,
fill: disabledFill,
lineWidth: 1,
},

View File

@ -1,7 +1,12 @@
import { isArray } from '@antv/util';
import { ItemStyleSets, ThemeSpecification } from '../../types/theme';
import {
NodeStyleSets,
EdgeStyleSets,
ThemeSpecification,
} from '../../types/theme';
import { mergeStyles } from '../../util/shape';
import BaseThemeSolver, { ThemeSpecificationMap } from './base';
import { GraphData } from 'types';
interface SpecThemeSolverOptions {
base: 'light' | 'dark';
@ -9,17 +14,17 @@ interface SpecThemeSolverOptions {
node?: {
dataTypeField?: string;
palette: string[] | { [dataType: string]: string };
getStyleSets: (palette) => ItemStyleSets;
getStyleSets: (palette) => NodeStyleSets;
};
edge?: {
dataTypeField?: string;
palette: string[] | { [dataType: string]: string };
getStyleSets: (palette) => ItemStyleSets;
getStyleSets: (palette) => EdgeStyleSets;
};
combo?: {
dataTypeField?: string;
palette: string[] | { [dataType: string]: string };
getStyleSets: (palette) => ItemStyleSets;
getStyleSets: (palette) => NodeStyleSets;
};
canvas?: {
[cssName: string]: unknown;
@ -40,14 +45,28 @@ export default class SpecThemeSolver extends BaseThemeSolver {
if (specification) {
['node', 'edge', 'combo'].forEach((itemType) => {
if (!specification[itemType]) return;
const { palette, dataTypeField, getStyleSets } =
specification[itemType];
let {
palette = mergedSpec[itemType].palette,
dataTypeField,
getStyleSets,
} = specification[itemType];
if (dataTypeField && !getStyleSets) {
getStyleSets = (paletteProps) => {
return paletteProps.map((color) => ({
default: {
keyShape:
itemType === 'edge' ? { stroke: color } : { fill: color },
},
}));
};
}
// merge the custom part spec and the built-in spec
const {
styles: [baseStyles],
} = baseSpec[itemType];
const incomingStyles = getStyleSets(palette);
const incomingStyles = getStyleSets?.(palette) || {};
let mergedStyles;
if (isArray(incomingStyles)) {
mergedStyles = incomingStyles.map((incomingStyle) => {

View File

@ -2,14 +2,15 @@ import { Node as GNode, PlainObject } from '@antv/graphlib';
import { AnimateAttr } from './animate';
import { Padding } from './common';
import {
BadgePosition,
Encode,
IItem,
ItemShapeStyles,
LabelBackground,
ShapeAttrEncode,
ShapesEncode,
ShapeStyle,
} from './item';
import { AnchorPoint } from './node';
export type ComboLabelPosition =
| 'bottom'
@ -32,28 +33,56 @@ export interface ComboUserModelData extends PlainObject {
export interface ComboModelData extends ComboUserModelData {
visible?: boolean;
label?: string;
}
export interface ComboLabelShapeStyle extends ShapeStyle {
position?: ComboLabelPosition;
offsetX?: number;
offsetY?: number;
background?: LabelBackground;
}
/** Displayed data, only for drawing and not received by users. */
export interface ComboDisplayModelData extends ComboModelData {
keyShape?: ShapeStyle;
labelShape?: ComboLabelShapeStyle;
iconShape?: ShapeStyle;
otherShapes?: {
[shapeId: string]: ShapeStyle;
};
anchorPoints?: AnchorPoint[];
anchorPoints?: number[][];
fixSize?: number | number[];
padding?: Padding;
}
export interface ComboShapeStyles extends ItemShapeStyles {
keyShape?: ShapeStyle & {
padding?: number | number[];
};
labelShape?: ShapeStyle & {
position?: ComboLabelPosition;
offsetX?: number;
offsetY?: number;
};
labelBackgroundShape?: ShapeStyle & {
padding?: number | number[];
};
// common badge styles
badgeShapes?: ShapeStyle & {
color?: string;
textColor?: string;
// individual styles and their position
[key: number]: ShapeStyle & {
position?: BadgePosition;
color?: string;
textColor?: string;
};
};
// common badge styles
anchorShapes?: ShapeStyle & {
color?: string;
textColor?: string;
size?: number;
offsetX?: number;
offsetY?: number;
// individual styles and their position
[key: number]: ShapeStyle & {
position?: BadgePosition;
color?: string;
textColor?: string;
size?: number;
offsetX?: number;
offsetY?: number;
};
};
}
/** Displayed data, only for drawing and not received by users. */
export type ComboDisplayModelData = ComboModelData & ComboShapeStyles;
/** User input model. */
export type ComboUserModel = GNode<ComboUserModelData>;
@ -72,7 +101,7 @@ interface ComboLabelShapeAttrEncode extends ShapeAttrEncode {
}
export interface ComboShapesEncode extends ShapesEncode {
labelShape?: ComboLabelShapeAttrEncode;
anchorPoints?: AnchorPoint[] | Encode<AnchorPoint[]>;
anchorPoints?: number[][] | Encode<number[][]>;
fixSize?: number | number[] | Encode<number | number[]>;
padding?: Padding | Encode<Padding>;
}

View File

@ -1,38 +1,77 @@
import { DisplayObject } from '@antv/g';
import { Edge as GEdge, PlainObject } from '@antv/graphlib';
import { AnimateAttr } from './animate';
import {
BadgePosition,
Encode,
IItem,
ItemShapeStyles,
LabelBackground,
ShapeAttrEncode,
ShapesEncode,
ShapeStyle,
} from './item';
export type EdgeUserModelData = PlainObject;
export interface EdgeModelData extends EdgeUserModelData {
visible?: boolean;
label?: string;
}
export interface EdgeLabelShapeStyle extends ShapeStyle {
position?: EdgeLabelPosition;
offsetX?: number;
offsetY?: number;
background?: LabelBackground;
autoRotate?: boolean;
}
export interface EdgeDisplayModelData extends EdgeModelData {
keyShape?: ShapeStyle;
labelShape?: EdgeLabelShapeStyle;
iconShape?: ShapeStyle;
otherShapes?: {
[shapeId: string]: ShapeStyle;
};
export interface EdgeUserModelData extends PlainObject {
/**
* Anchor index to link to the source / target node.
*/
sourceAnchor?: number;
targetAnchor?: number;
/**
* Edge type, e.g. 'line'.
*/
type?: string;
/**
* Subject color for the keyShape and arrows.
* More styles should be configured in edge mapper.
*/
color?: string;
/**
* The text to show on the edge.
* More styles should be configured in edge mapper.
*/
label?: string;
/**
* Whether show the edge by default.
*/
visible?: boolean;
/**
* The icon to show on the edge.
* More styles should be configured in edge mapper.
*/
icon?: {
type: 'icon' | 'text';
text?: string;
img?: string;
};
/**
* The badges to show on the edge.
* More styles should be configured in edge mapper.
*/
badge?: {
type: 'icon' | 'text';
text: string;
};
}
export interface EdgeModelData extends EdgeUserModelData {}
export interface EdgeShapeStyles extends ItemShapeStyles {
labelShape?: ShapeStyle & {
position?: 'start' | 'middle' | 'end';
offsetX?: number;
offsetY?: number;
autoRotate?: boolean;
};
labelBackgroundShape?: ShapeStyle & {
padding?: number | number[];
};
badgeShape?: ShapeStyle & {
color?: string;
textColor?: string;
};
}
export type EdgeDisplayModelData = EdgeModelData & EdgeShapeStyles;
/** User input data. */
export type EdgeUserModel = GEdge<EdgeUserModelData>;
@ -54,8 +93,8 @@ interface EdgeLabelShapeAttrEncode extends ShapeAttrEncode {
export interface EdgeShapesEncode extends ShapesEncode {
labelShape?: EdgeLabelShapeAttrEncode;
sourceAnchor?: number;
targetAnchor?: number;
labelBackgroundShape?: LabelBackground | Encode<LabelBackground>;
badgeShape?: ShapeAttrEncode | Encode<ShapeStyle>;
}
export interface EdgeEncode extends EdgeShapesEncode {
type?: string | Encode<string>;

View File

@ -40,16 +40,6 @@ import {
TorusGeometryProps,
} from '@antv/g-plugin-3d';
export interface ShapeStyle {
[shapeAttr: string]: unknown;
animate?: AnimateAttr;
x?: number;
y?: number;
width?: number;
height?: number;
r?: number;
}
export type GShapeStyle = CircleStyleProps &
RectStyleProps &
EllipseStyleProps &
@ -63,6 +53,11 @@ export type GShapeStyle = CircleStyleProps &
CubeGeometryProps &
PlaneGeometryProps;
export type ShapeStyle = Partial<
GShapeStyle & {
animate?: AnimateAttr;
}
>;
export interface Encode<T> {
fields: string[];
formatter: (values: NodeUserModel | EdgeUserModel | ComboUserModel) => T;
@ -82,8 +77,8 @@ export interface LabelBackground {
}
export interface ShapesEncode {
keyShape?: ShapeAttrEncode;
iconShape?: ShapeAttrEncode;
keyShape?: ShapeAttrEncode | Encode<ShapeStyle>;
iconShape?: ShapeAttrEncode | Encode<ShapeStyle>;
otherShapes?: {
[shapeId: string]: {
[shapeAtrr: string]: unknown | Encode<unknown>;
@ -126,12 +121,35 @@ export type State = {
value: boolean | string;
};
export enum BadgePosition {
rightTop = 'rightTop',
right = 'right',
rightBottom = 'rightBottom',
bottomRight = 'bottomRight',
bottom = 'bottom',
bottomLeft = 'bottomLeft',
leftBottom = 'leftBottom',
left = 'left',
leftTop = 'leftTop',
topLeft = 'topLeft',
top = 'top',
topRight = 'topRight',
}
export type IBadgePosition = `${BadgePosition}`;
/** Shape styles for an item. */
export type ItemShapeStyles = {
// labelShape, labelBackgroundShape, badgeShapes, overwrote by node / edge / combo
// anchorShapes, overwrote by node / combo
keyShape?: ShapeStyle;
labelShape?: ShapeStyle;
iconShape?: ShapeStyle;
[shapeId: string]: ShapeStyle;
iconShape?: Partial<
TextStyleProps &
ImageStyleProps & {
offsetX?: number;
offsetY?: number;
}
>;
haloShape?: ShapeStyle;
};
/**

View File

@ -1,9 +1,11 @@
import { DisplayObject } from '@antv/g';
import { DisplayObject, Point } from '@antv/g';
import { Node as GNode, PlainObject } from '@antv/graphlib';
import { AnimateAttr } from './animate';
import {
BadgePosition,
Encode,
IItem,
ItemShapeStyles,
LabelBackground,
ShapeAttrEncode,
ShapesEncode,
@ -14,32 +16,104 @@ export type NodeLabelPosition = 'bottom' | 'center' | 'top' | 'left' | 'right';
/** Data in user input model. */
export interface NodeUserModelData extends PlainObject {
parentId?: string;
}
/** Data in inner model. */
export interface NodeModelData extends NodeUserModelData {
visible?: boolean;
/**
* Node position.
*/
x?: number;
y?: number;
z?: number;
/**
* Node type, e.g. 'circle'.
*/
type?: string;
/**
* Subject color for the keyShape and anchor points.
* More styles should be configured in node mapper.
*/
color?: string;
/**
* The text to show on the node.
* More styles should be configured in node mapper.
*/
label?: string;
/**
* Whether show the node by default.
*/
visible?: boolean;
/**
* Reserved for combo.
*/
parentId?: string;
/**
* The icon to show on the node.
* More styles should be configured in node mapper.
*/
icon?: {
type: 'icon' | 'text';
text?: string;
img?: string;
};
/**
* The ratio position of the keyShape for related edges linking into.
* More styles should be configured in node mapper.
*/
anchorPoints?: number[][];
/**
* The badges to show on the node.
* More styles should be configured in node mapper.
*/
badges?: {
type: 'icon' | 'text';
text: string;
position: BadgePosition;
}[];
}
export interface NodeLabelShapeStyle extends ShapeStyle {
position?: NodeLabelPosition;
offsetX?: number;
offsetY?: number;
background?: LabelBackground;
/** Data in inner model. Same format to the user data. */
export interface NodeModelData extends NodeUserModelData {}
export interface NodeShapeStyles extends ItemShapeStyles {
// keyShape, iconShape, haloShape are defined in ItemShapeStyles
labelShape?: ShapeStyle & {
position?: 'top' | 'bottom' | 'left' | 'right' | 'center';
offsetX?: number;
offsetY?: number;
};
labelBackgroundShape?: ShapeStyle & {
padding?: number | number[];
};
badgeShapes?: ShapeStyle & {
// common badge styles
color?: string;
textColor?: string;
// individual styles and their position
[key: number]: ShapeStyle & {
position?: BadgePosition;
color?: string;
textColor?: string;
};
};
anchorShapes?: ShapeStyle & {
// common badge styles
color?: string;
textColor?: string;
size?: number;
offsetX?: number;
offsetY?: number;
// individual styles and their position
[key: number]: ShapeStyle & {
position?: BadgePosition;
color?: string;
textColor?: string;
size?: number;
offsetX?: number;
offsetY?: number;
};
};
}
/** Data in display model. */
export interface NodeDisplayModelData extends NodeModelData {
keyShape?: ShapeStyle;
labelShape?: NodeLabelShapeStyle;
iconShape?: ShapeStyle;
otherShapes?: {
[shapeId: string]: ShapeStyle;
};
anchorPoints?: AnchorPoint[];
}
export type NodeDisplayModelData = NodeModelData & NodeShapeStyles;
/** User input model. */
export type NodeUserModel = GNode<NodeUserModelData>;
@ -50,24 +124,17 @@ export type NodeModel = GNode<NodeModelData>;
/** Displayed model, only for drawing and not received by users. */
export type NodeDisplayModel = GNode<NodeDisplayModelData>;
/** Anchor points, for linking edges and drawing circles. */
export interface AnchorPoint {
position?: [number, number]; // range from 0 to 1
show?: boolean;
[shapeAttr: string]: unknown;
animate: AnimateAttr;
}
interface NodeLabelShapeAttrEncode extends ShapeAttrEncode {
// TODO: extends Text shape attr, import from G
position?: NodeLabelPosition | Encode<NodeLabelPosition>;
offsetX?: number | Encode<number>;
offsetY?: number | Encode<number>;
background?: LabelBackground | Encode<LabelBackground>;
}
export interface NodeShapesEncode extends ShapesEncode {
labelShape?: NodeLabelShapeAttrEncode;
anchorPoints?: AnchorPoint[] | Encode<AnchorPoint[]>;
labelShape?: NodeLabelShapeAttrEncode | Encode<ShapeStyle>;
labelBackgroundShape?: ShapeAttrEncode[] | Encode<ShapeStyle[]>;
anchorShapes?: ShapeAttrEncode[] | Encode<ShapeStyle[]>;
badgeShapes?: ShapeAttrEncode[] | Encode<ShapeStyle[]>;
}
export interface NodeEncode extends NodeShapesEncode {
type?: string | Encode<string>;
@ -77,6 +144,8 @@ export interface NodeShapeMap {
keyShape: DisplayObject;
labelShape?: DisplayObject;
iconShape?: DisplayObject;
// TODO other badge shapes
[otherShapeId: string]: DisplayObject;
}

View File

@ -1,4 +1,6 @@
import { ItemShapeStyles, ShapeStyle } from './item';
import { ComboShapeStyles } from './combo';
import { EdgeShapeStyles } from './edge';
import { NodeShapeStyles } from './node';
export interface ThemeOption {}
/**
@ -41,33 +43,53 @@ export type ThemeObjectOptionsOf<T extends ThemeRegistry = {}> = {
: never;
}[Extract<keyof T, string>];
export type ItemStyleSets =
| ItemStyleSet[]
| { [dataTypeValue: string]: ItemStyleSet };
/** Default and stateStyle for an item */
export type ItemStyleSet = {
default?: ItemShapeStyles;
[stateName: string]: ItemShapeStyles;
export type NodeStyleSet = {
default?: NodeShapeStyles;
seledted?: NodeShapeStyles;
[stateName: string]: NodeShapeStyles;
};
export type EdgeStyleSet = {
default?: EdgeShapeStyles;
[stateName: string]: EdgeShapeStyles;
};
export type ComboStyleSet = {
default?: ComboShapeStyles;
[stateName: string]: ComboShapeStyles;
};
export interface ItemThemeSpecifications {
export type NodeStyleSets =
| NodeStyleSet[]
| { [dataTypeValue: string]: NodeStyleSet };
export type EdgeStyleSets =
| EdgeStyleSet[]
| { [dataTypeValue: string]: EdgeStyleSet };
export type ComboStyleSets =
| ComboStyleSet[]
| { [dataTypeValue: string]: ComboStyleSet };
export interface NodeThemeSpecifications {
dataTypeField?: string;
palette?: string[] | { [dataTypeValue: string]: string };
styles?:
| ItemStyleSet[]
| {
[dataTypeValue: string]: ItemStyleSet;
};
styles?: NodeStyleSets;
}
export interface EdgeThemeSpecifications {
dataTypeField?: string;
palette?: string[] | { [dataTypeValue: string]: string };
styles?: EdgeStyleSets;
}
export interface ComboThemeSpecifications {
dataTypeField?: string;
palette?: string[] | { [dataTypeValue: string]: string };
styles?: ComboStyleSets;
}
/**
* Theme specification
*/
export interface ThemeSpecification {
node?: ItemThemeSpecifications;
edge?: ItemThemeSpecifications;
combo?: ItemThemeSpecifications;
node?: NodeThemeSpecifications;
edge?: EdgeThemeSpecifications;
combo?: ComboThemeSpecifications;
canvas?: {
[cssName: string]: unknown;
};

View File

@ -0,0 +1,61 @@
import { NodeDisplayModelData } from 'types/node';
/**
* Default mapper to transform simple styles in inner data.
*/
export const DEFAULT_MAPPER = {
node: (innerNodeModel) => {
const { id, data } = innerNodeModel;
const { color, label, icon, badges, anchorPoints } = data;
const resultData: NodeDisplayModelData = { ...data, keyShape: {} };
if (color) {
resultData.keyShape.fill = color;
}
if (label) {
resultData.labelShape = {
text: label,
};
}
if (icon) {
resultData.iconShape = icon;
}
if (badges) {
resultData.badgeShapes = badges;
}
if (anchorPoints) {
resultData.anchorShapes = anchorPoints.map((point) => ({
position: point,
}));
}
return {
id,
data: resultData,
};
},
edge: (innerEdgeModel) => {
const { id, source, target, data } = innerEdgeModel;
const { color, label, icon, badge } = data;
const resultData: NodeDisplayModelData = { ...data, keyShape: {} };
if (color) {
resultData.keyShape.stroke = color;
}
if (label) {
resultData.labelShape = {
text: label,
};
}
if (icon) {
resultData.iconShape = icon;
}
if (badge) {
resultData.badgeShape = badge;
}
return {
id,
source,
target,
data: resultData,
};
},
combo: (innerComboModel) => innerComboModel,
};

View File

@ -0,0 +1,9 @@
/**
* Whether the value is begween the range of [min, max]
* @param {number} value the value to be judged
* @param {number} min the min of the range
* @param {number} max the max of the range
* @return {boolean} bool the result boolean
*/
export const isBetween = (value: number, min: number, max: number) =>
value >= min && value <= max;

View File

@ -0,0 +1,195 @@
import { Point } from '../types/common';
import { isBetween } from './math';
/**
* Find the nearest on in points to the curPoint.
* @param points
* @param curPoint
* @returns
*/
export const getNearestPoint = (
points: Point[],
curPoint: Point,
): {
index: number;
nearestPoint: Point;
} => {
let index = 0;
let nearestPoint = points[0];
let minDistance = distance(points[0], curPoint);
for (let i = 0; i < points.length; i++) {
const point = points[i];
const dis = distance(point, curPoint);
if (dis < minDistance) {
nearestPoint = point;
minDistance = dis;
index = i;
}
}
return {
index,
nearestPoint,
};
};
/**
* Get distance by two points.
* @param p1 first point
* @param p2 second point
*/
export const distance = (p1: Point, p2: Point): number => {
const vx = p1.x - p2.x;
const vy = p1.y - p2.y;
return Math.sqrt(vx * vx + vy * vy);
};
/**
* Get point and circle intersect point.
* @param {ICircle} circle Circle's center x,y and radius r
* @param {Point} point Point x,y
* @return {Point} calculated intersect point
*/
export const getCircleIntersectByPoint = (
circleProps: { x: number; y: number; r: number },
point: Point,
): Point | null => {
const { x: cx, y: cy, r } = circleProps;
const { x, y } = point;
const dx = x - cx;
const dy = y - cy;
if (dx * dx + dy * dy < r * r) {
return null;
}
const angle = Math.atan(dy / dx);
return {
x: cx + Math.abs(r * Math.cos(angle)) * Math.sign(dx),
y: cy + Math.abs(r * Math.sin(angle)) * Math.sign(dy),
};
};
/**
* Get point and ellipse inIntersect.
* @param {Object} ellipse ellipse center x,y and radius rx,ry
* @param {Object} point Point x,y
* @return {object} calculated intersect point
*/
export const getEllipseIntersectByPoint = (
ellipseProps: {
rx: number;
ry: number;
x: number;
y: number;
},
point: Point,
): Point => {
const { rx: a, ry: b, x: cx, y: cy } = ellipseProps;
const dx = point.x - cx;
const dy = point.y - cy;
// The angle will be in range [-PI, PI]
let angle = Math.atan2(dy / b, dx / a);
if (angle < 0) {
// transfer to [0, 2*PI]
angle += 2 * Math.PI;
}
return {
x: cx + a * Math.cos(angle),
y: cy + b * Math.sin(angle),
};
};
/**
* Point and rectangular intersection point.
* @param {IRect} rect rect
* @param {Point} point point
* @return {PointPoint} rst;
*/
export const getRectIntersectByPoint = (
rectProps: { x: number; y: number; width: number; height: number },
point: Point,
): Point | null => {
const { x, y, width, height } = rectProps;
const cx = x + width / 2;
const cy = y + height / 2;
const points: Point[] = [];
const center: Point = {
x: cx,
y: cy,
};
points.push({
x,
y,
});
points.push({
x: x + width,
y,
});
points.push({
x: x + width,
y: y + height,
});
points.push({
x,
y: y + height,
});
points.push({
x,
y,
});
let rst: Point | null = null;
for (let i = 1; i < points.length; i++) {
rst = getLineIntersect(points[i - 1], points[i], center, point);
if (rst) {
break;
}
}
return rst;
};
/**
* Get the intersect point of two lines.
* @param {Point} p0 The start point of the first line.
* @param {Point} p1 The end point of the first line.
* @param {Point} p2 The start point of the second line.
* @param {Point} p3 The end point of the second line.
* @return {Point} Calculated intersect point.
*/
export const getLineIntersect = (
p0: Point,
p1: Point,
p2: Point,
p3: Point,
): Point | null => {
const tolerance = 0.0001;
const E: Point = {
x: p2.x - p0.x,
y: p2.y - p0.y,
};
const D0: Point = {
x: p1.x - p0.x,
y: p1.y - p0.y,
};
const D1: Point = {
x: p3.x - p2.x,
y: p3.y - p2.y,
};
const kross: number = D0.x * D1.y - D0.y * D1.x;
const sqrKross: number = kross * kross;
const invertKross: number = 1 / kross;
const sqrLen0: number = D0.x * D0.x + D0.y * D0.y;
const sqrLen1: number = D1.x * D1.x + D1.y * D1.y;
if (sqrKross > tolerance * sqrLen0 * sqrLen1) {
const s = (E.x * D1.y - E.y * D1.x) * invertKross;
const t = (E.x * D0.y - E.y * D0.x) * invertKross;
if (!isBetween(s, 0, 1) || !isBetween(t, 0, 1)) return null;
return {
x: p0.x + s * D0.x,
y: p0.y + s * D0.y,
};
}
return null;
};

View File

@ -17,8 +17,15 @@ import { clone, isArray, isNumber } from '@antv/util';
import { DEFAULT_LABEL_BG_PADDING } from '../constant';
import { Point } from '../types/common';
import { EdgeShapeMap } from '../types/edge';
import { GShapeStyle, SHAPE_TYPE, ItemShapeStyles } from '../types/item';
import {
GShapeStyle,
SHAPE_TYPE,
ItemShapeStyles,
ShapeStyle,
} from '../types/item';
import { NodeShapeMap } from '../types/node';
import { isArrayOverlap } from './array';
import { isBetween } from './math';
export const ShapeTagMap = {
circle: Circle,
@ -46,20 +53,30 @@ export const upsertShape = (
id: string,
style: GShapeStyle,
shapeMap: { [shapeId: string]: DisplayObject },
): DisplayObject => {
): {
updateStyles: ShapeStyle;
shape: DisplayObject;
} => {
let shape = shapeMap[id];
let updateStyles = {};
if (!shape) {
shape = createShape(type, style, id);
updateStyles = style;
} else if (shape.nodeName !== type) {
shape.remove();
shape = createShape(type, style, id);
updateStyles = style;
} else {
const oldStyles = shape.attributes;
Object.keys(style).forEach((key) => {
shape.style[key] = style[key];
if (oldStyles[key] !== style[key]) {
updateStyles[key] = style[key];
shape.style[key] = style[key];
}
});
}
shapeMap[id] = shape;
return shape;
return { shape, updateStyles };
};
export const getGroupSucceedMap = (
@ -136,6 +153,8 @@ export const formatPadding = (value, defaultArr = DEFAULT_LABEL_BG_PADDING) => {
return [value[0], value[0], value[0], value[0]];
case 2:
return value.concat(value);
case 3:
return value.concat([value[0]]);
default:
return value;
}
@ -167,8 +186,8 @@ const merge2Styles = (
styleMap1: ItemShapeStyles,
styleMap2: ItemShapeStyles,
) => {
if (!styleMap1) return clone(styleMap2);
else if (!styleMap2) return clone(styleMap1);
if (!styleMap1) return { ...styleMap2 };
else if (!styleMap2) return { ...styleMap1 };
const mergedStyle = clone(styleMap1);
Object.keys(styleMap2).forEach((shapeId) => {
const style = styleMap2[shapeId];
@ -404,12 +423,23 @@ export const getLineIntersect = (
return null;
};
const FEILDS_AFFECT_BBOX = {
circle: ['r', 'lineWidth'],
rect: ['width', 'height', 'lineWidth'],
image: ['width', 'height', 'lineWidth'],
ellipse: ['rx', 'ry', 'lineWidth'],
text: ['fontSize', 'fontWeight'],
polygon: ['points', 'lineWidth'],
line: ['x1', 'x2', 'y1', 'y2', 'lineWidth'],
polyline: ['points', 'lineWidth'],
path: ['points', 'lineWidth'],
};
/**
* Whether the value is begween the range of [min, max]
* @param {number} value the value to be judged
* @param {number} min the min of the range
* @param {number} max the max of the range
* @return {boolean} bool the result boolean
* Will the fields in style affect the bbox.
* @param type shape type
* @param style style object
* @returns
*/
const isBetween = (value: number, min: number, max: number) =>
value >= min && value <= max;
export const isStyleAffectBBox = (type: SHAPE_TYPE, style: ShapeStyle) => {
return isArrayOverlap(Object.keys(style), FEILDS_AFFECT_BBOX[type]);
};

View File

@ -10,7 +10,12 @@ import {
} from '@antv/g-plugin-3d';
import { EdgeShapeMap } from '../types/edge';
import { NodeShapeMap } from '../types/node';
import { GShapeStyle, SHAPE_TYPE, SHAPE_TYPE_3D } from '../types/item';
import {
GShapeStyle,
SHAPE_TYPE,
SHAPE_TYPE_3D,
ShapeStyle,
} from '../types/item';
import { ShapeTagMap, createShape } from './shape';
const GeometryTagMap = {
@ -103,20 +108,30 @@ export const upsertShape3D = (
style: GShapeStyle,
shapeMap: { [shapeId: string]: DisplayObject },
device: any,
): DisplayObject => {
): {
updateStyles: ShapeStyle;
shape: DisplayObject;
} => {
let shape = shapeMap[id];
let updateStyles = {};
if (!shape) {
shape = createShape3D(type, style, id, device);
updateStyles = style;
} else if (shape.nodeName !== type) {
shape.remove();
shape = createShape3D(type, style, id, device);
updateStyles = style;
} else {
const oldStyles = shape.attributes;
Object.keys(style).forEach((key) => {
shape.style[key] = style[key];
if (oldStyles[key] !== style[key]) {
updateStyles[key] = style[key];
shape.style[key] = style[key];
}
});
}
shapeMap[id] = shape;
return shape;
return { shape, updateStyles };
};
/**

View File

@ -2,7 +2,6 @@ import * as graphs from './intergration/index';
const SelectGraph = document.getElementById('select') as HTMLSelectElement;
let firstKey;
console.log('firstKey', firstKey);
const Options = Object.keys(graphs).map((key, index) => {
const option = document.createElement('option');
if (index === 0) {
@ -18,10 +17,9 @@ SelectGraph.replaceChildren(...Options);
SelectGraph.onchange = (e) => {
//@ts-ignore
const { value } = e.target;
console.log(value);
history.pushState({ value }, '', `?name=${value}`);
const container = document.getElementById('container');
container.replaceChildren('');
container?.replaceChildren('');
graphs[value]();
};
// 初始化

View File

@ -1370,10 +1370,6 @@ describe('lasso-select behavior with brushStyle', () => {
canvas: { x: 200, y: 150 },
shiftKey: true,
});
console.log(
'graph.transientCanvas.getRoot().childNodes',
graph.transientCanvas.getRoot().childNodes,
);
expect(graph.transientCanvas.getRoot().childNodes.length).toBe(2);
// TODO: wait for correct removeBehaviors

View File

@ -0,0 +1,857 @@
// @ts-nocheck
import { DisplayObject } from '@antv/g';
import { clone } from '@antv/util';
import G6, {
EdgeDisplayModel,
Graph,
IGraph,
NodeDisplayModel,
} from '../../src/index';
import { LineEdge } from '../../src/stdlib/item/edge';
import { CircleNode } from '../../src/stdlib/item/node';
import { NodeModelData, NodeShapeMap } from '../../src/types/node';
import { extend } from '../../src/util/extend';
import { upsertShape } from '../../src/util/shape';
const container = document.createElement('div');
document.querySelector('body').appendChild(container);
let graph: IGraph<any>;
describe('edge item', () => {
it('new graph with two nodes and one edge', (done) => {
graph = new G6.Graph({
container,
width: 500,
height: 500,
type: 'graph',
modes: {
default: ['drag-node'],
},
data: {
nodes: [
{
id: 'node1',
data: {
x: 100,
y: 100,
},
},
{
id: 'node2',
data: { x: 300, y: 300 },
},
],
edges: [
{
id: 'edge1',
source: 'node1',
target: 'node2',
data: {},
},
],
},
});
graph.on('afterrender', () => {
const edgeItem = graph.itemController.itemMap['edge1'];
expect(edgeItem).not.toBe(undefined);
expect(edgeItem.shapeMap.labelShape).toBe(undefined);
done();
});
});
it('update edge label', (done) => {
const padding = [4, 16, 4, 8];
graph.updateData('edge', {
id: 'edge1',
data: {
labelShape: {
text: 'edge-label',
},
labelBackgroundShape: {
radius: 10,
padding,
fill: '#f00',
},
iconShape: {
text: 'A',
fill: '#f00',
},
},
});
const edgeItem = graph.itemController.itemMap['edge1'];
expect(edgeItem.shapeMap.labelShape).not.toBe(undefined);
expect(edgeItem.shapeMap.labelShape.attributes.text).toBe('edge-label');
const fill = edgeItem.shapeMap.labelShape.attributes.fill;
expect(fill).toBe('rgba(0,0,0,0.85)');
expect(edgeItem.shapeMap.labelShape.attributes.transform).toBe(
'rotate(45)',
);
expect(edgeItem.shapeMap.labelBackgroundShape.attributes.transform).toBe(
'rotate(45)',
);
let labelBounds = edgeItem.shapeMap.labelShape.getGeometryBounds();
expect(edgeItem.shapeMap.labelBackgroundShape.attributes.width).toBe(
labelBounds.max[0] - labelBounds.min[0] + padding[1] + padding[3],
);
expect(edgeItem.shapeMap.labelBackgroundShape.attributes.height).toBe(
labelBounds.max[1] - labelBounds.min[1] + padding[0] + padding[2],
);
graph.updateData('edge', {
id: 'edge1',
data: {
labelShape: {
fill: '#00f',
position: 'start',
},
},
});
expect(edgeItem.shapeMap.labelShape.attributes.fill).toBe('#00f');
expect(
edgeItem.shapeMap.labelShape.attributes.x -
edgeItem.shapeMap.labelBackgroundShape.attributes.x,
).toBe(padding[3]);
labelBounds = edgeItem.shapeMap.labelShape.getGeometryBounds();
const labelWidth = labelBounds.max[0] - labelBounds.min[0];
const labelHeight = labelBounds.max[1] - labelBounds.min[1];
const labelBgBounds =
edgeItem.shapeMap.labelBackgroundShape.getGeometryBounds();
const labelBgWidth = labelBgBounds.max[0] - labelBgBounds.min[0];
const labelBgHeight = labelBgBounds.max[1] - labelBgBounds.min[1];
expect(labelBgWidth - labelWidth).toBe(padding[1] + padding[3]);
expect(labelBgHeight - labelHeight).toBe(padding[0] + padding[2]);
graph.updateData('edge', {
id: 'edge1',
data: {
labelShape: undefined,
},
});
expect(edgeItem.shapeMap.labelShape).toBe(undefined);
expect(edgeItem.shapeMap.labelBackgroundShape).toBe(undefined);
done();
});
it('update edge icon', (done) => {
// add image icon to follow the label at path's center
graph.updateData('edge', {
id: 'edge1',
data: {
labelShape: {
text: 'abcddd',
fill: '#f00',
position: 'center',
},
iconShape: {
text: '',
img: 'https://gw.alipayobjects.com/zos/basement_prod/012bcf4f-423b-4922-8c24-32a89f8c41ce.svg',
// text: 'A',
fill: '#f00',
fontSize: 20,
},
},
});
const edgeItem = graph.itemController.itemMap['edge1'];
let { labelShape, iconShape, labelBackgroundShape } = edgeItem.shapeMap;
expect(iconShape.attributes.x + iconShape.attributes.width + 6).toBe(
labelBackgroundShape.getGeometryBounds().min[0] +
labelBackgroundShape.attributes.x,
);
expect(iconShape.attributes.transform).toBe(
labelBackgroundShape.attributes.transform,
);
expect(
Math.floor(iconShape.attributes.y + iconShape.attributes.height / 2),
).toBeCloseTo(
Math.floor(
labelBackgroundShape.getGeometryBounds().center[1] +
labelBackgroundShape.attributes.y,
),
0.01,
);
// update icon to be a text
graph.updateData('edge', {
id: 'edge1',
data: {
iconShape: {
text: 'A',
fill: '#f00',
fontWeight: 800,
},
},
});
labelShape = edgeItem.shapeMap['labelShape'];
iconShape = edgeItem.shapeMap['iconShape'];
labelBackgroundShape = edgeItem.shapeMap['labelBackgroundShape'];
expect(iconShape.attributes.x + iconShape.attributes.fontSize + 6).toBe(
labelBackgroundShape.getGeometryBounds().min[0] +
labelBackgroundShape.attributes.x,
);
expect(iconShape.attributes.transform).toBe(
labelBackgroundShape.attributes.transform,
);
expect(
Math.floor(iconShape.attributes.y + iconShape.attributes.fontSize / 2),
).toBeCloseTo(
Math.floor(
labelBackgroundShape.getGeometryBounds().center[1] +
labelBackgroundShape.attributes.y,
),
0.01,
);
// move label to the start, and the icon follows
graph.updateData('edge', {
id: 'edge1',
data: {
labelShape: {
position: 'start',
},
},
});
labelShape = edgeItem.shapeMap['labelShape'];
iconShape = edgeItem.shapeMap['iconShape'];
labelBackgroundShape = edgeItem.shapeMap['labelBackgroundShape'];
expect(iconShape.attributes.x + iconShape.attributes.fontSize + 6).toBe(
labelBackgroundShape.getGeometryBounds().min[0] +
labelBackgroundShape.attributes.x,
);
expect(iconShape.attributes.transform).toBe(
labelShape.attributes.transform,
);
expect(iconShape.attributes.y + iconShape.attributes.fontSize / 2).toBe(
labelBackgroundShape.getGeometryBounds().center[1] +
labelBackgroundShape.attributes.y,
);
graph.destroy();
done();
});
});
describe('edge mapper', () => {
const data = {
nodes: [
{
id: 'node1',
data: { x: 100, y: 200 },
},
{
id: 'node2',
data: { x: 100, y: 300 },
},
{
id: 'node3',
data: { x: 200, y: 300 },
},
],
edges: [
{
id: 'edge1',
source: 'node1',
target: 'node2',
data: { buStatus: true, buType: 1, buName: 'edge-1' },
},
{
id: 'edge2',
source: 'node1',
target: 'node3',
data: { buStatus: false, buType: 0, buName: 'edge-2' },
},
],
};
const graphConfig = {
container,
width: 500,
height: 500,
type: 'graph',
};
it('function mapper', (done) => {
const graph = new G6.Graph({
...graphConfig,
edge: (innerModel) => {
const { x, y, buStatus } = innerModel.data;
return {
...innerModel,
data: {
x,
y,
keyShape: {
stroke: buStatus ? '#0f0' : '#f00',
},
},
};
},
} as any);
graph.read(clone(data));
graph.on('afterrender', () => {
const edge1 = graph.itemController.itemMap['edge1'];
expect(edge1.shapeMap.keyShape.attributes.stroke).toBe('#0f0');
let edge2 = graph.itemController.itemMap['edge2'];
expect(edge2.shapeMap.keyShape.attributes.stroke).toBe('#f00');
// update user data
graph.updateData('edge', {
id: 'edge2',
data: {
buStatus: true,
},
});
edge2 = graph.itemController.itemMap['edge2'];
expect(edge2.shapeMap.keyShape.attributes.stroke).toBe('#0f0');
graph.destroy();
done();
});
});
it('value and encode mapper', (done) => {
const graph = new G6.Graph({
...graphConfig,
edge: {
keyShape: {
stroke: {
fields: ['buStatus'],
formatter: (innerModel) =>
innerModel.data.buStatus ? '#0f0' : '#f00',
},
lineWidth: 5,
lineDash: {
fields: ['buStatus', 'buType'],
formatter: (innerModel) =>
innerModel.data.buStatus || innerModel.data.buType
? undefined
: [5, 5],
},
},
labelShape: {
text: {
fields: ['buName', 'buType'],
formatter: (innerModel) =>
`${innerModel.data.buName}-${innerModel.data.buType}`,
},
},
},
} as any);
graph.read(clone(data));
graph.on('afterrender', () => {
const edge1 = graph.itemController.itemMap['edge1'];
expect(edge1.shapeMap.keyShape.attributes.stroke).toBe('#0f0');
expect(edge1.shapeMap.keyShape.attributes.lineWidth).toBe(5);
expect(edge1.shapeMap.keyShape.attributes.lineDash).toBe('');
expect(edge1.shapeMap.labelShape.attributes.text).toBe('edge-1-1');
let edge2 = graph.itemController.itemMap['edge2'];
expect(edge2.shapeMap.keyShape.attributes.stroke).toBe('#f00');
expect(edge2.shapeMap.keyShape.attributes.lineWidth).toBe(5);
expect(JSON.stringify(edge2.shapeMap.keyShape.attributes.lineDash)).toBe(
'[5,5]',
);
expect(edge2.shapeMap.labelShape.attributes.text).toBe('edge-2-0');
// update user data
graph.updateData('edge', {
id: 'edge2',
data: {
buStatus: true,
buName: 'newedge2name',
},
});
edge2 = graph.itemController.itemMap['edge2'];
expect(edge2.shapeMap.keyShape.attributes.stroke).toBe('#0f0');
expect(edge2.shapeMap.keyShape.attributes.lineDash).toBe('');
expect(edge2.shapeMap.labelShape.attributes.text).toBe('newedge2name-0');
graph.destroy();
done();
});
});
});
describe('state', () => {
it('edge state', (done) => {
const graph = new Graph({
container,
width: 500,
height: 500,
type: 'graph',
data: {
nodes: [
{
id: 'node1',
data: { x: 100, y: 200 },
},
{
id: 'node2',
data: { x: 100, y: 300 },
},
{
id: 'node3',
data: { x: 200, y: 300 },
},
],
edges: [
{
id: 'edge1',
source: 'node1',
target: 'node2',
data: {},
},
{
id: 'edge2',
source: 'node1',
target: 'node3',
data: {},
},
],
},
edgeState: {
selected: {
keyShape: {
stroke: '#0f0',
lineWidth: 2,
},
},
highlight: {
keyShape: {
stroke: '#00f',
opacity: 0.5,
},
},
},
});
graph.on('afterrender', () => {
expect(graph.findIdByState('edge', 'selected').length).toBe(0);
graph.setItemState('edge1', 'selected', true);
expect(graph.findIdByState('edge', 'selected').length).toBe(1);
expect(graph.findIdByState('edge', 'selected')[0]).toBe('edge1');
expect(
graph.itemController.itemMap['edge1'].shapeMap.keyShape.style.lineWidth,
).toBe(2);
expect(
graph.itemController.itemMap['edge1'].shapeMap.keyShape.style.stroke,
).toBe('#0f0');
graph.setItemState('edge1', 'selected', false);
expect(graph.findIdByState('edge', 'selected').length).toBe(0);
expect(
graph.itemController.itemMap['edge1'].shapeMap.keyShape.style.lineWidth,
).toBe(1);
// set multiple edges state
graph.setItemState(['edge1', 'edge2'], 'selected', true);
expect(graph.findIdByState('edge', 'selected').length).toBe(2);
expect(
graph.itemController.itemMap['edge1'].shapeMap.keyShape.style.lineWidth,
).toBe(2);
expect(
graph.itemController.itemMap['edge1'].shapeMap.keyShape.style.stroke,
).toBe('#0f0');
expect(
graph.itemController.itemMap['edge2'].shapeMap.keyShape.style.lineWidth,
).toBe(2);
expect(
graph.itemController.itemMap['edge2'].shapeMap.keyShape.style.stroke,
).toBe('#0f0');
graph.setItemState('edge1', 'selected', false);
expect(graph.findIdByState('edge', 'selected').length).toBe(1);
expect(graph.findIdByState('edge', 'selected')[0]).toBe('edge2');
expect(
graph.itemController.itemMap['edge1'].shapeMap.keyShape.style.lineWidth,
).toBe(1);
graph.setItemState(['edge1', 'edge2'], 'selected', false);
expect(graph.findIdByState('edge', 'selected').length).toBe(0);
expect(
graph.itemController.itemMap['edge2'].shapeMap.keyShape.style.lineWidth,
).toBe(1);
// // set multiple states
graph.setItemState(['edge2', 'edge1'], ['selected', 'highlight'], true);
expect(graph.findIdByState('edge', 'selected').length).toBe(2);
expect(graph.findIdByState('edge', 'highlight').length).toBe(2);
// should be merged styles from selected and highlight
expect(
graph.itemController.itemMap['edge1'].shapeMap.keyShape.style.lineWidth,
).toBe(2);
expect(
graph.itemController.itemMap['edge1'].shapeMap.keyShape.style.stroke,
).toBe('#00f');
expect(
graph.itemController.itemMap['edge1'].shapeMap.keyShape.style.opacity,
).toBe(0.5);
expect(
graph.itemController.itemMap['edge2'].shapeMap.keyShape.style.lineWidth,
).toBe(2);
expect(
graph.itemController.itemMap['edge2'].shapeMap.keyShape.style.stroke,
).toBe('#00f');
expect(
graph.itemController.itemMap['edge2'].shapeMap.keyShape.style.opacity,
).toBe(0.5);
// clear states
graph.clearItemState(['edge1', 'edge2']);
expect(graph.findIdByState('edge', 'selected').length).toBe(0);
expect(graph.findIdByState('edge', 'highlight').length).toBe(0);
expect(
graph.itemController.itemMap['edge1'].shapeMap.keyShape.style.lineWidth,
).toBe(1);
expect(
graph.itemController.itemMap['edge1'].shapeMap.keyShape.style.opacity,
).toBe(1);
graph.destroy();
done();
});
});
it('edge state with assigned style', (done) => {
const graph = new Graph({
container,
width: 500,
height: 500,
type: 'graph',
data: {
nodes: [
{
id: 'node1',
data: { x: 100, y: 200 },
},
{
id: 'node2',
data: { x: 100, y: 300 },
},
{
id: 'node3',
data: { x: 200, y: 300 },
},
],
edges: [
{
id: 'edge1',
source: 'node1',
target: 'node2',
data: {
keyShape: {
stroke: '#f00',
lineDash: [2, 2],
},
},
},
{
id: 'edge2',
source: 'node1',
target: 'node3',
data: {
keyShape: {
stroke: '#f00',
lineDash: [2, 2],
},
},
},
],
},
edgeState: {
selected: {
keyShape: {
stroke: '#0f0',
lineWidth: 2,
},
},
highlight: {
keyShape: {
stroke: '#00f',
opacity: 0.5,
},
},
},
});
graph.on('afterrender', () => {
expect(graph.findIdByState('edge', 'selected').length).toBe(0);
graph.setItemState('edge1', 'selected', true);
expect(graph.findIdByState('edge', 'selected').length).toBe(1);
expect(graph.findIdByState('edge', 'selected')[0]).toBe('edge1');
expect(
graph.itemController.itemMap['edge1'].shapeMap.keyShape.style.lineWidth,
).toBe(2);
expect(
graph.itemController.itemMap['edge1'].shapeMap.keyShape.style.stroke,
).toBe('#0f0');
expect(
JSON.stringify(
graph.itemController.itemMap['edge1'].shapeMap.keyShape.style
.lineDash,
),
).toBe('[2,2]');
graph.setItemState('edge1', 'selected', false);
expect(graph.findIdByState('edge', 'selected').length).toBe(0);
expect(
graph.itemController.itemMap['edge1'].shapeMap.keyShape.style.lineWidth,
).toBe(1);
expect(
graph.itemController.itemMap['edge1'].shapeMap.keyShape.style.stroke,
).toBe('#f00');
expect(
JSON.stringify(
graph.itemController.itemMap['edge1'].shapeMap.keyShape.style
.lineDash,
),
).toBe('[2,2]');
// set multiple edges state
graph.setItemState(['edge1', 'edge2'], 'selected', true);
expect(graph.findIdByState('edge', 'selected').length).toBe(2);
expect(
graph.itemController.itemMap['edge1'].shapeMap.keyShape.style.lineWidth,
).toBe(2);
expect(
graph.itemController.itemMap['edge1'].shapeMap.keyShape.style.stroke,
).toBe('#0f0');
expect(
JSON.stringify(
graph.itemController.itemMap['edge1'].shapeMap.keyShape.style
.lineDash,
),
).toBe('[2,2]');
expect(
graph.itemController.itemMap['edge2'].shapeMap.keyShape.style.lineWidth,
).toBe(2);
expect(
graph.itemController.itemMap['edge2'].shapeMap.keyShape.style.stroke,
).toBe('#0f0');
expect(
JSON.stringify(
graph.itemController.itemMap['edge1'].shapeMap.keyShape.style
.lineDash,
),
).toBe('[2,2]');
graph.setItemState('edge1', 'selected', false);
expect(graph.findIdByState('edge', 'selected').length).toBe(1);
expect(graph.findIdByState('edge', 'selected')[0]).toBe('edge2');
expect(
graph.itemController.itemMap['edge1'].shapeMap.keyShape.style.lineWidth,
).toBe(1);
expect(
graph.itemController.itemMap['edge1'].shapeMap.keyShape.style.stroke,
).toBe('#f00');
graph.setItemState(['edge1', 'edge2'], 'selected', false);
expect(graph.findIdByState('edge', 'selected').length).toBe(0);
expect(
graph.itemController.itemMap['edge2'].shapeMap.keyShape.style.lineWidth,
).toBe(1);
expect(
graph.itemController.itemMap['edge1'].shapeMap.keyShape.style.stroke,
).toBe('#f00');
// set multiple states
graph.setItemState(['edge2', 'edge1'], ['selected', 'highlight'], true);
expect(graph.findIdByState('edge', 'selected').length).toBe(2);
expect(graph.findIdByState('edge', 'highlight').length).toBe(2);
// should be merged styles from selected and highlight
expect(
graph.itemController.itemMap['edge1'].shapeMap.keyShape.style.lineWidth,
).toBe(2);
expect(
graph.itemController.itemMap['edge1'].shapeMap.keyShape.style.stroke,
).toBe('#00f');
expect(
graph.itemController.itemMap['edge1'].shapeMap.keyShape.style.opacity,
).toBe(0.5);
expect(
graph.itemController.itemMap['edge2'].shapeMap.keyShape.style.lineWidth,
).toBe(2);
expect(
graph.itemController.itemMap['edge2'].shapeMap.keyShape.style.stroke,
).toBe('#00f');
expect(
graph.itemController.itemMap['edge2'].shapeMap.keyShape.style.opacity,
).toBe(0.5);
// clear states
graph.clearItemState(['edge1', 'edge2']);
expect(graph.findIdByState('edge', 'selected').length).toBe(0);
expect(graph.findIdByState('edge', 'highlight').length).toBe(0);
expect(
graph.itemController.itemMap['edge1'].shapeMap.keyShape.style.lineWidth,
).toBe(1);
expect(
graph.itemController.itemMap['edge1'].shapeMap.keyShape.style.opacity,
).toBe(1);
graph.destroy();
done();
});
});
class CustomNode extends CircleNode {
public defaultStyles = {
keyShape: {
r: 25,
x: 0,
y: 0,
fill: '#ff0',
lineWidth: 0,
stroke: '#0f0',
},
};
public drawLabelShape(
model: NodeDisplayModel,
shapeMap: NodeShapeMap,
diffData?: { oldData: NodeModelData; newData: NodeModelData },
) {
const { labelShape: propsLabelStyle } = model.data;
const labelStyle = Object.assign(
{},
this.defaultStyles.labelShape,
propsLabelStyle,
);
const labelShape = this.upsertShape(
'text',
'labelShape',
{
...labelStyle,
text: model.id,
},
shapeMap,
).shape;
return labelShape;
}
public drawOtherShapes(
model: NodeDisplayModel,
shapeMap: NodeShapeMap,
diffData?: { oldData: NodeModelData; newData: NodeModelData },
) {
return {
extraShape: upsertShape(
'circle',
'extraShape',
{
r: 4,
fill: '#0f0',
x: -20,
y: 0,
},
shapeMap,
).shape,
};
}
}
class CustomEdge extends LineEdge {
public afterDraw(
model: EdgeDisplayModel,
shapeMap: { [shapeId: string]: DisplayObject<any, any> },
shapesChanged?: string[],
): { [otherShapeId: string]: DisplayObject } {
const { keyShape } = shapeMap;
const point = keyShape.getPoint(0.3);
return {
buShape: this.upsertShape(
'rect',
'buShape',
{
width: 6,
height: 6,
x: point.x,
y: point.y,
fill: '#0f0',
...model.data?.otherShapes?.buShape, // merged style from mappers and states
},
shapeMap,
).shape,
};
}
}
const CustomGraph = extend(Graph, {
nodes: {
'custom-node2': CustomNode,
},
edges: {
'custom-edge2': CustomEdge,
},
});
it('custom edge with setState', (done) => {
const graph = new CustomGraph({
container,
width: 500,
height: 500,
type: 'graph',
data: {
nodes: [
{
id: 'node1',
data: { x: 100, y: 200, type: 'custom-node2' },
},
{
id: 'node2',
data: { x: 100, y: 300, type: 'circle-node' },
},
{
id: 'node3',
data: { x: 200, y: 300, labelShape: undefined },
},
],
edges: [
{
id: 'edge1',
source: 'node1',
target: 'node2',
data: { type: 'custom-edge2' },
},
{
id: 'edge2',
source: 'node1',
target: 'node3',
data: {},
},
],
},
node: {
// affect the nodes without type field in their data object, which means configurations in the user data has higher priority than that in the mapper
type: 'custom-node2',
// affect the nodes without labelShape field in their data object, which means configurations in the user data has higher priority than that in the mapper
labelShape: {},
},
edgeState: {
selected: {
keyShape: {
lineWidth: 2,
stroke: '#00f',
},
otherShapes: {
buShape: {
fill: '#fff',
},
},
},
},
});
graph.on('afterrender', () => {
const edge1 = graph.itemController.itemMap['edge1'];
graph.setItemState('edge1', 'selected', true);
expect(edge1.shapeMap.keyShape.style.stroke).toBe('#00f');
expect(edge1.shapeMap.keyShape.style.lineWidth).toBe(2);
expect(edge1.shapeMap.buShape.style.fill).toBe('#fff');
const edge2 = graph.itemController.itemMap['edge2'];
graph.setItemState('edge2', 'selected', true);
expect(edge2.shapeMap.keyShape.style.stroke).toBe('#00f');
expect(edge2.shapeMap.keyShape.style.lineWidth).toBe(2);
// update node type
graph.updateData('edge', {
id: 'edge2',
data: {
type: 'custom-edge2',
},
});
expect(edge2.shapeMap.buShape).not.toBe(undefined);
expect(edge2.shapeMap.keyShape.style.stroke).toBe('#00f');
expect(edge2.shapeMap.keyShape.style.lineWidth).toBe(2);
expect(edge2.shapeMap.buShape.style.fill).toBe('#fff');
graph.clearItemState(['edge2'], ['selected']);
expect(edge2.shapeMap.keyShape.style.stroke).not.toBe('#00f');
expect(edge2.shapeMap.keyShape.style.lineWidth).toBe(1);
expect(edge2.shapeMap.buShape.style.fill).toBe('#0f0');
graph.destroy();
done();
});
});
});

View File

@ -76,7 +76,6 @@ describe('node item', () => {
graph.on('afterrender', (e) => {
graph.on('node:click', (e) => {
console.log('nodeclick');
graph.setItemState(e.itemId, 'selected', true);
});
});

View File

@ -13,15 +13,71 @@ document.querySelector('body').appendChild(container);
const data: GraphData = {
nodes: [
{ id: 'node1', data: { x: 100, y: 200, dt: 'a' } },
{ id: 'node2', data: { x: 200, y: 250, dt: 'b' } },
{ id: 'node3', data: { x: 300, y: 200, dt: 'c' } },
{ id: 'node4', data: { x: 300, y: 250 } },
{
id: 'node1',
data: {
x: 100,
y: 200,
dt: 'a',
labelBackgroundShape: {
radius: 8,
},
},
},
{
id: 'node2',
data: {
x: 200,
y: 250,
dt: 'b',
badges: [
{
type: 'icon',
text: 'a',
position: 'rightTop',
},
],
},
},
{
id: 'node3',
data: {
x: 300,
y: 200,
dt: 'c',
labelBackgroundShape: {
radius: 8,
},
},
},
{
id: 'node4',
data: {
x: 300,
y: 250,
anchorPoints: [
[0.5, 0],
[0.5, 1],
[0, 0.5],
[1, 0.5],
],
},
},
],
edges: [
{ id: 'edge1', source: 'node1', target: 'node2', data: { edt: '1' } },
{ id: 'edge2', source: 'node1', target: 'node3', data: { edt: '2' } },
{ id: 'edge3', source: 'node1', target: 'node4', data: {} },
{
id: 'edge3',
source: 'node1',
target: 'node4',
data: {
edt: 'xxx',
labelBackgroundShape: {
radius: 8,
},
},
},
],
};
@ -44,6 +100,21 @@ describe('theme', () => {
formatter: (model) => model.id,
},
},
iconShape: {
img: 'https://gw.alipayobjects.com/zos/basement_prod/012bcf4f-423b-4922-8c24-32a89f8c41ce.svg',
},
anchorShapes: {
fields: ['anchorPoints'],
formatter: (model) => {
return model.data.anchorPoints?.map((point, i) => ({
position: point,
}));
},
},
badgeShapes: {
fields: ['badges'],
formatter: (model) => model.data.badges,
},
},
edge: {
labelShape: {
@ -58,6 +129,7 @@ describe('theme', () => {
const node = graph.itemController.itemMap['node1'];
const { keyShape: nodeKeyShape, labelShape: nodeLabelShape } =
node.shapeMap;
expect(node.shapeMap.haloShape).toBe(undefined);
expect(nodeKeyShape.style.fill).toBe(
LightTheme.node.styles[0].default.keyShape.fill,
);
@ -67,6 +139,7 @@ describe('theme', () => {
const edge = graph.itemController.itemMap['edge1'];
const { keyShape: edgeKeyShape, labelShape: edgeLabelShape } =
edge.shapeMap;
expect(edge.shapeMap.haloShape).toBe(undefined);
expect(edgeKeyShape.style.stroke).toBe(
LightTheme.edge.styles[0].default.keyShape.stroke,
);
@ -76,8 +149,12 @@ describe('theme', () => {
// set state, should response with default theme state style
graph.setItemState('node1', 'selected', true);
expect(node.shapeMap.haloShape).not.toBe(undefined);
expect(node.shapeMap.haloShape.style.lineWidth).not.toBe(
LightTheme.node.styles[0].selected.haloShape.lineWith,
);
expect(nodeKeyShape.style.fill).toBe(
LightTheme.node.styles[0].selected.keyShape.fill,
LightTheme.node.styles[0].default.keyShape.fill, // no change
);
expect(nodeLabelShape.style.fontWeight).toBe(
LightTheme.node.styles[0].selected.labelShape.fontWeight,
@ -91,20 +168,31 @@ describe('theme', () => {
);
graph.setItemState('edge1', 'selected', true);
expect(edge.shapeMap.haloShape).not.toBe(undefined);
expect(edge.shapeMap.haloShape.style.lineWidth).not.toBe(
LightTheme.edge.styles[0].selected.haloShape.lineWith,
);
expect(edgeKeyShape.style.stroke).toBe(
LightTheme.edge.styles[0].selected.keyShape.stroke,
LightTheme.edge.styles[0].default.keyShape.stroke, // no change
);
expect(edgeKeyShape.style.lineWidth).toBe(
LightTheme.edge.styles[0].selected.keyShape.lineWidth,
);
console.log(
'xssx',
edgeLabelShape.style.fontWeight,
LightTheme.edge.styles[0].default.labelShape.fontWeight,
);
expect(edgeLabelShape.style.fill).toBe(
LightTheme.edge.styles[0].default.labelShape.fill,
); // no change in theme def
);
const node4 = graph.itemController.itemMap['node4'];
expect(node4.shapeMap.anchorShape0).not.toBe(undefined);
expect(node4.shapeMap.anchorShape1).not.toBe(undefined);
expect(node4.shapeMap.anchorShape2).not.toBe(undefined);
expect(node4.shapeMap.anchorShape3).not.toBe(undefined);
graph.setItemState('node4', 'selected', true);
expect(node4.shapeMap.anchorShape0).not.toBe(undefined);
expect(node4.shapeMap.anchorShape1).not.toBe(undefined);
expect(node4.shapeMap.anchorShape2).not.toBe(undefined);
expect(node4.shapeMap.anchorShape3).not.toBe(undefined);
graph.destroy();
done();
@ -271,17 +359,11 @@ describe('theme', () => {
// node setState with state in builtin theme
graph.setItemState('node1', 'selected', true);
expect(nodeKeyShape1.style.fill).toBe(
LightTheme.node.styles[0].selected.keyShape.fill,
);
expect(nodeLabelShape1.style.fontWeight).toBe(
LightTheme.node.styles[0].selected.labelShape.fontWeight,
);
// edge setState with state in builtin theme
graph.setItemState('edge1', 'selected', true);
expect(edgeKeyShape1.style.stroke).toBe(
LightTheme.edge.styles[0].selected.keyShape.stroke,
);
expect(edgeLabelShape1.style.fill).toBe('#0f0'); // not assigned in selected theme
expect(edgeLabelShape1.style.fontWeight).toBe(
LightTheme.edge.styles[0].selected.labelShape.fontWeight,
@ -323,9 +405,6 @@ describe('theme', () => {
// clear node's one state
graph.clearItemState('node1', ['state2']);
expect(nodeKeyShape1.style.fill).toBe('#ff0');
expect(nodeKeyShape1.style.stroke).toBe(
LightTheme.node.styles[0].default.keyShape.stroke,
);
// clear edge's one state, state1 + state3 is kept
graph.clearItemState('edge1', ['state2']);
expect(edgeKeyShape1.style.stroke).toBe('#ff0');
@ -483,17 +562,11 @@ describe('theme', () => {
// node setState with state in builtin theme
graph.setItemState('node1', 'selected', true);
expect(nodeKeyShape1.style.fill).toBe(
LightTheme.node.styles[0].selected.keyShape.fill,
);
expect(nodeLabelShape1.style.fontWeight).toBe(
LightTheme.node.styles[0].selected.labelShape.fontWeight,
);
// edge setState with state in builtin theme
graph.setItemState('edge1', 'selected', true);
expect(edgeKeyShape1.style.stroke).toBe(
LightTheme.edge.styles[0].selected.keyShape.stroke,
);
expect(edgeLabelShape1.style.fill).toBe('#0f0'); // not assigned in selected theme
expect(edgeLabelShape1.style.fontWeight).toBe(
LightTheme.edge.styles[0].selected.labelShape.fontWeight,
@ -535,9 +608,6 @@ describe('theme', () => {
// clear node's one state
graph.clearItemState('node1', ['state2']);
expect(nodeKeyShape1.style.fill).toBe('#ff0');
expect(nodeKeyShape1.style.stroke).toBe(
LightTheme.node.styles[0].default.keyShape.stroke,
);
// clear edge's one state, state1 + state3 is kept
graph.clearItemState('edge1', ['state2']);
@ -572,19 +642,8 @@ describe('theme', () => {
shapeMap: NodeShapeMap,
diffData?: { oldData: NodeModelData; newData: NodeModelData },
) {
const extraShape = upsertShape(
'circle',
'extraShape',
{
r: 4,
fill: '#0f0',
x: -20,
y: 0,
},
shapeMap,
);
const { labelShape: labelStyle } = this.mergedStyles;
const labelShape = upsertShape(
return this.upsertShape(
'text',
'labelShape',
{
@ -592,8 +651,26 @@ describe('theme', () => {
text: model.id,
},
shapeMap,
);
return { labelShape, extraShape };
).shape;
}
public drawOtherShapes(
model: NodeDisplayModel,
shapeMap: NodeShapeMap,
diffData?: { oldData: NodeModelData; newData: NodeModelData },
) {
return {
extraShape: this.upsertShape(
'circle',
'extraShape',
{
r: 4,
fill: '#0f0',
x: -20,
y: 0,
},
shapeMap,
).shape,
};
}
}
class CustomEdge extends LineEdge {
@ -605,7 +682,7 @@ describe('theme', () => {
const { keyShape } = shapeMap;
const point = keyShape.getPoint(0.3);
return {
buShape: upsertShape(
buShape: this.upsertShape(
'rect',
'buShape',
{
@ -616,7 +693,7 @@ describe('theme', () => {
fill: '#0f0',
},
shapeMap,
),
).shape,
};
}
}
@ -645,6 +722,7 @@ describe('theme', () => {
formatter: (model) => model.id,
},
},
otherShapes: {},
},
edge: {
type: 'theme-spec-custom-edge',