Skip to content
Open
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
/*.seq
*.svg
*~
*#
sequence/__pycache__/*
33 changes: 20 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Sequence
A tool for creating SVG sequence diagrams from text input files.

###Example
### Example
Sequence lets you make sequence diagrams that look like this:

<img src="http://jasonreisman.github.io/sequence/test.png" width="640`">
Expand Down Expand Up @@ -29,12 +29,11 @@ Browser, Browser, Rasterizes content
Browser, User, Presents content
@endphase
```
###Data format

### Data format
Sequence generates its SVG from a simple text data file. There are two basic entities in the data file: _steps_ and _phases_. Lines that begin with a hashtag (#) will be interpretted as comments, and have no effect on the image rendered. Empty lines are also ignored.

####Steps

#### Steps
Each _step_ in the sequence, where a step is defined as _an action between two actors_, is a single line in the input data file. (A step will be rendered as an arrow with text between actor columns.) The line must be comma-separated and contain either three or four values. The first value is the source actor, the one invoking the action. The second entry is the target actor, the one receiving the action. The third entry is a description of the action itself. The fourth value is optional, but if present will be used as the step's color.

For example:
Expand All @@ -54,8 +53,7 @@ Client, Server, Makes Request, green

**Note #2**: if the source actor and target actor are the same, then no arrow will be drawn between the actors. Instead a dot will appear in the source/target actor column with the text describing the action.

####Phases

#### Phases
A _phase_ is a collection of steps which will visually grouped together in the SVG. Phases are opened by the special directive in the data `@phase`. When opening a phase you will need to supply the name for the phase. This is just text included after the open phase directive. For example:

```
Expand Down Expand Up @@ -84,10 +82,9 @@ Bob, Bob, Decrypts message w/ priv key

which produces this SVG:

<img src="http://jasonreisman.github.io/sequence/alice_bob.png" width="480`">

####Ordering
<img src="help/alice_bob.png" width="480`">

#### Ordering
By defaults, actors will be rendered from left to right in the order that they appear in the sequence file. However, if you want to specify a specific order for the actors in your diagram, you can do so by using the `@order` directive as the first line in your sequence file, followed by the (comma separated) specific actor order that you'd like to see. Comment and blank lines may preceed `@order`, but nothing else.

For instance, if we wanted to change the previous example to be in the order Alice, Bob, Keystore (instead of Bob, Keystore, Alice) we could add a `@order` directive at the top:
Expand All @@ -107,14 +104,24 @@ Bob, Bob, Decrypts message w/ priv key

which then produces this SVG:

<img src="http://jasonreisman.github.io/sequence/alice_bob_ordered.png" width="480`">
<img src="help/alice_bob_ordered.png" width="480`">

Any actors not specified in the `@order` directive will appear in the order they appear in the rest of the file.

### Prerequisites
You must have a python 2.7 installation and install the Python package `svgwrite` (e.g., `pip install svgwrite`)
You must have a python 3 installation and install the Python package `svgwrite` (e.g., `pip install svgwrite`)

###Usage
### Usage
```
python make_sequence.py in.seq > out.svg
usage: make_sequence.py [-h] --in text_flow_filename --out svg_filename [--debug]

A tool for creating SVG sequence diagrams from text input files.

options:
-h, --help show this help message and exit
--in text_flow_filename, -i text_flow_filename
Flow text file input.
--out svg_filename, -o svg_filename
This is the result svg file.
--debug, -d Enable debug mode.
```
Binary file added help/alice_bob.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added help/alice_bob_ordered.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
255 changes: 70 additions & 185 deletions make_sequence.py
Original file line number Diff line number Diff line change
@@ -1,190 +1,75 @@
import svgwrite
#!/usr/bin/env python3

import argparse
import os.path
import sys

class Colors:
black = '#000000'
gray = '#C0C0C0'

class Phase:
def __init__(self, name, color, action0):
self.name = name
self.color = color
self.action0 = action0
self.action1 = None

class Sequence:
def __init__(self, filename):
self.filename = filename
self.actors = []
self.actors_map = {}
self.actions = []
self.phases = []
# read actions into file
self.parse_input_file(filename)
# create drawing
self.top_left = (100, 50)
self.step_size = (100, 25)
self.text_fudge = (0, 5)
self.width = self.top_left[0] + (len(self.actors) + 1)*self.step_size[0]
self.height = self.top_left[1] + (len(self.actions) + 1)*self.step_size[1]
self.drawing = svgwrite.Drawing(size=(self.width, self.height))
self.markers = {}

def parse_input_file(self, filename):
# read actions into file
open_phases = []
num_lines_processed = 0
with open(filename) as f:
for i, line in enumerate(f):
line = line.strip()
if len(line) == 0:
# skip empty lines
continue
if line[0] == '#':
# skip comments
continue
if line.startswith('@phase'):
tokens = map(lambda s : s.strip(), line[len('@phase'):].split(','))
assert len(tokens) > 0, '@phase must contain at least a name'
phase_name = tokens[0]
assert len(phase_name) > 0, '@phase must contain at least a name'
phase_color = tokens[1] if len(tokens) > 1 else Colors.gray
p = Phase(phase_name, phase_color, len(self.actions))
open_phases.append(p)
elif line.startswith('@endphase'):
assert len(open_phases) > 0, '@endphase found with no corresponding opening @phase'
p = open_phases.pop()
p.action1 = len(self.actions)
self.phases.append(p)
elif line.startswith('@order'):
assert num_lines_processed==0, '@order may only be on the first line!'
tokens = map(lambda s : s.strip(), line[len('@order'):].split(','))
for t in tokens:
key = len(self.actors)
self.actors_map[t] = key
self.actors.append(t)
else:
self.parse_step(line)
num_lines_processed += 1
assert len(open_phases) == 0, '@phase opened without corresponding closing @endphase'

def parse_step(self, line):
tokens = map(lambda s : s.strip(), line.split(','))
assert len(tokens) >= 3, 'line %i has less than 3 tokens' % (i)
if len(tokens) < 3:
return
actor0 = tokens[0]
actor1 = tokens[1]
action = tokens[2]
if actor0 not in self.actors_map:
key = len(self.actors)
self.actors_map[actor0] = key
self.actors.append(actor0)
if actor1 not in self.actors_map:
key = len(self.actors)
self.actors_map[actor1] = key
self.actors.append(actor1)
key0 = self.actors_map[actor0]
key1 = self.actors_map[actor1]
color = Colors.black
if len(tokens) > 3:
color = tokens[3].strip()
self.actions.append((key0, key1, action, color))


def build(self):
self.create_header()
self.create_actors()
self.create_phases()
self.create_actions()
pass

def to_string(self):
return self.drawing.tostring()

def create_header(self):
text = 'filename: %s' % self.filename
self.drawing.add(self.drawing.text(text, insert=(self.top_left[0] - 0.5*self.step_size[0], 0.5*self.top_left[1]), stroke='none', fill=Colors.gray, font_family="Helevetica", font_size="8pt", text_anchor="start"))

def create_phases(self):
for phase in self.phases:
action0 = self.actions[phase.action0]
action1 = self.actions[phase.action1 - 1]

left = len(self.actors) + 1
right = -1
for action in self.actions[phase.action0:phase.action1]:
mn = min(action[0:2])
mx = max(action[0:2])
left = mn if mn < left else left
right = mx if mx > right else right
x = self.top_left[0] + (left - 0.5)*self.step_size[0]
y = self.top_left[1] + (phase.action0 + 0.5)*self.step_size[1]
w = (right - left + 1)*self.step_size[0]
h = (phase.action1 - phase.action0 - 0.125)*self.step_size[1]
filled_rect = self.drawing.add(self.drawing.rect((x,y), (w,h)))
filled_rect.fill(phase.color, None, 0.15)
self.drawing.add(self.drawing.rect((x,y), (w,h), stroke=phase.color, stroke_width=1, fill='none'))
transform = 'rotate(180,%i,%i) translate(6,0)' % (x, y+0.5*h)
self.drawing.add(self.drawing.text(phase.name, insert=(x, y+0.5*h), stroke='none', fill=phase.color, font_family="Helevetica", font_size="6pt", text_anchor="middle", writing_mode="tb", transform=transform))

def create_actors(self):
x = self.top_left[0]
y = self.top_left[1]
for name in self.actors:
self.drawing.add(self.drawing.text(name, insert=(x, y - self.text_fudge[1]), stroke='none', fill=Colors.black, font_family="Helevetica", font_size="8pt", text_anchor="middle"))
line = self.drawing.add(self.drawing.line((x, y), (x, self.height), stroke=Colors.gray, stroke_width=1))
line.dasharray([5, 5])
x += self.step_size[0]

def create_actions(self):
markers = {}
# add actions
y = self.top_left[1] + self.step_size[1]
for (a0, a1, act, color) in self.actions:
x0 = self.top_left[0] + a0*self.step_size[0]
x1 = self.top_left[0] + a1*self.step_size[0]
if a0 == a1:
self.drawing.add(self.drawing.circle((x0, y), r=2, fill=color))
else:
start_marker, end_marker = self.get_markers(color)
assert start_marker is not None
assert end_marker is not None
line = self.drawing.add(self.drawing.line((x0, y), (x1, y), stroke=color, stroke_width=1))
line['marker-start'] = start_marker.get_funciri()
line['marker-end'] = end_marker.get_funciri()
self.drawing.add(self.drawing.text(act, insert=(0.5*(x0 + x1), y - self.text_fudge[1]), stroke='none', fill=color, font_family="Helevetica", font_size="6pt", text_anchor="middle"))
y += self.step_size[1]

def get_markers(self, color):
# create or get marker objects
start_marker, end_marker = None, None
if color in self.markers:
start_marker, end_marker = self.markers[color]
else:
start_marker = self.drawing.marker(insert=(2,2), size=(10,10), orient='auto')
start_marker.add(self.drawing.circle((2,2), r=2, fill=color))
self.drawing.defs.add(start_marker)
end_marker = self.drawing.marker(insert=(6,3), size=(10,10), orient='auto')
end_marker.add(self.drawing.path("M0,0 L0,7 L6,3 L0,0", fill=color))
self.drawing.defs.add(end_marker)
self.markers[color] = (start_marker, end_marker)
return start_marker, end_marker

def usage():
print 'Usage: ./make_sequence.py <in filename> > <out filename>'
sys.exit(-1)
import logging
from sequence import sequence

# Default logging configuration
logging.basicConfig(level=logging.INFO)

# Get logger object
logger = logging.getLogger(__name__)

class MakeSequence(sequence.Sequence):
def __init__(self, filename, loglevel=logging.INFO):
self.sequence_lines = []
# read actions into list
self.parse_input_file(filename)
# create drawing
super().__init__(self.sequence_lines, loglevel=loglevel)


def parse_input_file(self, filename):
# read actions into file
open_phases = []
num_lines_processed = 0
with open(filename, 'rt') as f:
for i, line in enumerate(f):
line = line.strip()
self.sequence_lines.append(line)

def main():
logging_level = logging.INFO
# Parameters
cmd_param = argparse.ArgumentParser(
description='A tool for creating SVG sequence diagrams from text input files.')
cmd_param.add_argument('--in', '-i', dest='inputfile',
metavar='text_flow_filename',
type=str, required=True,
help='Flow text file input.')
cmd_param.add_argument('--out', '-o', dest='outputfile',
metavar='svg_filename', type=str,
required=True,
help='This is the result svg file.')
cmd_param.add_argument('--debug', '-d', dest='debug', default=False,
required=False, action='store_true',
help='Enable debug mode.')

param = cmd_param.parse_args()
if param.debug:
logging_level = logging.DEBUG
# change logger verbose level
logger.setLevel(logging.DEBUG)
txtin_flow_filename = param.inputfile
svgout_flow_filename = param.outputfile

logger.info('Process start')

if not os.path.isfile(txtin_flow_filename):
print(f'File {txtin_flow_filename} not found')
sys.exit(-1)

logger.info('Processing sequence text file')
seq = MakeSequence(txtin_flow_filename, loglevel=logging_level)
seq.build()

logger.debug('SVG file: %s' %seq.to_string())

with open(svgout_flow_filename, 'w') as f:
print(seq.to_string(), file=f)
logger.info('SVG file saved')

if __name__ == '__main__':
if len(sys.argv) < 2:
print 'missing input filename'
usage()
filename = sys.argv[1]
if not os.path.isfile(filename):
print 'file %s not found' % filename
sys.exit(-1)
seq = Sequence(sys.argv[1])
seq.build()
print seq.to_string()
main()
Empty file added sequence/__init__.py
Empty file.
8 changes: 8 additions & 0 deletions sequence/colors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import logging

# Get logger object
logger = logging.getLogger(__name__)

class Colors:
black = '#000000'
gray = '#C0C0C0'
14 changes: 14 additions & 0 deletions sequence/phase.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import logging

# Get logger object
logger = logging.getLogger(__name__)

class PhaseError(Exception):
pass

class Phase(object):
def __init__(self, name, color, action0):
self.name = name
self.color = color
self.action0 = action0
self.action1 = None
Loading