Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions build/sequence-diagram-min.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion build/sequence-diagram-min.js.map

Large diffs are not rendered by default.

76 changes: 75 additions & 1 deletion src/diagram.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,28 +50,62 @@
};

Diagram.prototype.addSignal = function(signal) {
// Set the numerical index of the signal.
signal.index = this.signals.length;
this.signals.push( signal );
};

Diagram.Actor = function(alias, name, index) {
this.alias = alias;
this.name = name;
this.index = index;
this.executionStack = [];
this.executions = [];
this.maxExecutionsLevel = -1;
};

Diagram.Signal = function(actorA, signaltype, actorB, message) {
Diagram.Signal = function(actorA, signaltype, actorB, message,
executionLevelChangeA, executionLevelChangeB) {
this.type = "Signal";
this.actorA = actorA;
this.actorB = actorB;
this.linetype = signaltype & 3;
this.arrowtype = (signaltype >> 2) & 3;
this.message = message;
this.index = null;
// If this is a self-signal and an Execution level modifier was only applied to the
// left-hand side of the signal, move it to the right-hand side to prevent rendering issues.
if (actorA === actorB && executionLevelChangeB === Diagram.EXECUTION_LVL_CHANGE.UNCHANGED) {
executionLevelChangeB = executionLevelChangeA;
executionLevelChangeA = Diagram.EXECUTION_LVL_CHANGE.UNCHANGED;
}

if (actorA === actorB && executionLevelChangeA === executionLevelChangeB &&
executionLevelChangeA !== Diagram.EXECUTION_LVL_CHANGE.UNCHANGED) {
throw new Error("You cannot move the Execution nesting level in the same " +
"direction twice on a single self-signal.");
}
this.actorA.changeExecutionLevel(executionLevelChangeA, this);
this.startLevel = this.actorA.executionStack.length - 1;
this.actorB.changeExecutionLevel(executionLevelChangeB, this);
this.endLevel = this.actorB.executionStack.length - 1;
};

Diagram.Signal.prototype.isSelf = function() {
return this.actorA.index == this.actorB.index;
};

/*
* If the signal is a self signal, this method returns the higher Execution nesting level
* between the start and end of the signal.
*/
Diagram.Signal.prototype.maxExecutionLevel = function () {
if (!this.isSelf()) {
throw new Error("maxExecutionLevel() was called on a non-self signal.");
}
return Math.max(this.startLevel, this.endLevel);
};

Diagram.Note = function(actor, placement, message) {
this.type = "Note";
this.actor = actor;
Expand All @@ -83,6 +117,40 @@
}
};

Diagram.Execution = function(actor, startSignal, level) {
this.actor = actor;
this.startSignal = startSignal;
this.endSignal = null;
this.level = level;
};

Diagram.Actor.prototype.changeExecutionLevel = function(change, signal) {
switch (change) {
case Diagram.EXECUTION_LVL_CHANGE.UNCHANGED:
break;
case Diagram.EXECUTION_LVL_CHANGE.INCREASE_LEVEL:
var newLevel = this.executionStack.length;
this.maxExecutionsLevel =
Math.max(this.maxExecutionsLevel, newLevel);
var execution = new Diagram.Execution(this, signal, newLevel);
this.executionStack.push(execution);
this.executions.push(execution);
break;
case Diagram.EXECUTION_LVL_CHANGE.DECREASE_LEVEL:
if (this.executionStack.length > 0) {
this.executionStack.pop().setEndSignal(signal);
} else {
throw new Error("The execution level for actor " + this.name +
" was dropped below 0.");
}
break;
}
};

Diagram.Execution.prototype.setEndSignal = function (signal) {
this.endSignal = signal;
};

Diagram.Note.prototype.hasManyActors = function() {
return _.isArray(this.actor);
};
Expand All @@ -108,6 +176,12 @@
OVER : 2
};

Diagram.EXECUTION_LVL_CHANGE = {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe just "Diagram.EXECUTION_CHANGE"

UNCHANGED : 0,
INCREASE_LEVEL : 1,
DECREASE_LEVEL : -1
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then, None:0, INCREASE:1, DECREASE:2.

Does the LVL, and LEVEL help explain?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not really. I should be able to get to this tonight.

};


// Some older browsers don't have getPrototypeOf, thus we polyfill it
// https://github.com/bramp/js-sequence-diagrams/issues/57
Expand Down
2 changes: 1 addition & 1 deletion src/grammar.ebnf
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ statement ::=
( 'left of' | 'right of') actor
| 'over' (actor | actor ',' actor)
) ':' message
| actor ( '-' | '--' ) ( '>' | '>>' )? actor ':' message
| ( '-' | '+' )? actor ( '-' | '--' ) ( '>' | '>>' )? ( '-' | '+' )? actor ':' message
)

/*
Expand Down
13 changes: 10 additions & 3 deletions src/grammar.jison
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,11 @@
"note" return 'note';
"title" return 'title';
"," return ',';
[^\->:,\r\n"]+ return 'ACTOR';
[^\->:,\r\n"+]+ return 'ACTOR';
\"[^"]+\" return 'ACTOR';
"--" return 'DOTLINE';
"-" return 'LINE';
"+" return 'PLUS';
">>" return 'OPENARROW';
">" return 'ARROW';
:[^\r\n]+ return 'MESSAGE';
Expand Down Expand Up @@ -76,8 +77,14 @@ placement
;

signal
: actor signaltype actor message
{ $$ = new Diagram.Signal($1, $2, $3, $4); }
: execution_modifier actor signaltype execution_modifier actor message
{ $$ = new Diagram.Signal($2, $3, $5, $6, $1, $4); }
;

execution_modifier
: /* empty */ { $$ = Diagram.EXECUTION_LVL_CHANGE.UNCHANGED }
| LINE { $$ = Diagram.EXECUTION_LVL_CHANGE.DECREASE_LEVEL }
| PLUS { $$ = Diagram.EXECUTION_LVL_CHANGE.INCREASE_LEVEL }
;

actor
Expand Down
97 changes: 95 additions & 2 deletions src/sequence-diagram.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@

var SELF_SIGNAL_WIDTH = 20; // How far out a self signal goes

var EXECUTION_WIDTH = 10;
var OVERLAPPING_EXECUTION_OFFSET = EXECUTION_WIDTH * 0.5;

var PLACEMENT = Diagram.PLACEMENT;
var LINETYPE = Diagram.LINETYPE;
var ARROWTYPE = Diagram.ARROWTYPE;
Expand All @@ -47,6 +50,12 @@
'fill': "#fff"
};

var EXECUTION_RECT = {
'stroke': '#000',
'stroke-width': 2,
'fill': '#e6e6e6' // Color taken from the UML examples
};

function AssertException(message) { this.message = message; }
AssertException.prototype.toString = function () {
return 'AssertException: ' + this.message;
Expand Down Expand Up @@ -76,6 +85,27 @@
return box.y + box.height / 2;
}

/******************
* Drawing-related extra diagram methods.
******************/

// These functions return the x-offset from the lifeline centre given the current Execution nesting-level.
function executionMarginLeft(level) {
if (level < 0) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is level allowed to be <0? Should that be stopped elsewhere?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A level < 0 indicates that there are no executions (just the actor/lifeline) It is impossible to go lower than -1, the parser will throw an error.

return 0;
} else {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need for the else, so you return above.

return -EXECUTION_WIDTH * 0.5 + level * OVERLAPPING_EXECUTION_OFFSET;
}
}

function executionMarginRight(level) {
if (level < 0) {
return 0;
} else {
return EXECUTION_WIDTH * 0.5 + level * OVERLAPPING_EXECUTION_OFFSET;
}
}

/******************
* Raphaël extras
******************/
Expand Down Expand Up @@ -230,6 +260,7 @@

this.draw_title();
this.draw_actors(y);
this.draw_executions(y + this._actors_height);
this.draw_signals(y + this._actors_height);

this._paper.setFinish();
Expand Down Expand Up @@ -274,6 +305,11 @@

a.distances = [];
a.padding_right = 0;
if (a.maxExecutionsLevel >= 0) {
a.padding_right = (EXECUTION_WIDTH / 2.0) +
(a.maxExecutionsLevel *
OVERLAPPING_EXECUTION_OFFSET);
}
self._actors_height = Math.max(a.height, self._actors_height);
});

Expand Down Expand Up @@ -418,6 +454,52 @@
this.draw_text_box(actor, actor.name, ACTOR_MARGIN, ACTOR_PADDING, this._font);
},

draw_executions : function (offsetY) {
var y = offsetY;
var self = this;

// Calculate the y-positions of each signal before we attempt to draw the executions.
_.each(this.diagram.signals, function(s) {
if (s.type == "Signal") {
if (s.isSelf()) {
s.startY = y + SIGNAL_MARGIN;
s.endY = s.startY + s.height - SIGNAL_MARGIN;
} else {
s.startY = s.endY = y + s.height - SIGNAL_MARGIN - SIGNAL_PADDING;
}
}

y += s.height;
});

_.each(this.diagram.actors, function(a) {
self.draw_actors_executions(a);
});
},

draw_actors_executions : function (actor) {
var self = this;
_.each(actor.executions, function (e) {
var aX = getCenterX(actor);
aX += e.level * OVERLAPPING_EXECUTION_OFFSET;
var x = aX - EXECUTION_WIDTH / 2.0;
var y;
var w = EXECUTION_WIDTH;
var h;
if (e.startSignal === e.endSignal) {
y = e.startSignal.startY;
h = e.endSignal ? e.endSignal.endY - y : (actor.y - y);
} else {
y = e.startSignal.endY;
h = e.endSignal ? e.endSignal.startY - y : (actor.y - y);
}

// Draw actual execution.
var rect = self.draw_rect(x, y, w, h);
rect.attr(EXECUTION_RECT);
});
},

draw_signals : function (offsetY) {
var y = offsetY;
var self = this;
Expand All @@ -442,6 +524,7 @@

var text_bb = signal.text_bb;
var aX = getCenterX(signal.actorA);
aX += executionMarginRight(signal.maxExecutionLevel());

var x = aX + SELF_SIGNAL_WIDTH + SIGNAL_PADDING - text_bb.x;
var y = offsetY + signal.height / 2;
Expand All @@ -452,18 +535,20 @@
'stroke-dasharray': this.line_types[signal.linetype]
});

var x1 = getCenterX(signal.actorA) + executionMarginRight(signal.startLevel);
var x2 = getCenterX(signal.actorA) + executionMarginRight(signal.endLevel);
var y1 = offsetY + SIGNAL_MARGIN;
var y2 = y1 + signal.height - SIGNAL_MARGIN;

// Draw three lines, the last one with a arrow
var line;
line = this.draw_line(aX, y1, aX + SELF_SIGNAL_WIDTH, y1);
line = this.draw_line(x1, y1, aX + SELF_SIGNAL_WIDTH, y1);
line.attr(attr);

line = this.draw_line(aX + SELF_SIGNAL_WIDTH, y1, aX + SELF_SIGNAL_WIDTH, y2);
line.attr(attr);

line = this.draw_line(aX + SELF_SIGNAL_WIDTH, y2, aX, y2);
line = this.draw_line(aX + SELF_SIGNAL_WIDTH, y2, x2, y2);
attr['arrow-end'] = this.arrow_types[signal.arrowtype] + '-wide-long';
line.attr(attr);
},
Expand All @@ -472,6 +557,14 @@
var aX = getCenterX( signal.actorA );
var bX = getCenterX( signal.actorB );

if (bX > aX) {
aX += executionMarginRight(signal.startLevel);
bX += executionMarginLeft(signal.endLevel);
} else {
aX += executionMarginLeft(signal.startLevel);
bX += executionMarginRight(signal.endLevel);
}

// Mid point between actors
var x = (bX - aX) / 2 + aX;
var y = offsetY + SIGNAL_MARGIN + 2*SIGNAL_PADDING;
Expand Down
46 changes: 46 additions & 0 deletions test/grammar-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,13 @@ function assertEmptyDocument(d) {
equal(d.signals.length, 0, "Zero signals");
}

function testExecutions(execution, affectedActorName, startSignal, endSignal, level) {
equal(execution.actor.name, affectedActorName, "Correct actor");
equal(execution.startSignal, startSignal, "Start signal of Execution");
equal(execution.endSignal, endSignal, "End signal of Execution");
equal(execution.level, level, "Nesting level of Execution");
}


var LINETYPE = Diagram.LINETYPE;
var ARROWTYPE = Diagram.ARROWTYPE;
Expand Down Expand Up @@ -185,6 +192,45 @@ test( "Quoted names", function() {
assertSingleArrow(Diagram.parse("\"->:\"->B: M"), ARROWTYPE.FILLED, LINETYPE.SOLID, "->:", "B", "M");
assertSingleArrow(Diagram.parse("A->\"->:\": M"), ARROWTYPE.FILLED, LINETYPE.SOLID, "A", "->:", "M");
assertSingleActor(Diagram.parse("Participant \"->:\""), "->:");
assertSingleArrow(Diagram.parse("A->\"+B\": M"), ARROWTYPE.FILLED, LINETYPE.SOLID, "A", "+B", "M");
assertSingleArrow(Diagram.parse("\"+A\"->B: M"), ARROWTYPE.FILLED, LINETYPE.SOLID, "+A", "B", "M");
assertSingleArrow(Diagram.parse("\"+A\"->\"+B\": M"), ARROWTYPE.FILLED, LINETYPE.SOLID, "+A", "+B", "M");
});

test( "Executions", function () {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does
++A->B
do anything?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll have to check what happens, I know -- will throw an error.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I imagine ++ will throw an error too. I've only allowed single changes to the execution level.

assertSingleArrow(Diagram.parse("A->+B: M"), ARROWTYPE.FILLED, LINETYPE.SOLID, "A", "B", "M");
assertSingleArrow(Diagram.parse("+A->B: M"), ARROWTYPE.FILLED, LINETYPE.SOLID, "A", "B", "M");
assertSingleArrow(Diagram.parse("+A-->+B: M"), ARROWTYPE.FILLED, LINETYPE.DOTTED, "A", "B", "M");
assertSingleArrow(Diagram.parse("+\"+A\"-->+B: M"), ARROWTYPE.FILLED, LINETYPE.DOTTED, "+A", "B", "M");

var d = Diagram.parse("A->+B: M1\n+B-->-B: M2\n-B-->>+A: M3");
equal(d.actors.length, 2, "Correct actors count");

var a = d.actors[0];
var b = d.actors[1];
equal(a.name, "A", "Actors A name");
equal(b.name, "B", "Actors B name");
var execsA = a.executions;
var execsB = b.executions;

equal(d.signals.length, 3, "Correct signals count");
equal(execsA.length, 1, "Correct actor A Execution count");
equal(execsB.length, 2, "Correct actor B Execution count");

// More or less normal Execution
testExecutions(execsB[0], "B", d.signals[0], d.signals[2], 0);
// Self-signalled Execution
testExecutions(execsB[1], "B", d.signals[1], d.signals[1], 1);
// Endless Execution
testExecutions(execsA[0], "A", d.signals[2], null, 0);

// Make sure we haven't broken the different arrow types.
equal(d.signals[0].arrowtype, ARROWTYPE.FILLED, "Signal 1 Arrow Type");
equal(d.signals[0].linetype, LINETYPE.SOLID, "Signal 1 Line Type");
equal(d.signals[1].arrowtype, ARROWTYPE.FILLED, "Signal 2 Arrow Type");
equal(d.signals[1].linetype, LINETYPE.DOTTED, "Signal 2 Line Type");
equal(d.signals[2].arrowtype, ARROWTYPE.OPEN, "Signal 3 Arrow Type");
equal(d.signals[2].linetype, LINETYPE.DOTTED, "Signal 3 Line Type");
});

test( "API", function() {
Expand Down