gregoryv/draw

View on GitHub
design/seqdia.go

Summary

Maintainability
A
3 hrs
Test Coverage
package design

import (
    "io"
    "reflect"

    "github.com/gregoryv/draw/shape"
)

// NewSequenceDiagram returns a sequence diagram with default column
// width.
func NewSequenceDiagram() *SequenceDiagram {
    return &SequenceDiagram{
        Diagram:  NewDiagram(),
        ColWidth: 190,
        VMargin:  10,
    }
}

// SequenceDiagram defines columns and links between columns.
type SequenceDiagram struct {
    *Diagram
    ColWidth int
    VMargin  int // top margin for each horizontal lane

    columns []string
    links   []*Link
    groups  []group
}

type group struct {
    fromColumn string
    toColumn   string
    text       string
    class      string
}

// Group adds a colored area below span of columns. Predefined classes
// are red, green, blue.
func (me *SequenceDiagram) Group(fromColumn, toColumn, text, class string) {
    g := group{
        fromColumn: fromColumn,
        toColumn:   toColumn,
        text:       text,
        class:      class,
    }
    if me.groups == nil {
        me.groups = []group{g}
        return
    }
    me.groups = append(me.groups, g)
}

// WriteSvg renders the diagram as SVG to the given writer.
func (d *SequenceDiagram) WriteSVG(w io.Writer) error {
    var (
        colWidth = d.ColWidth

        top = d.top()
        x   = d.Pad.Left
        y1  = top + d.TextPad.Bottom + d.Font.LineHeight // below label
        y2  = d.Height()
    )
    lines := make([]*shape.Line, len(d.columns))
    vlines := make(map[string]*shape.Line)
    // save x values for rendering skip lines
    columnX := make([]int, len(d.columns))
    // columns and vertical lines
    for i, column := range d.columns {
        // todo add label in Add, AddStruct and AddInterface methods
        // so one can place other shapes relative to them
        label := shape.NewLabel(column)
        label.Font = d.Font
        label.Pad = d.Pad
        label.SetX(i * colWidth)
        label.SetY(top)

        firstColumn := i == 0
        if firstColumn {
            x += label.Width() / 2
            columnX = append(columnX, x)
        }
        line := shape.NewLine(x, y1, x, y2)
        line.SetClass("column-line")
        lines[i] = line
        x += colWidth
        columnX = append(columnX, x)

        d.VAlignCenter(lines[i], label)
        d.Place(lines[i], label)
        vlines[column] = line // save for groups

        // groups
        for _, group := range d.groups {
            if group.toColumn == column { // assume from is already there
                r := shape.NewRect("") // add align label

                alabel := shape.NewLabel(group.text)
                alabel.Font = d.Font
                alabel.Pad = d.Pad
                alabel.SetClass("area-" + group.class + "-label")

                from := vlines[group.fromColumn]
                to := line
                x, y := from.Position()
                x2, _ := to.Position()
                width := x2 - x
                r.SetX(x)
                r.SetY(y)
                r.SetWidth(width)
                r.SetClass("area-" + group.class)
                r.SetHeight(y2 + d.top() + label.Height())
                d.Prepend(r) // behind

                d.Place(alabel).Below(r)
                d.VAlignCenter(r, alabel)
                d.HAlignBottom(r, alabel)
                shape.Move(alabel, 0, -alabel.Pad.Bottom)
            }
        }
    }

    y := y1 + d.plainHeight()
    for _, lnk := range d.links {
        if lnk == skip {
            for _, x := range columnX {
                dots := shape.NewLine(x, y, x, y+d.Font.LineHeight)
                dots.SetClass("skip")
                d.Place(dots)
            }
            y += d.plainHeight()
            continue
        }
        fromX := lines[lnk.fromIndex].Start.X
        toX := lines[lnk.toIndex].Start.X
        label := shape.NewLabel(lnk.text)
        label.Font = d.Font
        label.Pad = d.Pad
        label.SetX(fromX)
        label.SetY(y - 3 - d.Font.LineHeight)

        if lnk.toSelf() {
            margin := 15
            // add two lines + arrow
            l1 := shape.NewLine(fromX, y, fromX+margin, y)
            l1.SetClass(lnk.class())
            l2 := shape.NewLine(fromX+margin, y, fromX+margin, y+d.Font.LineHeight*2)
            l2.SetClass(lnk.class())
            d.HAlignCenter(l2, label)
            label.SetX(fromX + l1.Width() + d.TextPad.Left)
            label.SetY(y + 3)

            arrow := shape.NewArrow(
                l2.End.X,
                l2.End.Y,
                l1.Start.X,
                l2.End.Y,
            )
            arrow.SetClass(lnk.class())

            d.Place(l1, l2, arrow, label)
            y += d.selfHeight()
        } else {
            arrow := shape.NewArrow(
                fromX,
                y,
                toX,
                y,
            )
            arrow.SetClass(lnk.class())
            d.VAlignCenter(arrow, label)
            d.Place(arrow, label)
            y += d.plainHeight()
        }
    }
    return d.Diagram.WriteSVG(w)
}

// Width returns the total width of the diagram
func (d *SequenceDiagram) Width() int {
    w := d.SVG.Width()
    if w != 0 {
        return w
    }
    return len(d.columns) * d.ColWidth
}

// Height returns the total height of the diagram
func (d *SequenceDiagram) Height() int {
    h := d.SVG.Height()
    if h != 0 {
        return h
    }
    if len(d.columns) == 0 {
        return 0
    }
    height := d.top() + d.plainHeight()
    for _, lnk := range d.links {
        if lnk.toSelf() {
            height += d.selfHeight()
            continue
        }
        height += d.plainHeight()
    }
    return height
}

// selfHeight is the height of a self referencing link
func (d *SequenceDiagram) selfHeight() int {
    return 3*d.Font.LineHeight + d.Pad.Bottom
}

// plainHeight returns the height of and arrow and label
func (d *SequenceDiagram) plainHeight() int {
    return d.Font.LineHeight + d.Pad.Bottom + d.VMargin
}

func (d *SequenceDiagram) top() int {
    return d.Pad.Top
}

// AddColumns adds the names as columns in the given order.
func (d *SequenceDiagram) AddColumns(names ...string) {
    for _, name := range names {
        d.Add(name)
    }
}

// Add the name as next column and return name.
func (d *SequenceDiagram) Add(name string) string {
    d.columns = append(d.columns, name)
    return name
}

func (d *SequenceDiagram) SaveAs(filename string) error {
    return saveAs(d, d.Style, filename)
}

// Inline returns rendered SVG with inlined style
func (d *SequenceDiagram) Inline() string {
    return inline(d, d.Style)
}

// String returns rendered SVG
func (d *SequenceDiagram) String() string { return toString(d) }

func (d *SequenceDiagram) AddStruct(obj interface{}) string {
    name := reflect.TypeOf(obj).String()
    d.Add(name)
    return name
}

func (d *SequenceDiagram) AddInterface(obj interface{}) string {
    name := reflect.TypeOf(obj).Elem().String()
    d.Add(name)
    return name
}