graph/layout: new package for calculating graph layouts

This commit is contained in:
Dan Kortschak
2019-07-20 20:12:57 +09:30
committed by GitHub
parent b8a36307b8
commit c1fdadf7ea
18 changed files with 814 additions and 2 deletions

1
go.mod
View File

@@ -6,4 +6,5 @@ require (
golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2
golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e
gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0
gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b
)

18
go.sum
View File

@@ -1,6 +1,24 @@
github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af h1:wVe6/Ea46ZMeNkQjjBW6xcqyQA/j5e0D6GytH95g0gQ=
github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90 h1:WXb3TSNmHp2vHoCroCIB1foO/yQ36swABL8aOVeDpgg=
github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5 h1:PJr+ZMXIecYc1Ey2zucXdR73SMBtgjPgwa31099IMv0=
github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2 h1:y102fOLFqhV41b+4GPiJoa0k/x+pJcEi2/HB1Y5T6fU=
golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81 h1:00VmoueYNlNz/aHIilyyQz/MHSqGoWJzpFv/HW8xpzI=
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e h1:Io7mpb+aUAGF0MKxbyQ7HQl1VgB+cL6ZJZUFaFNqVV4=
golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo=
gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0 h1:OE9mWmgKkjJyEmDAAtGMPjXu+YNeGvK9VTSHY6+Qihc=
gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b h1:Qh4dB5D/WpoUUp3lSod7qgoyEHbDGPUWjIbnqdqqe1k=
gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc=
rsc.io/pdf v0.1.1 h1:k1MczvYDUvJBe93bYd7wrZLLUEcLZAuF824/I4e5Xr4=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

6
graph/layout/doc.go Normal file
View File

@@ -0,0 +1,6 @@
// Copyright ©2019 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 layout defines functions for performing graph layout.
package layout // import "gonum.org/v1/gonum/graph/layout"

171
graph/layout/eades.go Normal file
View File

@@ -0,0 +1,171 @@
// Copyright ©2019 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 layout
import (
"math"
"golang.org/x/exp/rand"
"gonum.org/v1/gonum/graph"
"gonum.org/v1/gonum/spatial/barneshut"
"gonum.org/v1/gonum/spatial/r2"
)
// EadesR2 implements the graph layout algorithm essentially as
// described in "A heuristic for graph drawing", Congressus
// numerantium 42:149-160.
// The implementation here uses the Barnes-Hut approximation for
// global repulsion calculation, and edge weights are considered
// when calculating adjacent node attraction.
type EadesR2 struct {
// Updates is the number of updates to perform.
Updates int
// Repulsion is the strength of the global
// repulsive force between nodes in the
// layout. It corresponds to C3 in the paper.
Repulsion float64
// Rate is the gradient descent rate. It
// corresponds to C4 in the paper.
Rate float64
// Theta is the Barnes-Hut theta constant.
Theta float64
// Src is the source of randomness used
// to initialize the nodes' locations. If
// Src is nil, the global random number
// generator is used.
Src rand.Source
nodes graph.Nodes
indexOf map[int64]int
particles []barneshut.Particle2
forces []r2.Vec
}
// Update is the EadesR2 spatial graph update function.
func (u *EadesR2) Update(g graph.Graph, layout LayoutR2) bool {
if u.Updates <= 0 {
return false
}
u.Updates--
if !layout.IsInitialized() {
var rnd func() float64
if u.Src == nil {
rnd = rand.Float64
} else {
rnd = rand.New(u.Src).Float64
}
u.nodes = g.Nodes()
u.indexOf = make(map[int64]int, u.nodes.Len())
u.particles = make([]barneshut.Particle2, 0, u.nodes.Len())
u.forces = make([]r2.Vec, u.nodes.Len())
for u.nodes.Next() {
id := u.nodes.Node().ID()
u.indexOf[id] = len(u.particles)
u.particles = append(u.particles, eadesR2Node{id: id, pos: r2.Vec{X: rnd(), Y: rnd()}})
}
}
u.nodes.Reset()
// Apply global repulsion.
plane, err := barneshut.NewPlane(u.particles)
if err != nil {
return false
}
var updated bool
for i, p := range u.particles {
f := plane.ForceOn(p, u.Theta, barneshut.Gravity2).Scale(-u.Repulsion)
// Prevent marginal updates that can be caused by
// floating point error when nodes are very far apart.
if math.Hypot(f.X, f.Y) > 1e-12 {
updated = true
}
u.forces[i] = f
}
// Handle edge weighting for attraction.
var weight func(uid, vid int64) float64
if wg, ok := g.(graph.Weighted); ok {
if _, ok := g.(graph.Directed); ok {
weight = func(xid, yid int64) float64 {
var w float64
f, ok := wg.Weight(xid, yid)
if ok {
w += f
}
r, ok := wg.Weight(yid, xid)
if ok {
w += r
}
return w
}
} else {
weight = func(xid, yid int64) float64 {
w, ok := wg.Weight(xid, yid)
if ok {
return w
}
return 0
}
}
} else {
// This is only called when the adjacency is known so just return unit.
weight = func(_, _ int64) float64 { return 1 }
}
seen := make(map[[2]int64]bool)
for u.nodes.Next() {
xid := u.nodes.Node().ID()
xidx := u.indexOf[xid]
to := g.From(xid)
for to.Next() {
yid := to.Node().ID()
if seen[[2]int64{xid, yid}] {
continue
}
seen[[2]int64{yid, xid}] = true
yidx := u.indexOf[yid]
// Apply adjacent node attraction.
v := u.particles[yidx].Coord2().Sub(u.particles[xidx].Coord2())
f := v.Scale(weight(xid, yid) * math.Log(math.Hypot(v.X, v.Y)))
if math.Hypot(f.X, f.Y) > 1e-12 {
updated = true
}
u.forces[xidx] = u.forces[xidx].Add(f)
u.forces[yidx] = u.forces[yidx].Sub(f)
}
}
if !updated {
return false
}
rate := u.Rate
if rate == 0 {
rate = 0.1
}
for i, f := range u.forces {
n := u.particles[i].(eadesR2Node)
n.pos = n.pos.Add(f.Scale(rate))
u.particles[i] = n
layout.SetCoord2(n.id, n.pos)
}
return true
}
type eadesR2Node struct {
id int64
pos r2.Vec
}
func (p eadesR2Node) Coord2() r2.Vec { return p.pos }
func (p eadesR2Node) Mass() float64 { return 1 }

233
graph/layout/eades_test.go Normal file
View File

@@ -0,0 +1,233 @@
// Copyright ©2019 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 layout
import (
"path/filepath"
"testing"
"golang.org/x/exp/rand"
"gonum.org/v1/gonum/graph"
"gonum.org/v1/gonum/graph/simple"
"gonum.org/v1/gonum/spatial/r2"
"gonum.org/v1/plot"
"gonum.org/v1/plot/vg"
)
var eadesR2Tests = []struct {
name string
g graph.Graph
param EadesR2
wantIters int
}{
{
name: "line",
g: func() graph.Graph {
edges := []simple.Edge{
{simple.Node(0), simple.Node(1)},
}
g := simple.NewUndirectedGraph()
for _, e := range edges {
g.SetEdge(e)
}
return orderedGraph{g}
}(),
param: EadesR2{Repulsion: 1, Rate: 0.1, Updates: 100, Theta: 0.1, Src: rand.NewSource(1)},
wantIters: 100,
},
{
name: "square",
g: func() graph.Graph {
edges := []simple.Edge{
{simple.Node(0), simple.Node(1)},
{simple.Node(0), simple.Node(2)},
{simple.Node(1), simple.Node(3)},
{simple.Node(2), simple.Node(3)},
}
g := simple.NewUndirectedGraph()
for _, e := range edges {
g.SetEdge(e)
}
return orderedGraph{g}
}(),
param: EadesR2{Repulsion: 1, Rate: 0.1, Updates: 100, Theta: 0.1, Src: rand.NewSource(1)},
wantIters: 100,
},
{
name: "tetrahedron",
g: func() graph.Graph {
edges := []simple.Edge{
{simple.Node(0), simple.Node(1)},
{simple.Node(0), simple.Node(2)},
{simple.Node(0), simple.Node(3)},
{simple.Node(1), simple.Node(2)},
{simple.Node(1), simple.Node(3)},
{simple.Node(2), simple.Node(3)},
}
g := simple.NewUndirectedGraph()
for _, e := range edges {
g.SetEdge(e)
}
return orderedGraph{g}
}(),
param: EadesR2{Repulsion: 1, Rate: 0.1, Updates: 100, Theta: 0.1, Src: rand.NewSource(1)},
wantIters: 100,
},
{
name: "sheet",
g: func() graph.Graph {
edges := []simple.Edge{
{simple.Node(0), simple.Node(1)},
{simple.Node(0), simple.Node(3)},
{simple.Node(1), simple.Node(2)},
{simple.Node(1), simple.Node(4)},
{simple.Node(2), simple.Node(5)},
{simple.Node(3), simple.Node(4)},
{simple.Node(3), simple.Node(6)},
{simple.Node(4), simple.Node(5)},
{simple.Node(4), simple.Node(7)},
{simple.Node(5), simple.Node(8)},
{simple.Node(6), simple.Node(7)},
{simple.Node(7), simple.Node(8)},
}
g := simple.NewUndirectedGraph()
for _, e := range edges {
g.SetEdge(e)
}
return orderedGraph{g}
}(),
param: EadesR2{Repulsion: 1, Rate: 0.1, Updates: 100, Theta: 0.1, Src: rand.NewSource(1)},
wantIters: 100,
},
{
name: "tube",
g: func() graph.Graph {
edges := []simple.Edge{
{simple.Node(0), simple.Node(1)},
{simple.Node(0), simple.Node(2)},
{simple.Node(0), simple.Node(3)},
{simple.Node(1), simple.Node(2)},
{simple.Node(1), simple.Node(4)},
{simple.Node(2), simple.Node(5)},
{simple.Node(3), simple.Node(4)},
{simple.Node(3), simple.Node(5)},
{simple.Node(3), simple.Node(6)},
{simple.Node(4), simple.Node(5)},
{simple.Node(4), simple.Node(7)},
{simple.Node(5), simple.Node(8)},
{simple.Node(6), simple.Node(7)},
{simple.Node(6), simple.Node(8)},
{simple.Node(7), simple.Node(8)},
}
g := simple.NewUndirectedGraph()
for _, e := range edges {
g.SetEdge(e)
}
return orderedGraph{g}
}(),
param: EadesR2{Repulsion: 1, Rate: 0.1, Updates: 100, Theta: 0.1, Src: rand.NewSource(1)},
wantIters: 100,
},
{
// This test does not produce a good layout, but is here to
// ensure that Update does not panic with steep decent rates.
name: "tube-steep",
g: func() graph.Graph {
edges := []simple.Edge{
{simple.Node(0), simple.Node(1)},
{simple.Node(0), simple.Node(2)},
{simple.Node(0), simple.Node(3)},
{simple.Node(1), simple.Node(2)},
{simple.Node(1), simple.Node(4)},
{simple.Node(2), simple.Node(5)},
{simple.Node(3), simple.Node(4)},
{simple.Node(3), simple.Node(5)},
{simple.Node(3), simple.Node(6)},
{simple.Node(4), simple.Node(5)},
{simple.Node(4), simple.Node(7)},
{simple.Node(5), simple.Node(8)},
{simple.Node(6), simple.Node(7)},
{simple.Node(6), simple.Node(8)},
{simple.Node(7), simple.Node(8)},
}
g := simple.NewUndirectedGraph()
for _, e := range edges {
g.SetEdge(e)
}
return orderedGraph{g}
}(),
param: EadesR2{Repulsion: 1, Rate: 1, Updates: 100, Theta: 0.1, Src: rand.NewSource(1)},
wantIters: 99,
},
{
name: "wp_page", // https://en.wikipedia.org/wiki/PageRank#/media/File:PageRanks-Example.jpg
g: func() graph.Graph {
edges := []simple.Edge{
{simple.Node(0), simple.Node(3)},
{simple.Node(1), simple.Node(2)},
{simple.Node(1), simple.Node(3)},
{simple.Node(1), simple.Node(4)},
{simple.Node(1), simple.Node(5)},
{simple.Node(1), simple.Node(6)},
{simple.Node(1), simple.Node(7)},
{simple.Node(1), simple.Node(8)},
{simple.Node(3), simple.Node(4)},
{simple.Node(4), simple.Node(5)},
{simple.Node(4), simple.Node(6)},
{simple.Node(4), simple.Node(7)},
{simple.Node(4), simple.Node(8)},
{simple.Node(4), simple.Node(9)},
{simple.Node(4), simple.Node(10)},
}
g := simple.NewUndirectedGraph()
for _, e := range edges {
g.SetEdge(e)
}
return orderedGraph{g}
}(),
param: EadesR2{Repulsion: 1, Rate: 0.1, Updates: 100, Theta: 0.1, Src: rand.NewSource(1)},
wantIters: 100,
},
}
func TestEadesR2(t *testing.T) {
for _, test := range eadesR2Tests {
eades := test.param
o := NewOptimizerR2(test.g, eades.Update)
var n int
for o.Update() {
n++
}
if n != test.wantIters {
t.Errorf("unexpected number of iterations for %q: got:%d want:%d", test.name, n, test.wantIters)
}
p, err := plot.New()
if err != nil {
t.Errorf("unexpected error: %v", err)
continue
}
p.Add(render{o})
p.HideAxes()
path := filepath.Join("testdata", test.name+".png")
err = p.Save(10*vg.Centimeter, 10*vg.Centimeter, path)
if err != nil {
t.Errorf("unexpected error: %v", err)
continue
}
ok := checkRenderedLayout(t, path)
if !ok {
got := make(map[int64]r2.Vec)
nodes := test.g.Nodes()
for nodes.Next() {
id := nodes.Node().ID()
got[id] = o.Coord2(id)
}
t.Logf("got node positions: %#v", got)
}
}
}

23
graph/layout/layout.go Normal file
View File

@@ -0,0 +1,23 @@
// Copyright ©2019 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 layout
import (
"gonum.org/v1/gonum/graph"
"gonum.org/v1/gonum/spatial/r2"
)
// GraphR2 is a graph with planar spatial representation of node positions.
type GraphR2 interface {
graph.Graph
LayoutNodeR2(id int64) NodeR2
}
// NodeR2 is a graph node with planar spatial representation of its position.
// A NodeR2 is only valid when the graph.Node is not nil.
type NodeR2 struct {
graph.Node
Coord2 r2.Vec
}

120
graph/layout/layout_test.go Normal file
View File

@@ -0,0 +1,120 @@
// Copyright ©2019 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 layout
import (
"bytes"
"encoding/base64"
"fmt"
"image"
"image/png"
"io/ioutil"
"os"
"path/filepath"
"sort"
"strings"
"testing"
"gonum.org/v1/gonum/graph"
"gonum.org/v1/gonum/graph/encoding"
"gonum.org/v1/gonum/graph/internal/ordered"
"gonum.org/v1/gonum/graph/iterator"
"gonum.org/v1/gonum/spatial/r2"
"gonum.org/v1/plot/cmpimg"
)
// orderedGraph wraps a graph.Graph ensuring consistent ordering of nodes
// in graph queries. Removal of this causes to tests to fail due to changes
// in node iteration order, but the produced graph layouts are still good.
type orderedGraph struct {
graph.Graph
}
func (g orderedGraph) Nodes() graph.Nodes {
n := graph.NodesOf(g.Graph.Nodes())
sort.Sort(ordered.ByID(n))
return iterator.NewOrderedNodes(n)
}
func (g orderedGraph) From(id int64) graph.Nodes {
n := graph.NodesOf(g.Graph.From(id))
sort.Sort(ordered.ByID(n))
return iterator.NewOrderedNodes(n)
}
// positionNode is a graph.Node with an XY position.
type positionNode struct {
id int64
pos r2.Vec
}
func (n positionNode) ID() int64 { return n.id }
func (n positionNode) Attributes() []encoding.Attribute {
return []encoding.Attribute{{Key: "pos", Value: fmt.Sprintf(`"%f,%f"`, n.pos.X, n.pos.Y)}}
}
func goldenPath(path string) string {
ext := filepath.Ext(path)
noext := strings.TrimSuffix(path, ext)
return noext + "_golden" + ext
}
func checkRenderedLayout(t *testing.T, path string) (ok bool) {
if *cmpimg.GenerateTestData {
// Recreate Golden image and exit.
golden := goldenPath(path)
_ = os.Remove(golden)
if err := os.Rename(path, golden); err != nil {
t.Fatal(err)
}
return true
}
// Read the images we've just generated and check them against the
// Golden Images.
got, err := ioutil.ReadFile(path)
if err != nil {
t.Errorf("failed to read %s: %v", path, err)
return true
}
golden := goldenPath(path)
want, err := ioutil.ReadFile(golden)
if err != nil {
t.Errorf("failed to read golden file %s: %v", golden, err)
return true
}
typ := filepath.Ext(path)[1:] // remove the dot in ".png"
ok, err = cmpimg.Equal(typ, got, want)
if err != nil {
t.Errorf("failed to compare image for %s: %v", path, err)
return true
}
if !ok {
t.Errorf("image mismatch for %s\n", path)
v1, _, err := image.Decode(bytes.NewReader(got))
if err != nil {
t.Errorf("failed to decode %s: %v", path, err)
return false
}
v2, _, err := image.Decode(bytes.NewReader(want))
if err != nil {
t.Errorf("failed to decode %s: %v", golden, err)
return false
}
dst := image.NewRGBA64(v1.Bounds().Union(v2.Bounds()))
rect := cmpimg.Diff(dst, v1, v2)
t.Logf("image bounds union:%+v diff bounds intersection:%+v", dst.Bounds(), rect)
var buf bytes.Buffer
err = png.Encode(&buf, dst)
if err != nil {
t.Errorf("failed to encode difference png: %v", err)
return false
}
t.Log("IMAGE:" + base64.StdEncoding.EncodeToString(buf.Bytes()))
}
return ok
}

106
graph/layout/optimizer.go Normal file
View File

@@ -0,0 +1,106 @@
// Copyright ©2019 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 layout
import (
"gonum.org/v1/gonum/graph"
"gonum.org/v1/gonum/spatial/r2"
)
// LayoutR2 implements graph layout updates and representations.
type LayoutR2 interface {
// IsInitialized returns whether the Layout is initialized.
IsInitialized() bool
// SetCoord2 sets the coordinates of the node with the given
// id to coords.
SetCoord2(id int64, coords r2.Vec)
// Coord2 returns the coordinated of the node with the given
// id in the graph layout.
Coord2(id int64) r2.Vec
}
// NewOptimizerR2 returns a new layout optimizer. If g implements LayoutR2 the layout
// will be updated into g, otherwise the OptimizerR2 will hold the graph layout. A nil
// value for update is a valid no-op layout update function.
func NewOptimizerR2(g graph.Graph, update func(graph.Graph, LayoutR2) bool) OptimizerR2 {
l, ok := g.(LayoutR2)
if !ok {
l = make(coordinatesR2)
}
return OptimizerR2{
g: g,
layout: l,
Updater: update,
}
}
// coordinatesR2 is the default layout store for R2.
type coordinatesR2 map[int64]r2.Vec
func (c coordinatesR2) IsInitialized() bool { return len(c) != 0 }
func (c coordinatesR2) SetCoord2(id int64, pos r2.Vec) { c[id] = pos }
func (c coordinatesR2) Coord2(id int64) r2.Vec { return c[id] }
// OptimizerR2 is a helper type that holds a graph and layout
// optimization state.
type OptimizerR2 struct {
g graph.Graph
layout LayoutR2
// Updater is the function called for each call to Update.
// It updates the OptimizerR2's spatial distribution of the
// nodes in the backing graph.
Updater func(graph.Graph, LayoutR2) bool
}
// Coord2 returns the location of the node with the given
// ID. The returned value is only valid if the node exists
// in the graph.
func (g OptimizerR2) Coord2(id int64) r2.Vec {
return g.layout.Coord2(id)
}
// Update updates the locations of the nodes in the graph
// according to the provided update function. It returns whether
// the update function is able to further refine the graph's
// node locations.
func (g OptimizerR2) Update() bool {
if g.Updater == nil {
return false
}
return g.Updater(g.g, g.layout)
}
// LayoutNodeR2 implements the GraphR2 interface.
func (g OptimizerR2) LayoutNodeR2(id int64) NodeR2 {
n := g.g.Node(id)
if n == nil {
return NodeR2{}
}
return NodeR2{Node: n, Coord2: g.Coord2(id)}
}
// Node returns the node with the given ID if it exists
// in the graph, and nil otherwise.
func (g OptimizerR2) Node(id int64) graph.Node { return g.g.Node(id) }
// Nodes returns all the nodes in the graph.
func (g OptimizerR2) Nodes() graph.Nodes { return g.g.Nodes() }
// From returns all nodes that can be reached directly
// from the node with the given ID.
func (g OptimizerR2) From(id int64) graph.Nodes { return g.g.From(id) }
// HasEdgeBetween returns whether an edge exists between
// nodes with IDs xid and yid without considering direction.
func (g OptimizerR2) HasEdgeBetween(xid, yid int64) bool { return g.g.HasEdgeBetween(xid, yid) }
// Edge returns the edge from u to v, with IDs uid and vid,
// if such an edge exists and nil otherwise. The node v
// must be directly reachable from u as defined by the
// From method.
func (g OptimizerR2) Edge(uid, vid int64) graph.Edge { return g.g.Edge(uid, vid) }

View File

@@ -0,0 +1,132 @@
// Copyright ©2019 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 layout
import (
"fmt"
"image/color"
"math"
"gonum.org/v1/plot"
"gonum.org/v1/plot/plotter"
"gonum.org/v1/plot/vg"
"gonum.org/v1/plot/vg/draw"
)
const radius = vg.Length(15)
// render implements the plot.Plotter interface for graphs.
type render struct {
GraphR2
}
func (p render) Plot(c draw.Canvas, plt *plot.Plot) {
nodes := p.GraphR2.Nodes()
if nodes.Len() == 0 {
return
}
xys := make(plotter.XYs, 0, nodes.Len())
ids := make([]string, 0, nodes.Len())
for nodes.Next() {
u := nodes.Node()
uid := u.ID()
ur2 := p.GraphR2.LayoutNodeR2(uid)
xys = append(xys, plotter.XY(ur2.Coord2))
ids = append(ids, fmt.Sprint(uid))
to := p.GraphR2.From(uid)
for to.Next() {
v := to.Node()
vid := v.ID()
vr2 := p.GraphR2.LayoutNodeR2(vid)
l, err := plotter.NewLine(plotter.XYs{plotter.XY(ur2.Coord2), plotter.XY(vr2.Coord2)})
if err != nil {
panic(err)
}
l.Plot(c, plt)
if err != nil {
panic(err)
}
}
}
n, err := plotter.NewScatter(xys)
if err != nil {
panic(err)
}
n.GlyphStyle.Shape = nodeGlyph{}
n.GlyphStyle.Radius = radius
n.Plot(c, plt)
l, err := plotter.NewLabels(plotter.XYLabels{XYs: xys, Labels: ids})
if err != nil {
panic(err)
}
fnt, err := vg.MakeFont(plot.DefaultFont, vg.Points(18))
if err != nil {
panic(err)
}
for i := range l.TextStyle {
l.TextStyle[i] = draw.TextStyle{Font: fnt, XAlign: draw.XCenter, YAlign: -0.33}
}
l.Plot(c, plt)
}
// DataRange returns the minimum and maximum X and Y values
func (p render) DataRange() (xmin, xmax, ymin, ymax float64) {
nodes := p.GraphR2.Nodes()
if nodes.Len() == 0 {
return
}
xys := make(plotter.XYs, 0, nodes.Len())
for nodes.Next() {
u := nodes.Node()
uid := u.ID()
ur2 := p.GraphR2.LayoutNodeR2(uid)
xys = append(xys, plotter.XY(ur2.Coord2))
}
return plotter.XYRange(xys)
}
// GlyphBoxes returns a slice of plot.GlyphBoxes, implementing the
// plot.GlyphBoxer interface.
func (p render) GlyphBoxes(plt *plot.Plot) []plot.GlyphBox {
nodes := p.GraphR2.Nodes()
if nodes.Len() == 0 {
return nil
}
b := make([]plot.GlyphBox, nodes.Len())
for i := 0; nodes.Next(); i++ {
u := nodes.Node()
uid := u.ID()
ur2 := p.GraphR2.LayoutNodeR2(uid)
b[i].X = plt.X.Norm(ur2.Coord2.X)
b[i].Y = plt.Y.Norm(ur2.Coord2.Y)
r := radius
b[i].Rectangle = vg.Rectangle{
Min: vg.Point{X: -r, Y: -r},
Max: vg.Point{X: +r, Y: +r},
}
}
return b
}
// nodeGlyph is a glyph that draws a filled circle.
type nodeGlyph struct{}
// DrawGlyph implements the GlyphDrawer interface.
func (nodeGlyph) DrawGlyph(c *draw.Canvas, sty draw.GlyphStyle, pt vg.Point) {
var p vg.Path
c.Push()
c.SetColor(color.White)
p.Move(vg.Point{X: pt.X + sty.Radius, Y: pt.Y})
p.Arc(pt, sty.Radius, 0, 2*math.Pi)
p.Close()
c.Fill(p)
c.Pop()
c.Stroke(p)
}

2
graph/layout/testdata/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
*.png
!*_golden.png

BIN
graph/layout/testdata/line_golden.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

BIN
graph/layout/testdata/sheet_golden.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
graph/layout/testdata/square_golden.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
graph/layout/testdata/tube_golden.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

BIN
graph/layout/testdata/wp_page_golden.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

@@ -97,8 +97,8 @@ func (q *Plane) Reset() (err error) {
defer func() {
switch r := recover(); r {
case nil:
case volumeTooBig:
err = volumeTooBig
case planeTooBig:
err = planeTooBig
default:
panic(r)
}