feat(sequence): actor creation and destruction #1838

This commit is contained in:
Laura Valentine Tscharner 2023-06-08 16:25:06 +01:00
parent 9a080bb975
commit d06bb05c5f
6 changed files with 388 additions and 70 deletions

View File

@ -156,6 +156,81 @@ context('Sequence diagram', () => {
`
);
});
it('should render a sequence diagram with basic actor creation and destruction', () => {
imgSnapshotTest(
`
sequenceDiagram
Alice ->> Bob: Hello Bob, how are you ?
Bob ->> Alice: Fine, thank you. And you?
create participant Polo
Alice ->> Polo: Hi Polo!
create actor Ola1 as Ola
Polo ->> Ola1: Hiii
Ola1 ->> Alice: Hi too
destroy Ola1
Alice --x Ola1: Bye!
Alice ->> Bob: And now?
create participant Ola2 as Ola
Alice ->> Ola2: Hello again
destroy Alice
Alice --x Ola2: Bye for me!
destroy Bob
Ola2 --> Bob: The end
`
);
});
it('should render a sequence diagram with actor creation and destruction coupled with backgrounds, loops and notes', () => {
imgSnapshotTest(
`
sequenceDiagram
accTitle: test the accTitle
accDescr: Test a description
participant Alice
participant Bob
autonumber 10 10
rect rgb(200, 220, 100)
rect rgb(200, 255, 200)
Alice ->> Bob: Hello Bob, how are you?
create participant John as John<br />Second Line
Bob-->>John: How about you John?
end
Bob--x Alice: I am good thanks!
Bob-x John: I am good thanks!
Note right of John: John thinks a long<br />long time, so long<br />that the text does<br />not fit on a row.
Bob-->Alice: Checking with John...
Note over John:wrap: John looks like he's still thinking, so Bob prods him a bit.
Bob-x John: Hey John - we're still waiting to know<br />how you're doing
Note over John:nowrap: John's trying hard not to break his train of thought.
destroy John
Bob-x John: John! Cmon!
Note over John: After a few more moments, John<br />finally snaps out of it.
end
autonumber off
alt either this
create actor Lola
Alice->>+Lola: Yes
Lola-->>-Alice: OK
else or this
autonumber
Alice->>Lola: No
else or this will happen
Alice->Lola: Maybe
end
autonumber 200
par this happens in parallel
destroy Bob
Alice -->> Bob: Parallel message 1
and
Alice -->> Lola: Parallel message 2
end
`
);
});
context('font settings', () => {
it('should render different note fonts when configured', () => {
imgSnapshotTest(

View File

@ -38,6 +38,8 @@
"box" { this.begin('LINE'); return 'box'; }
"participant" { this.begin('ID'); return 'participant'; }
"actor" { this.begin('ID'); return 'participant_actor'; }
"create" return 'create';
"destroy" { this.begin('ID'); return 'destroy'; }
<ID>[^\->:\n,;]+?([\-]*[^\->:\n,;]+?)*?(?=((?!\n)\s)+"as"(?!\n)\s|[#\n;]|$) { yytext = yytext.trim(); this.begin('ALIAS'); return 'ACTOR'; }
<ALIAS>"as" { this.popState(); this.popState(); this.begin('LINE'); return 'AS'; }
<ALIAS>(?:) { this.popState(); this.popState(); return 'NEWLINE'; }
@ -138,6 +140,7 @@ directive
statement
: participant_statement
| 'create' participant_statement {$2.type='createParticipant'; $$=$2;}
| 'box' restOfLine box_section end
{
$3.unshift({type: 'boxStart', boxData:yy.parseBoxData($2) });
@ -234,10 +237,11 @@ else_sections
;
participant_statement
: 'participant' actor 'AS' restOfLine 'NEWLINE' {$2.type='addParticipant';$2.description=yy.parseMessage($4); $$=$2;}
| 'participant' actor 'NEWLINE' {$2.type='addParticipant';$$=$2;}
| 'participant_actor' actor 'AS' restOfLine 'NEWLINE' {$2.type='addActor';$2.description=yy.parseMessage($4); $$=$2;}
| 'participant_actor' actor 'NEWLINE' {$2.type='addActor'; $$=$2;}
: 'participant' actor 'AS' restOfLine 'NEWLINE' {$2.draw='participant'; $2.type='addParticipant';$2.description=yy.parseMessage($4); $$=$2;}
| 'participant' actor 'NEWLINE' {$2.draw='participant'; $2.type='addParticipant';$$=$2;}
| 'participant_actor' actor 'AS' restOfLine 'NEWLINE' {$2.draw='actor'; $2.type='addParticipant';$2.description=yy.parseMessage($4); $$=$2;}
| 'participant_actor' actor 'NEWLINE' {$2.draw='actor'; $2.type='addParticipant'; $$=$2;}
| 'destroy' actor 'NEWLINE' {$2.type='destroyParticipant'; $$=$2;}
;
note_statement

View File

@ -14,12 +14,16 @@ import {
let prevActor = undefined;
let actors = {};
let createdActors = {};
let destroyedActors = {};
let boxes = [];
let messages = [];
const notes = [];
let sequenceNumbersEnabled = false;
let wrapEnabled;
let currentBox = undefined;
let lastCreated = undefined;
let lastDestroyed = undefined;
export const parseDirective = function (statement, context, type) {
mermaidAPI.parseDirective(this, statement, context, type);
@ -165,6 +169,12 @@ export const getBoxes = function () {
export const getActors = function () {
return actors;
};
export const getCreatedActors = function () {
return createdActors;
};
export const getDestroyedActors = function () {
return destroyedActors;
};
export const getActor = function (id) {
return actors[id];
};
@ -194,6 +204,8 @@ export const autoWrap = () => {
export const clear = function () {
actors = {};
createdActors = {};
destroyedActors = {};
boxes = [];
messages = [];
sequenceNumbersEnabled = false;
@ -459,10 +471,21 @@ export const apply = function (param) {
});
break;
case 'addParticipant':
addActor(param.actor, param.actor, param.description, 'participant');
addActor(param.actor, param.actor, param.description, param.draw);
break;
case 'addActor':
addActor(param.actor, param.actor, param.description, 'actor');
case 'createParticipant':
if (actors[param.actor]) {
throw new Error(
"It is not possible to have actors with the same id, even if one is destroyed before the next is created. Use 'AS' aliases to simulate the behavior"
);
}
lastCreated = param.actor;
addActor(param.actor, param.actor, param.description, param.draw);
createdActors[param.actor] = messages.length;
break;
case 'destroyParticipant':
lastDestroyed = param.actor;
destroyedActors[param.actor] = messages.length;
break;
case 'activeStart':
addSignal(param.actor, undefined, undefined, param.signalType);
@ -486,6 +509,27 @@ export const apply = function (param) {
addDetails(param.actor, param.text);
break;
case 'addMessage':
if (lastCreated) {
if (param.to !== lastCreated) {
throw new Error(
'The created participant ' +
lastCreated +
' does not have an associated creating message after its declaration. Please check the sequence diagram.'
);
} else {
lastCreated = undefined;
}
} else if (lastDestroyed) {
if (param.to !== lastDestroyed && param.from !== lastDestroyed) {
throw new Error(
'The destroyed participant ' +
lastDestroyed +
' does not have an associated destroying message after its declaration. Please check the sequence diagram.'
);
} else {
lastDestroyed = undefined;
}
}
addSignal(param.from, param.to, param.msg, param.signalType);
break;
case 'boxStart':
@ -566,6 +610,8 @@ export default {
showSequenceNumbers,
getMessages,
getActors,
getCreatedActors,
getDestroyedActors,
getActor,
getActorKeys,
getActorProperty,

View File

@ -1404,6 +1404,62 @@ link a: Tests @ https://tests.contoso.com/?svc=alice@contoso.com
expect(boxes[0].actorKeys).toEqual(['a', 'b']);
expect(boxes[0].fill).toEqual('Aqua');
});
it('should handle simple actor creation', async () => {
const str = `
sequenceDiagram
participant a as Alice
a ->>b: Hello Bob?
create participant c
b-->>c: Hello c!
c ->> b: Hello b?
create actor d as Donald
a ->> d: Hello Donald?
`;
await mermaidAPI.parse(str);
const actors = diagram.db.getActors();
const createdActors = diagram.db.getCreatedActors();
expect(actors['c'].name).toEqual('c');
expect(actors['c'].description).toEqual('c');
expect(actors['c'].type).toEqual('participant');
expect(createdActors['c']).toEqual(1);
expect(actors['d'].name).toEqual('d');
expect(actors['d'].description).toEqual('Donald');
expect(actors['d'].type).toEqual('actor');
expect(createdActors['d']).toEqual(3);
});
it('should handle simple actor destruction', async () => {
const str = `
sequenceDiagram
participant a as Alice
a ->>b: Hello Bob?
destroy a
b-->>a: Hello Alice!
b ->> c: Where is Alice?
destroy c
b ->> c: Where are you?
`;
await mermaidAPI.parse(str);
const destroyedActors = diagram.db.getDestroyedActors();
expect(destroyedActors['a']).toEqual(1);
expect(destroyedActors['c']).toEqual(3);
});
it('should handle the creation and destruction of the same actor', async () => {
const str = `
sequenceDiagram
a ->>b: Hello Bob?
create participant c
b ->>c: Hello c!
c ->> b: Hello b?
destroy c
b ->> c : Bye c !
`;
await mermaidAPI.parse(str);
const createdActors = diagram.db.getCreatedActors();
const destroyedActors = diagram.db.getDestroyedActors();
expect(createdActors['c']).toEqual(1);
expect(destroyedActors['c']).toEqual(3);
});
});
describe('when checking the bounds in a sequenceDiagram', function () {
beforeAll(() => {
@ -1973,7 +2029,9 @@ participant Alice`;
expect(bounds.startx).toBe(0);
expect(bounds.starty).toBe(0);
expect(bounds.stopx).toBe(conf.width);
expect(bounds.stopy).toBe(models.lastActor().y + models.lastActor().height + conf.boxMargin);
expect(bounds.stopy).toBe(
models.lastActor().stopy + models.lastActor().height + conf.boxMargin
);
});
});
});
@ -2025,7 +2083,7 @@ participant Alice
expect(bounds.startx).toBe(0);
expect(bounds.starty).toBe(0);
expect(bounds.stopy).toBe(
models.lastActor().y + models.lastActor().height + mermaid.sequence.boxMargin
models.lastActor().stopy + models.lastActor().height + mermaid.sequence.boxMargin
);
});
it('should handle one actor, when logLevel is 3 (dfg0)', async () => {
@ -2045,7 +2103,7 @@ participant Alice
expect(bounds.startx).toBe(0);
expect(bounds.starty).toBe(0);
expect(bounds.stopy).toBe(
models.lastActor().y + models.lastActor().height + mermaid.sequence.boxMargin
models.lastActor().stopy + models.lastActor().height + mermaid.sequence.boxMargin
);
});
it('should hide sequence numbers when autonumber is removed when autonumber is enabled', async () => {

View File

@ -1,6 +1,6 @@
// @ts-nocheck TODO: fix file
import { select, selectAll } from 'd3';
import svgDraw, { drawText, fixLifeLineHeights } from './svgDraw.js';
import svgDraw, { ACTOR_TYPE_WIDTH, drawText, fixLifeLineHeights } from './svgDraw.js';
import { log } from '../../logger.js';
import common from '../common/common.js';
import * as svgDrawCommon from '../common/svgDrawCommon';
@ -478,29 +478,19 @@ const drawMessage = function (diagram, msgModel, lineStartY: number, diagObj: Di
}
};
export const drawActors = function (
const addActorRenderingData = function (
diagram,
actors,
createdActors,
actorKeys,
verticalPos,
configuration,
messages,
isFooter
) {
if (configuration.hideUnusedParticipants === true) {
const newActors = new Set();
messages.forEach((message) => {
newActors.add(message.from);
newActors.add(message.to);
});
actorKeys = actorKeys.filter((actorKey) => newActors.has(actorKey));
}
// Draw the actors
let prevWidth = 0;
let prevMargin = 0;
let maxHeight = 0;
let prevBox = undefined;
let maxHeight = 0;
for (const actorKey of actorKeys) {
const actor = actors[actorKey];
@ -528,12 +518,16 @@ export const drawActors = function (
actor.height = common.getMax(actor.height || conf.height, conf.height);
actor.margin = actor.margin || conf.actorMargin;
actor.x = prevWidth + prevMargin;
actor.y = bounds.getVerticalPos();
maxHeight = common.getMax(maxHeight, actor.height);
// if the actor is created by a message, widen margin
if (createdActors[actor.name]) {
prevMargin += actor.width / 2;
}
actor.x = prevWidth + prevMargin;
actor.starty = bounds.getVerticalPos();
// Draw the box with the attached line
const height = svgDraw.drawActor(diagram, actor, conf, isFooter);
maxHeight = common.getMax(maxHeight, height);
bounds.insert(actor.x, verticalPos, actor.x + actor.width, actor.height);
prevWidth += actor.width + prevMargin;
@ -554,6 +548,28 @@ export const drawActors = function (
bounds.bumpVerticalPos(maxHeight);
};
export const drawActors = function (diagram, actors, actorKeys, isFooter) {
if (!isFooter) {
for (const actorKey of actorKeys) {
const actor = actors[actorKey];
// Draw the box with the attached line
svgDraw.drawActor(diagram, actor, conf, false);
}
} else {
let maxHeight = 0;
bounds.bumpVerticalPos(conf.boxMargin * 2);
for (const actorKey of actorKeys) {
const actor = actors[actorKey];
if (!actor.stopy) {
actor.stopy = bounds.getVerticalPos();
}
const height = svgDraw.drawActor(diagram, actor, conf, true);
maxHeight = common.getMax(maxHeight, height);
}
bounds.bumpVerticalPos(maxHeight + conf.boxMargin);
}
};
export const drawActorsPopup = function (diagram, actors, actorKeys, doc) {
let maxHeight = 0;
let maxWidth = 0;
@ -633,6 +649,95 @@ function adjustLoopHeightForWrap(loopWidths, msg, preMargin, postMargin, addLoop
bounds.bumpVerticalPos(heightAdjust);
}
/**
* Adjust the msgModel and the actor for the rendering in case the latter is created or destroyed by the msg
* @param msg - the potentially creating or destroying message
* @param msgModel - the model associated with the message
* @param lineStartY - the y position of the message line
* @param index - the index of the current actor under consideration
* @param actors - the array of all actors
* @param createdActors - the array of actors created in the diagram
* @param destroyedActors - the array of actors destroyed in the diagram
*/
function adjustCreatedDestroyedData(
msg,
msgModel,
lineStartY,
index,
actors,
createdActors,
destroyedActors
) {
function receiverAdjustment(actor, adjustment) {
if (actor.x < actors[msg.from].x) {
bounds.insert(
msgModel.stopx - adjustment,
msgModel.starty,
msgModel.startx,
msgModel.stopy + actor.height / 2 + conf.noteMargin
);
msgModel.stopx = msgModel.stopx + adjustment;
} else {
bounds.insert(
msgModel.startx,
msgModel.starty,
msgModel.stopx + adjustment,
msgModel.stopy + actor.height / 2 + conf.noteMargin
);
msgModel.stopx = msgModel.stopx - adjustment;
}
}
function senderAdjustment(actor, adjustment) {
if (actor.x < actors[msg.to].x) {
bounds.insert(
msgModel.startx - adjustment,
msgModel.starty,
msgModel.stopx,
msgModel.stopy + actor.height / 2 + conf.noteMargin
);
msgModel.startx = msgModel.startx + adjustment;
} else {
bounds.insert(
msgModel.stopx,
msgModel.starty,
msgModel.startx + adjustment,
msgModel.stopy + actor.height / 2 + conf.noteMargin
);
msgModel.startx = msgModel.startx - adjustment;
}
}
// if it is a create message
if (createdActors[msg.to] == index) {
const actor = actors[msg.to];
const adjustment = actor.type == 'actor' ? ACTOR_TYPE_WIDTH / 2 + 3 : actor.width / 2 + 3;
receiverAdjustment(actor, adjustment);
actor.starty = lineStartY - actor.height / 2;
bounds.bumpVerticalPos(actor.height / 2);
}
// if it is a destroy sender message
else if (destroyedActors[msg.from] == index) {
const actor = actors[msg.from];
if (conf.mirrorActors) {
const adjustment = actor.type == 'actor' ? ACTOR_TYPE_WIDTH / 2 : actor.width / 2;
senderAdjustment(actor, adjustment);
}
actor.stopy = lineStartY - actor.height / 2;
bounds.bumpVerticalPos(actor.height / 2);
}
// if it is a destroy receiver message
else if (destroyedActors[msg.to] == index) {
const actor = actors[msg.to];
if (conf.mirrorActors) {
const adjustment = actor.type == 'actor' ? ACTOR_TYPE_WIDTH / 2 + 3 : actor.width / 2 + 3;
receiverAdjustment(actor, adjustment);
}
actor.stopy = lineStartY - actor.height / 2;
bounds.bumpVerticalPos(actor.height / 2);
}
}
/**
* Draws a sequenceDiagram in the tag with id: id based on the graph definition in text.
*
@ -666,8 +771,10 @@ export const draw = function (_text: string, id: string, _version: string, diagO
// Fetch data from the parsing
const actors = diagObj.db.getActors();
const createdActors = diagObj.db.getCreatedActors();
const destroyedActors = diagObj.db.getDestroyedActors();
const boxes = diagObj.db.getBoxes();
const actorKeys = diagObj.db.getActorKeys();
let actorKeys = diagObj.db.getActorKeys();
const messages = diagObj.db.getMessages();
const title = diagObj.db.getDiagramTitle();
const hasBoxes = diagObj.db.hasAtLeastOneBox();
@ -686,7 +793,16 @@ export const draw = function (_text: string, id: string, _version: string, diagO
}
}
drawActors(diagram, actors, actorKeys, 0, conf, messages, false);
if (conf.hideUnusedParticipants === true) {
const newActors = new Set();
messages.forEach((message) => {
newActors.add(message.from);
newActors.add(message.to);
});
actorKeys = actorKeys.filter((actorKey) => newActors.has(actorKey));
}
addActorRenderingData(diagram, actors, createdActors, actorKeys, 0, messages, false);
const loopWidths = calculateLoopBounds(messages, actors, maxMessageWidthPerActor, diagObj);
// The arrow head definition is attached to the svg once
@ -720,7 +836,8 @@ export const draw = function (_text: string, id: string, _version: string, diagO
let sequenceIndex = 1;
let sequenceIndexStep = 1;
const messagesToDraw = [];
messages.forEach(function (msg) {
const backgrounds = [];
messages.forEach(function (msg, index) {
let loopModel, noteModel, msgModel;
switch (msg.type) {
@ -757,7 +874,7 @@ export const draw = function (_text: string, id: string, _version: string, diagO
break;
case diagObj.db.LINETYPE.RECT_END:
loopModel = bounds.endLoop();
svgDraw.drawBackgroundRect(diagram, loopModel);
backgrounds.push(loopModel);
bounds.models.addLoop(loopModel);
bounds.bumpVerticalPos(loopModel.stopy - bounds.getVerticalPos());
break;
@ -876,13 +993,20 @@ export const draw = function (_text: string, id: string, _version: string, diagO
break;
default:
try {
// lastMsg = msg
bounds.resetVerticalPos();
msgModel = msg.msgModel;
msgModel.starty = bounds.getVerticalPos();
msgModel.sequenceIndex = sequenceIndex;
msgModel.sequenceVisible = diagObj.db.showSequenceNumbers();
const lineStartY = boundMessage(diagram, msgModel);
adjustCreatedDestroyedData(
msg,
msgModel,
lineStartY,
index,
actors,
createdActors,
destroyedActors
);
messagesToDraw.push({ messageModel: msgModel, lineStartY: lineStartY });
bounds.models.addMessage(msgModel);
} catch (e) {
@ -907,15 +1031,16 @@ export const draw = function (_text: string, id: string, _version: string, diagO
}
});
messagesToDraw.forEach((e) => drawMessage(diagram, e.messageModel, e.lineStartY, diagObj));
log.debug('createdActors', createdActors);
log.debug('destroyedActors', destroyedActors);
drawActors(diagram, actors, actorKeys, false);
messagesToDraw.forEach((e) => drawMessage(diagram, e.messageModel, e.lineStartY, diagObj));
if (conf.mirrorActors) {
// Draw actors below diagram
bounds.bumpVerticalPos(conf.boxMargin * 2);
drawActors(diagram, actors, actorKeys, bounds.getVerticalPos(), conf, messages, true);
bounds.bumpVerticalPos(conf.boxMargin);
fixLifeLineHeights(diagram, bounds.getVerticalPos());
drawActors(diagram, actors, actorKeys, true);
}
backgrounds.forEach((e) => svgDraw.drawBackgroundRect(diagram, e));
fixLifeLineHeights(diagram, actors, actorKeys, conf);
bounds.models.boxes.forEach(function (box) {
box.height = bounds.getVerticalPos() - box.y;
@ -937,11 +1062,6 @@ export const draw = function (_text: string, id: string, _version: string, diagO
const { bounds: box } = bounds.getBounds();
// Adjust line height of actor lines now that the height of the diagram is known
log.debug('For line height fix Querying: #' + id + ' .actor-line');
const actorLines = selectAll('#' + id + ' .actor-line');
actorLines.attr('y2', box.stopy);
// Make sure the height of the diagram supports long menus.
let boxHeight = box.stopy - box.starty;
if (boxHeight < requiredBoxSize.maxHeight) {

View File

@ -4,6 +4,8 @@ import { addFunction } from '../../interactionDb.js';
import { ZERO_WIDTH_SPACE, parseFontSize } from '../../utils.js';
import { sanitizeUrl } from '@braintree/sanitize-url';
export const ACTOR_TYPE_WIDTH = 18 * 2;
export const drawRect = function (elem, rectData) {
return svgDrawCommon.drawRect(elem, rectData);
};
@ -294,14 +296,19 @@ export const drawLabel = function (elem, txtObject) {
let actorCnt = -1;
export const fixLifeLineHeights = (diagram, bounds) => {
if (!diagram.selectAll) {
export const fixLifeLineHeights = (diagram, actors, actorKeys, conf) => {
if (!diagram.select) {
return;
}
diagram
.selectAll('.actor-line')
.attr('class', '200')
.attr('y2', bounds - 55);
actorKeys.forEach((actorKey) => {
const actor = actors[actorKey];
const actorDOM = diagram.select('#actor' + actor.actorCnt);
if (!conf.mirrorActors && actor.stopy) {
actorDOM.attr('y2', actor.stopy + actor.height / 2);
} else if (conf.mirrorActors) {
actorDOM.attr('y2', actor.stopy);
}
});
};
/**
@ -313,10 +320,11 @@ export const fixLifeLineHeights = (diagram, bounds) => {
* @param {boolean} isFooter - If the actor is the footer one
*/
const drawActorTypeParticipant = function (elem, actor, conf, isFooter) {
const actorY = isFooter ? actor.stopy : actor.starty;
const center = actor.x + actor.width / 2;
const centerY = actor.y + 5;
const centerY = actorY + 5;
const boxpluslineGroup = elem.append('g');
const boxpluslineGroup = elem.append('g').lower();
var g = boxpluslineGroup;
if (!isFooter) {
@ -328,6 +336,7 @@ const drawActorTypeParticipant = function (elem, actor, conf, isFooter) {
.attr('x2', center)
.attr('y2', 2000)
.attr('class', 'actor-line')
.attr('class', '200')
.attr('stroke-width', '0.5px')
.attr('stroke', '#999');
@ -348,7 +357,7 @@ const drawActorTypeParticipant = function (elem, actor, conf, isFooter) {
rect.fill = '#eaeaea';
}
rect.x = actor.x;
rect.y = actor.y;
rect.y = actorY;
rect.width = actor.width;
rect.height = actor.height;
rect.class = cssclass;
@ -388,8 +397,11 @@ const drawActorTypeParticipant = function (elem, actor, conf, isFooter) {
};
const drawActorTypeActor = function (elem, actor, conf, isFooter) {
const actorY = isFooter ? actor.stopy : actor.starty;
const center = actor.x + actor.width / 2;
const centerY = actor.y + 80;
const centerY = actorY + 80;
elem.lower();
if (!isFooter) {
actorCnt++;
@ -401,15 +413,18 @@ const drawActorTypeActor = function (elem, actor, conf, isFooter) {
.attr('x2', center)
.attr('y2', 2000)
.attr('class', 'actor-line')
.attr('class', '200')
.attr('stroke-width', '0.5px')
.attr('stroke', '#999');
actor.actorCnt = actorCnt;
}
const actElem = elem.append('g');
actElem.attr('class', 'actor-man');
const rect = svgDrawCommon.getNoteRect();
rect.x = actor.x;
rect.y = actor.y;
rect.y = actorY;
rect.fill = '#eaeaea';
rect.width = actor.width;
rect.height = actor.height;
@ -421,33 +436,33 @@ const drawActorTypeActor = function (elem, actor, conf, isFooter) {
.append('line')
.attr('id', 'actor-man-torso' + actorCnt)
.attr('x1', center)
.attr('y1', actor.y + 25)
.attr('y1', actorY + 25)
.attr('x2', center)
.attr('y2', actor.y + 45);
.attr('y2', actorY + 45);
actElem
.append('line')
.attr('id', 'actor-man-arms' + actorCnt)
.attr('x1', center - 18)
.attr('y1', actor.y + 33)
.attr('x2', center + 18)
.attr('y2', actor.y + 33);
.attr('x1', center - ACTOR_TYPE_WIDTH / 2)
.attr('y1', actorY + 33)
.attr('x2', center + ACTOR_TYPE_WIDTH / 2)
.attr('y2', actorY + 33);
actElem
.append('line')
.attr('x1', center - 18)
.attr('y1', actor.y + 60)
.attr('x1', center - ACTOR_TYPE_WIDTH / 2)
.attr('y1', actorY + 60)
.attr('x2', center)
.attr('y2', actor.y + 45);
.attr('y2', actorY + 45);
actElem
.append('line')
.attr('x1', center)
.attr('y1', actor.y + 45)
.attr('x2', center + 16)
.attr('y2', actor.y + 60);
.attr('y1', actorY + 45)
.attr('x2', center + ACTOR_TYPE_WIDTH / 2 - 2)
.attr('y2', actorY + 60);
const circle = actElem.append('circle');
circle.attr('cx', actor.x + actor.width / 2);
circle.attr('cy', actor.y + 10);
circle.attr('cy', actorY + 10);
circle.attr('r', 15);
circle.attr('width', actor.width);
circle.attr('height', actor.height);