mirror of
https://github.com/gonum/gonum.git
synced 2025-10-06 23:52:47 +08:00
583 lines
13 KiB
Go
583 lines
13 KiB
Go
// Copyright ©2015 The Gonum Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package dot
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
|
|
"gonum.org/v1/gonum/graph"
|
|
"gonum.org/v1/gonum/graph/encoding"
|
|
"gonum.org/v1/gonum/graph/internal/ordered"
|
|
)
|
|
|
|
// Node is a DOT graph node.
|
|
type Node interface {
|
|
// DOTID returns a DOT node ID.
|
|
//
|
|
// An ID is one of the following:
|
|
//
|
|
// - a string of alphabetic ([a-zA-Z\x80-\xff]) characters, underscores ('_').
|
|
// digits ([0-9]), not beginning with a digit.
|
|
// - a numeral [-]?(.[0-9]+ | [0-9]+(.[0-9]*)?).
|
|
// - a double-quoted string ("...") possibly containing escaped quotes (\").
|
|
// - an HTML string (<...>).
|
|
DOTID() string
|
|
}
|
|
|
|
// Attributers are graph.Graph values that specify top-level DOT
|
|
// attributes.
|
|
type Attributers interface {
|
|
DOTAttributers() (graph, node, edge encoding.Attributer)
|
|
}
|
|
|
|
// Porter defines the behavior of graph.Edge values that can specify
|
|
// connection ports for their end points. The returned port corresponds
|
|
// to the the DOT node port to be used by the edge, compass corresponds
|
|
// to DOT compass point to which the edge will be aimed.
|
|
type Porter interface {
|
|
// FromPort returns the port and compass for the
|
|
// From node of a graph.Edge.
|
|
FromPort() (port, compass string)
|
|
|
|
// ToPort returns the port and compass for the
|
|
// To node of a graph.Edge.
|
|
ToPort() (port, compass string)
|
|
}
|
|
|
|
// Structurer represents a graph.Graph that can define subgraphs.
|
|
type Structurer interface {
|
|
Structure() []Graph
|
|
}
|
|
|
|
// MultiStructurer represents a graph.Multigraph that can define subgraphs.
|
|
type MultiStructurer interface {
|
|
Structure() []Multigraph
|
|
}
|
|
|
|
// Graph wraps named graph.Graph values.
|
|
type Graph interface {
|
|
graph.Graph
|
|
DOTID() string
|
|
}
|
|
|
|
// Multigraph wraps named graph.Multigraph values.
|
|
type Multigraph interface {
|
|
graph.Multigraph
|
|
DOTID() string
|
|
}
|
|
|
|
// Subgrapher wraps graph.Node values that represent subgraphs.
|
|
type Subgrapher interface {
|
|
Subgraph() graph.Graph
|
|
}
|
|
|
|
// MultiSubgrapher wraps graph.Node values that represent subgraphs.
|
|
type MultiSubgrapher interface {
|
|
Subgraph() graph.Multigraph
|
|
}
|
|
|
|
// Marshal returns the DOT encoding for the graph g, applying the prefix
|
|
// and indent to the encoding. Name is used to specify the graph name. If
|
|
// name is empty and g implements Graph, the returned string from DOTID
|
|
// will be used.
|
|
//
|
|
// Graph serialization will work for a graph.Graph without modification,
|
|
// however, advanced GraphViz DOT features provided by Marshal depend on
|
|
// implementation of the Node, Attributer, Porter, Attributers, Structurer,
|
|
// Subgrapher and Graph interfaces.
|
|
func Marshal(g graph.Graph, name, prefix, indent string) ([]byte, error) {
|
|
var p simpleGraphPrinter
|
|
p.indent = indent
|
|
p.prefix = prefix
|
|
p.visited = make(map[edge]bool)
|
|
err := p.print(g, name, false, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return p.buf.Bytes(), nil
|
|
}
|
|
|
|
// MarshalMulti returns the DOT encoding for the multigraph g, applying the
|
|
// prefix and indent to the encoding. Name is used to specify the graph name. If
|
|
// name is empty and g implements Graph, the returned string from DOTID
|
|
// will be used. If strict is true the output bytes will be prefixed with
|
|
// the DOT "strict" keyword.
|
|
//
|
|
// Graph serialization will work for a graph.Multigraph without modification,
|
|
// however, advanced GraphViz DOT features provided by Marshal depend on
|
|
// implementation of the Node, Attributer, Porter, Attributers, Structurer,
|
|
// MultiSubgrapher and Multigraph interfaces.
|
|
func MarshalMulti(g graph.Multigraph, name, prefix, indent string) ([]byte, error) {
|
|
var p multiGraphPrinter
|
|
p.indent = indent
|
|
p.prefix = prefix
|
|
p.visited = make(map[line]bool)
|
|
err := p.print(g, name, false, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return p.buf.Bytes(), nil
|
|
}
|
|
|
|
type printer struct {
|
|
buf bytes.Buffer
|
|
|
|
prefix string
|
|
indent string
|
|
depth int
|
|
|
|
err error
|
|
}
|
|
|
|
type edge struct {
|
|
inGraph string
|
|
from, to int64
|
|
}
|
|
|
|
func (p *simpleGraphPrinter) print(g graph.Graph, name string, needsIndent, isSubgraph bool) error {
|
|
if name == "" {
|
|
if g, ok := g.(Graph); ok {
|
|
name = g.DOTID()
|
|
}
|
|
}
|
|
|
|
_, isDirected := g.(graph.Directed)
|
|
p.printFrontMatter(name, needsIndent, isSubgraph, isDirected, true)
|
|
|
|
if a, ok := g.(Attributers); ok {
|
|
p.writeAttributeComplex(a)
|
|
}
|
|
if s, ok := g.(Structurer); ok {
|
|
for _, g := range s.Structure() {
|
|
_, subIsDirected := g.(graph.Directed)
|
|
if subIsDirected != isDirected {
|
|
return errors.New("dot: mismatched graph type")
|
|
}
|
|
p.buf.WriteByte('\n')
|
|
p.print(g, g.DOTID(), true, true)
|
|
}
|
|
}
|
|
|
|
nodes := graph.NodesOf(g.Nodes())
|
|
sort.Sort(ordered.ByID(nodes))
|
|
|
|
havePrintedNodeHeader := false
|
|
for _, n := range nodes {
|
|
if s, ok := n.(Subgrapher); ok {
|
|
// If the node is not linked to any other node
|
|
// the graph needs to be written now.
|
|
if g.From(n.ID()).Len() == 0 {
|
|
g := s.Subgraph()
|
|
_, subIsDirected := g.(graph.Directed)
|
|
if subIsDirected != isDirected {
|
|
return errors.New("dot: mismatched graph type")
|
|
}
|
|
if !havePrintedNodeHeader {
|
|
p.newline()
|
|
p.buf.WriteString("// Node definitions.")
|
|
havePrintedNodeHeader = true
|
|
}
|
|
p.newline()
|
|
p.print(g, graphID(g, n), false, true)
|
|
}
|
|
continue
|
|
}
|
|
if !havePrintedNodeHeader {
|
|
p.newline()
|
|
p.buf.WriteString("// Node definitions.")
|
|
havePrintedNodeHeader = true
|
|
}
|
|
p.newline()
|
|
p.writeNode(n)
|
|
if a, ok := n.(encoding.Attributer); ok {
|
|
p.writeAttributeList(a)
|
|
}
|
|
p.buf.WriteByte(';')
|
|
}
|
|
|
|
havePrintedEdgeHeader := false
|
|
for _, n := range nodes {
|
|
nid := n.ID()
|
|
to := graph.NodesOf(g.From(nid))
|
|
sort.Sort(ordered.ByID(to))
|
|
for _, t := range to {
|
|
tid := t.ID()
|
|
if isDirected {
|
|
if p.visited[edge{inGraph: name, from: nid, to: tid}] {
|
|
continue
|
|
}
|
|
p.visited[edge{inGraph: name, from: nid, to: tid}] = true
|
|
} else {
|
|
if p.visited[edge{inGraph: name, from: nid, to: tid}] {
|
|
continue
|
|
}
|
|
p.visited[edge{inGraph: name, from: nid, to: tid}] = true
|
|
p.visited[edge{inGraph: name, from: tid, to: n.ID()}] = true
|
|
}
|
|
|
|
if !havePrintedEdgeHeader {
|
|
p.buf.WriteByte('\n')
|
|
p.buf.WriteString(strings.TrimRight(p.prefix, " \t\n")) // Trim whitespace suffix.
|
|
p.newline()
|
|
p.buf.WriteString("// Edge definitions.")
|
|
havePrintedEdgeHeader = true
|
|
}
|
|
p.newline()
|
|
|
|
if s, ok := n.(Subgrapher); ok {
|
|
g := s.Subgraph()
|
|
_, subIsDirected := g.(graph.Directed)
|
|
if subIsDirected != isDirected {
|
|
return errors.New("dot: mismatched graph type")
|
|
}
|
|
p.print(g, graphID(g, n), false, true)
|
|
} else {
|
|
p.writeNode(n)
|
|
}
|
|
e := g.Edge(nid, tid)
|
|
porter, edgeIsPorter := e.(Porter)
|
|
if edgeIsPorter {
|
|
if e.From().ID() == nid {
|
|
p.writePorts(porter.FromPort())
|
|
} else {
|
|
p.writePorts(porter.ToPort())
|
|
}
|
|
}
|
|
|
|
if isDirected {
|
|
p.buf.WriteString(" -> ")
|
|
} else {
|
|
p.buf.WriteString(" -- ")
|
|
}
|
|
|
|
if s, ok := t.(Subgrapher); ok {
|
|
g := s.Subgraph()
|
|
_, subIsDirected := g.(graph.Directed)
|
|
if subIsDirected != isDirected {
|
|
return errors.New("dot: mismatched graph type")
|
|
}
|
|
p.print(g, graphID(g, t), false, true)
|
|
} else {
|
|
p.writeNode(t)
|
|
}
|
|
if edgeIsPorter {
|
|
if e.From().ID() == nid {
|
|
p.writePorts(porter.ToPort())
|
|
} else {
|
|
p.writePorts(porter.FromPort())
|
|
}
|
|
}
|
|
|
|
if a, ok := g.Edge(nid, tid).(encoding.Attributer); ok {
|
|
p.writeAttributeList(a)
|
|
}
|
|
|
|
p.buf.WriteByte(';')
|
|
}
|
|
}
|
|
|
|
p.closeBlock("}")
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *printer) printFrontMatter(name string, needsIndent, isSubgraph, isDirected, isStrict bool) error {
|
|
p.buf.WriteString(p.prefix)
|
|
if needsIndent {
|
|
for i := 0; i < p.depth; i++ {
|
|
p.buf.WriteString(p.indent)
|
|
}
|
|
}
|
|
|
|
if !isSubgraph && isStrict {
|
|
p.buf.WriteString("strict ")
|
|
}
|
|
|
|
if isSubgraph {
|
|
p.buf.WriteString("sub")
|
|
} else if isDirected {
|
|
p.buf.WriteString("di")
|
|
}
|
|
p.buf.WriteString("graph")
|
|
|
|
if name != "" {
|
|
p.buf.WriteByte(' ')
|
|
p.buf.WriteString(name)
|
|
}
|
|
|
|
p.openBlock(" {")
|
|
return nil
|
|
}
|
|
|
|
func (p *printer) writeNode(n graph.Node) {
|
|
p.buf.WriteString(nodeID(n))
|
|
}
|
|
|
|
func (p *printer) writePorts(port, cp string) {
|
|
if port != "" {
|
|
p.buf.WriteByte(':')
|
|
p.buf.WriteString(port)
|
|
}
|
|
if cp != "" {
|
|
p.buf.WriteByte(':')
|
|
p.buf.WriteString(cp)
|
|
}
|
|
}
|
|
|
|
func nodeID(n graph.Node) string {
|
|
switch n := n.(type) {
|
|
case Node:
|
|
return n.DOTID()
|
|
default:
|
|
return fmt.Sprint(n.ID())
|
|
}
|
|
}
|
|
|
|
func graphID(g interface{}, n graph.Node) string {
|
|
switch g := g.(type) {
|
|
case Node:
|
|
return g.DOTID()
|
|
default:
|
|
return nodeID(n)
|
|
}
|
|
}
|
|
|
|
func (p *printer) writeAttributeList(a encoding.Attributer) {
|
|
attributes := a.Attributes()
|
|
switch len(attributes) {
|
|
case 0:
|
|
case 1:
|
|
p.buf.WriteString(" [")
|
|
p.buf.WriteString(attributes[0].Key)
|
|
p.buf.WriteByte('=')
|
|
p.buf.WriteString(attributes[0].Value)
|
|
p.buf.WriteString("]")
|
|
default:
|
|
p.openBlock(" [")
|
|
for _, att := range attributes {
|
|
p.newline()
|
|
p.buf.WriteString(att.Key)
|
|
p.buf.WriteByte('=')
|
|
p.buf.WriteString(att.Value)
|
|
}
|
|
p.closeBlock("]")
|
|
}
|
|
}
|
|
|
|
var attType = []string{"graph", "node", "edge"}
|
|
|
|
func (p *printer) writeAttributeComplex(ca Attributers) {
|
|
g, n, e := ca.DOTAttributers()
|
|
haveWrittenBlock := false
|
|
for i, a := range []encoding.Attributer{g, n, e} {
|
|
attributes := a.Attributes()
|
|
if len(attributes) == 0 {
|
|
continue
|
|
}
|
|
if haveWrittenBlock {
|
|
p.buf.WriteByte(';')
|
|
}
|
|
p.newline()
|
|
p.buf.WriteString(attType[i])
|
|
p.openBlock(" [")
|
|
for _, att := range attributes {
|
|
p.newline()
|
|
p.buf.WriteString(att.Key)
|
|
p.buf.WriteByte('=')
|
|
p.buf.WriteString(att.Value)
|
|
}
|
|
p.closeBlock("]")
|
|
haveWrittenBlock = true
|
|
}
|
|
if haveWrittenBlock {
|
|
p.buf.WriteString(";\n")
|
|
}
|
|
}
|
|
|
|
func (p *printer) newline() {
|
|
p.buf.WriteByte('\n')
|
|
p.buf.WriteString(p.prefix)
|
|
for i := 0; i < p.depth; i++ {
|
|
p.buf.WriteString(p.indent)
|
|
}
|
|
}
|
|
|
|
func (p *printer) openBlock(b string) {
|
|
p.buf.WriteString(b)
|
|
p.depth++
|
|
}
|
|
|
|
func (p *printer) closeBlock(b string) {
|
|
p.depth--
|
|
p.newline()
|
|
p.buf.WriteString(b)
|
|
}
|
|
|
|
type simpleGraphPrinter struct {
|
|
printer
|
|
visited map[edge]bool
|
|
}
|
|
|
|
type multiGraphPrinter struct {
|
|
printer
|
|
visited map[line]bool
|
|
}
|
|
|
|
type line struct {
|
|
inGraph string
|
|
id int64
|
|
}
|
|
|
|
func (p *multiGraphPrinter) print(g graph.Multigraph, name string, needsIndent, isSubgraph bool) error {
|
|
if name == "" {
|
|
if g, ok := g.(Multigraph); ok {
|
|
name = g.DOTID()
|
|
}
|
|
}
|
|
|
|
_, isDirected := g.(graph.Directed)
|
|
p.printFrontMatter(name, needsIndent, isSubgraph, isDirected, false)
|
|
|
|
if a, ok := g.(Attributers); ok {
|
|
p.writeAttributeComplex(a)
|
|
}
|
|
if s, ok := g.(MultiStructurer); ok {
|
|
for _, g := range s.Structure() {
|
|
_, subIsDirected := g.(graph.Directed)
|
|
if subIsDirected != isDirected {
|
|
return errors.New("dot: mismatched graph type")
|
|
}
|
|
p.buf.WriteByte('\n')
|
|
p.print(g, g.DOTID(), true, true)
|
|
}
|
|
}
|
|
|
|
nodes := graph.NodesOf(g.Nodes())
|
|
sort.Sort(ordered.ByID(nodes))
|
|
|
|
havePrintedNodeHeader := false
|
|
for _, n := range nodes {
|
|
if s, ok := n.(MultiSubgrapher); ok {
|
|
// If the node is not linked to any other node
|
|
// the graph needs to be written now.
|
|
if g.From(n.ID()).Len() == 0 {
|
|
g := s.Subgraph()
|
|
_, subIsDirected := g.(graph.Directed)
|
|
if subIsDirected != isDirected {
|
|
return errors.New("dot: mismatched graph type")
|
|
}
|
|
if !havePrintedNodeHeader {
|
|
p.newline()
|
|
p.buf.WriteString("// Node definitions.")
|
|
havePrintedNodeHeader = true
|
|
}
|
|
p.newline()
|
|
p.print(g, graphID(g, n), false, true)
|
|
}
|
|
continue
|
|
}
|
|
if !havePrintedNodeHeader {
|
|
p.newline()
|
|
p.buf.WriteString("// Node definitions.")
|
|
havePrintedNodeHeader = true
|
|
}
|
|
p.newline()
|
|
p.writeNode(n)
|
|
if a, ok := n.(encoding.Attributer); ok {
|
|
p.writeAttributeList(a)
|
|
}
|
|
p.buf.WriteByte(';')
|
|
}
|
|
|
|
havePrintedEdgeHeader := false
|
|
for _, n := range nodes {
|
|
nid := n.ID()
|
|
to := graph.NodesOf(g.From(nid))
|
|
sort.Sort(ordered.ByID(to))
|
|
|
|
for _, t := range to {
|
|
tid := t.ID()
|
|
|
|
lines := graph.LinesOf(g.Lines(nid, tid))
|
|
sort.Sort(ordered.LinesByIDs(lines))
|
|
|
|
for _, l := range lines {
|
|
lid := l.ID()
|
|
if p.visited[line{inGraph: name, id: lid}] {
|
|
continue
|
|
}
|
|
p.visited[line{inGraph: name, id: lid}] = true
|
|
|
|
if !havePrintedEdgeHeader {
|
|
p.buf.WriteByte('\n')
|
|
p.buf.WriteString(strings.TrimRight(p.prefix, " \t\n")) // Trim whitespace suffix.
|
|
p.newline()
|
|
p.buf.WriteString("// Edge definitions.")
|
|
havePrintedEdgeHeader = true
|
|
}
|
|
p.newline()
|
|
|
|
if s, ok := n.(MultiSubgrapher); ok {
|
|
g := s.Subgraph()
|
|
_, subIsDirected := g.(graph.Directed)
|
|
if subIsDirected != isDirected {
|
|
return errors.New("dot: mismatched graph type")
|
|
}
|
|
p.print(g, graphID(g, n), false, true)
|
|
} else {
|
|
p.writeNode(n)
|
|
}
|
|
|
|
porter, edgeIsPorter := l.(Porter)
|
|
if edgeIsPorter {
|
|
if l.From().ID() == nid {
|
|
p.writePorts(porter.FromPort())
|
|
} else {
|
|
p.writePorts(porter.ToPort())
|
|
}
|
|
}
|
|
|
|
if isDirected {
|
|
p.buf.WriteString(" -> ")
|
|
} else {
|
|
p.buf.WriteString(" -- ")
|
|
}
|
|
|
|
if s, ok := t.(MultiSubgrapher); ok {
|
|
g := s.Subgraph()
|
|
_, subIsDirected := g.(graph.Directed)
|
|
if subIsDirected != isDirected {
|
|
return errors.New("dot: mismatched graph type")
|
|
}
|
|
p.print(g, graphID(g, t), false, true)
|
|
} else {
|
|
p.writeNode(t)
|
|
}
|
|
if edgeIsPorter {
|
|
if l.From().ID() == nid {
|
|
p.writePorts(porter.ToPort())
|
|
} else {
|
|
p.writePorts(porter.FromPort())
|
|
}
|
|
}
|
|
|
|
if a, ok := l.(encoding.Attributer); ok {
|
|
p.writeAttributeList(a)
|
|
}
|
|
|
|
p.buf.WriteByte(';')
|
|
}
|
|
}
|
|
}
|
|
|
|
p.closeBlock("}")
|
|
|
|
return nil
|
|
}
|