graph/formats: add packages for serialising graphs to js rendering engines

This commit is contained in:
Dan Kortschak
2018-10-20 06:39:27 +10:30
committed by GitHub
parent 4c3d820680
commit f0982070f5
11 changed files with 51954 additions and 0 deletions

View File

@@ -0,0 +1,310 @@
// Copyright ©2018 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 cytoscapejs implements marshaling and unmarshaling of Cytoscape.js JSON documents.
//
// See http://js.cytoscape.org/ for Cytoscape.js documentation.
package cytoscapejs // import "gonum.org/v1/gonum/graph/formats/cytoscapejs"
import (
"encoding/json"
"errors"
"fmt"
)
// GraphElem is a Cytoscape.js graph with mixed graph elements.
type GraphElem struct {
Elements []Element `json:"elements"`
Layout interface{} `json:"layout,omitempty"`
Style []interface{} `json:"style,omitempty"`
}
// Element is a mixed graph element.
type Element struct {
Group string `json:"group,omitempty"`
Data ElemData `json:"data"`
Position Position `json:"position,omitempty"`
RenderedPosition Position `json:"renderedPosition,omitempty"`
Selected bool `json:"selected,omitempty"`
Selectable bool `json:"selectable,omitempty"`
Locked bool `json:"locked,omitempty"`
Grabbable bool `json:"grabbable,omitempty"`
Classes string `json:"classes,omitempty"`
Scratch interface{} `json:"scratch,omitempty"`
}
// ElemType describes an Element type.
type ElemType int
const (
InvalidElement ElemType = iota - 1
NodeElement
EdgeElement
)
// Type returns the element type of the receiver. It returns an error if the Element Group
// is invalid or does not match the Element Data, or if the Elelement Data is an incomplete
// edge.
func (e Element) Type() (ElemType, error) {
et := InvalidElement
switch {
case e.Data.Source == "" && e.Data.Target == "":
et = NodeElement
case e.Data.Source != "" && e.Data.Target != "":
et = EdgeElement
default:
return et, errors.New("cytoscapejs: invalid element: incomplete edge")
}
switch {
case e.Group == "":
return et, nil
case e.Group == "node" && et == NodeElement:
return NodeElement, nil
case e.Group == "edge" && et == EdgeElement:
return NodeElement, nil
default:
return InvalidElement, errors.New("cytoscapejs: invalid element: mismatched group")
}
}
// ElemData is a graph element's data container.
type ElemData struct {
ID string
Source string
Target string
Parent string
Attributes map[string]interface{}
}
var (
_ json.Marshaler = (*ElemData)(nil)
_ json.Unmarshaler = (*ElemData)(nil)
)
// MarshalJSON implements the json.Marshaler interface.
func (e *ElemData) MarshalJSON() ([]byte, error) {
if e.Attributes == nil {
type edge struct {
ID string `json:"id"`
Source string `json:"source"`
Target string `json:"target"`
Parent string `json:"parent,omitempty"`
}
return json.Marshal(edge{ID: e.ID, Source: e.Source, Target: e.Target, Parent: e.Parent})
}
e.Attributes["id"] = e.ID
if e.Source != "" {
e.Attributes["source"] = e.Source
}
if e.Target != "" {
e.Attributes["target"] = e.Target
}
if e.Parent != "" {
e.Attributes["parent"] = e.Parent
}
b, err := json.Marshal(e.Attributes)
delete(e.Attributes, "id")
if e.Source != "" {
delete(e.Attributes, "source")
}
if e.Target != "" {
delete(e.Attributes, "target")
}
if e.Parent != "" {
delete(e.Attributes, "parent")
}
return b, err
}
// UnmarshalJSON implements the json.Unmarshaler interface.
func (e *ElemData) UnmarshalJSON(data []byte) error {
var attrs map[string]interface{}
err := json.Unmarshal(data, &attrs)
if err != nil {
return err
}
id, ok := attrs["id"]
if !ok {
return errors.New("cytoscapejs: no ID")
}
e.ID = fmt.Sprint(id)
source, ok := attrs["source"]
if ok {
e.Source = fmt.Sprint(source)
}
target, ok := attrs["target"]
if ok {
e.Target = fmt.Sprint(target)
}
p, ok := attrs["parent"]
if ok {
e.Parent = fmt.Sprint(p)
}
delete(attrs, "id")
delete(attrs, "source")
delete(attrs, "target")
delete(attrs, "parent")
if len(attrs) != 0 {
e.Attributes = attrs
}
return nil
}
// GraphNodeEdge is a Cytoscape.js graph with separated nodes and edges.
type GraphNodeEdge struct {
Elements Elements `json:"elements"`
Layout interface{} `json:"layout,omitempty"`
Style []interface{} `json:"style,omitempty"`
}
// Elements contains the nodes and edges of a GraphNodeEdge.
type Elements struct {
Nodes []Node `json:"nodes"`
Edges []Edge `json:"edges"`
}
// Node is a Cytoscape.js node.
type Node struct {
Data NodeData `json:"data"`
Position Position `json:"position,omitempty"`
RenderedPosition Position `json:"renderedPosition,omitempty"`
Selected bool `json:"selected,omitempty"`
Selectable bool `json:"selectable,omitempty"`
Locked bool `json:"locked,omitempty"`
Grabbable bool `json:"grabbable,omitempty"`
Classes string `json:"classes,omitempty"`
Scratch interface{} `json:"scratch,omitempty"`
}
// NodeData is a graph node's data container.
type NodeData struct {
ID string
Parent string
Attributes map[string]interface{}
}
var (
_ json.Marshaler = (*NodeData)(nil)
_ json.Unmarshaler = (*NodeData)(nil)
)
// MarshalJSON implements the json.Marshaler interface.
func (n *NodeData) MarshalJSON() ([]byte, error) {
if n.Attributes == nil {
type node struct {
ID string `json:"id"`
Parent string `json:"parent,omitempty"`
}
return json.Marshal(node{ID: n.ID, Parent: n.Parent})
}
n.Attributes["id"] = n.ID
n.Attributes["parent"] = n.Parent
b, err := json.Marshal(n.Attributes)
delete(n.Attributes, "id")
delete(n.Attributes, "parent")
return b, err
}
// UnmarshalJSON implements the json.Unmarshaler interface.
func (n *NodeData) UnmarshalJSON(data []byte) error {
var attrs map[string]interface{}
err := json.Unmarshal(data, &attrs)
if err != nil {
return err
}
id, ok := attrs["id"]
if !ok {
return errors.New("cytoscapejs: no ID")
}
n.ID = fmt.Sprint(id)
delete(attrs, "id")
p, ok := attrs["parent"]
if ok {
n.Parent = fmt.Sprint(p)
}
delete(attrs, "parent")
if len(attrs) != 0 {
n.Attributes = attrs
}
return nil
}
// Edge is a Cytoscape.js edge.
type Edge struct {
Data EdgeData `json:"data"`
Selected bool `json:"selected,omitempty"`
Selectable bool `json:"selectable,omitempty"`
Classes string `json:"classes,omitempty"`
Scratch interface{} `json:"scratch,omitempty"`
}
// EdgeData is a graph edge's data container.
type EdgeData struct {
ID string
Source string
Target string
Attributes map[string]interface{}
}
var (
_ json.Marshaler = (*EdgeData)(nil)
_ json.Unmarshaler = (*EdgeData)(nil)
)
// MarshalJSON implements the json.Marshaler interface.
func (e *EdgeData) MarshalJSON() ([]byte, error) {
if e.Attributes == nil {
type edge struct {
ID string `json:"id"`
Source string `json:"source"`
Target string `json:"target"`
}
return json.Marshal(edge{ID: e.ID, Source: e.Source, Target: e.Target})
}
e.Attributes["id"] = e.ID
e.Attributes["source"] = e.Source
e.Attributes["target"] = e.Target
b, err := json.Marshal(e.Attributes)
delete(e.Attributes, "id")
delete(e.Attributes, "source")
delete(e.Attributes, "target")
return b, err
}
// UnmarshalJSON implements the json.Unmarshaler interface.
func (e *EdgeData) UnmarshalJSON(data []byte) error {
var attrs map[string]interface{}
err := json.Unmarshal(data, &attrs)
if err != nil {
return err
}
id, ok := attrs["id"]
if !ok {
return errors.New("cytoscapejs: no ID")
}
source, ok := attrs["source"]
if !ok {
return errors.New("cytoscapejs: no source")
}
target, ok := attrs["target"]
if !ok {
return errors.New("cytoscapejs: no target")
}
e.ID = fmt.Sprint(id)
e.Source = fmt.Sprint(source)
e.Target = fmt.Sprint(target)
delete(attrs, "id")
delete(attrs, "source")
delete(attrs, "target")
if len(attrs) != 0 {
e.Attributes = attrs
}
return nil
}
// Position is a node position.
type Position struct {
X float64 `json:"x"`
Y float64 `json:"y"`
}

View File

@@ -0,0 +1,371 @@
// Copyright ©2018 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 cytoscapejs
import (
"encoding/json"
"io/ioutil"
"path/filepath"
"reflect"
"testing"
)
var cytoscapejsElementsTests = []struct {
path string
wantNodes int
wantEdges int
wantGraph []Element
wantAttributes []string
}{
{
path: "edge-type.json",
wantNodes: 10,
wantEdges: 10,
wantGraph: []Element{
{Data: ElemData{ID: "n01", Attributes: map[string]interface{}{"type": "bezier"}}},
{Data: ElemData{ID: "n02"}},
{Data: ElemData{ID: "e01", Source: "n01", Target: "n02"}, Classes: "bezier"},
{Data: ElemData{ID: "e02", Source: "n01", Target: "n02"}, Classes: "bezier"},
{Data: ElemData{ID: "e03", Source: "n02", Target: "n01"}, Classes: "bezier"},
{Data: ElemData{ID: "n03", Attributes: map[string]interface{}{"type": "unbundled-bezier"}}},
{Data: ElemData{ID: "n04"}},
{Data: ElemData{ID: "e04", Source: "n03", Target: "n04"}, Classes: "unbundled-bezier"},
{Data: ElemData{ID: "n05", Attributes: map[string]interface{}{"type": "unbundled-bezier(multiple)"}}},
{Data: ElemData{ID: "n06"}},
{Data: ElemData{ID: "e05", Source: "n05", Target: "n06", Parent: ""}, Classes: "multi-unbundled-bezier"},
{Data: ElemData{ID: "n07", Attributes: map[string]interface{}{"type": "haystack"}}},
{Data: ElemData{ID: "n08"}},
{Data: ElemData{ID: "e06", Source: "n08", Target: "n07"}, Classes: "haystack"},
{Data: ElemData{ID: "e07", Source: "n08", Target: "n07"}, Classes: "haystack"},
{Data: ElemData{ID: "e08", Source: "n08", Target: "n07"}, Classes: "haystack"},
{Data: ElemData{ID: "e09", Source: "n08", Target: "n07"}, Classes: "haystack"},
{Data: ElemData{ID: "n09", Attributes: map[string]interface{}{"type": "segments"}}},
{Data: ElemData{ID: "n10"}},
{Data: ElemData{ID: "e10", Source: "n09", Target: "n10"}, Classes: "segments"},
},
},
}
func TestUnmarshalElements(t *testing.T) {
for _, test := range cytoscapejsElementsTests {
data, err := ioutil.ReadFile(filepath.Join("testdata", test.path))
if err != nil {
t.Errorf("failed to read %q: %v", test.path, err)
continue
}
var got []Element
err = json.Unmarshal(data, &got)
if err != nil {
t.Errorf("failed to unmarshal %q: %v", test.path, err)
continue
}
var gotNodes, gotEdges int
for _, e := range got {
typ, err := e.Type()
if err != nil {
t.Errorf("unexpected error finding element type for %+v: %v", e, err)
}
switch typ {
case NodeElement:
gotNodes++
case EdgeElement:
gotEdges++
}
}
if gotNodes != test.wantNodes {
t.Errorf("unexpected result for order of %q: got:%d want:%d", test.path, gotNodes, test.wantNodes)
}
if gotEdges != test.wantEdges {
t.Errorf("unexpected result for size of %q: got:%d want:%d", test.path, gotEdges, test.wantEdges)
}
if test.wantGraph != nil && !reflect.DeepEqual(got, test.wantGraph) {
t.Errorf("unexpected result for %q:\ngot:\n%#v\nwant:\n%#v", test.path, got, test.wantGraph)
}
}
}
func TestMarshalElements(t *testing.T) {
for _, test := range cytoscapejsElementsTests {
data, err := ioutil.ReadFile(filepath.Join("testdata", test.path))
if err != nil {
t.Errorf("failed to read %q: %v", test.path, err)
continue
}
var want []Element
err = json.Unmarshal(data, &want)
if err != nil {
t.Errorf("failed to unmarshal %q: %v", test.path, err)
continue
}
marshaled, err := json.Marshal(want)
if err != nil {
t.Errorf("failed to unmarshal %q: %v", test.path, err)
continue
}
var got []Element
err = json.Unmarshal(marshaled, &got)
if err != nil {
t.Errorf("failed to unmarshal %q: %v", test.path, err)
continue
}
if !reflect.DeepEqual(got, want) {
t.Errorf("unexpected result for %q:\ngot:\n%#v\nwant:\n%#v", test.path, got, want)
}
}
}
var cytoscapejsNodeEdgeTests = []struct {
path string
wantNodes int
wantEdges int
wantGraph *Elements
firstNode Node
firstEdge Edge
wantNodeAttributes map[string]bool
wantEdgeAttributes map[string]bool
}{
{
path: "cola-compound.json",
wantNodes: 9,
wantEdges: 7,
wantGraph: &Elements{
Nodes: []Node{
{Data: NodeData{ID: "compound-1", Parent: ""}},
{Data: NodeData{ID: "compound-2", Parent: ""}},
{Data: NodeData{ID: "compound-3", Parent: ""}},
{Data: NodeData{ID: "b", Parent: "compound-1"}},
{Data: NodeData{ID: "c", Parent: "compound-1"}},
{Data: NodeData{ID: "a", Parent: "compound-2"}},
{Data: NodeData{ID: "d", Parent: "compound-3"}},
{Data: NodeData{ID: "e", Parent: "compound-3"}},
{Data: NodeData{ID: "f", Parent: ""}},
},
Edges: []Edge{
{Data: EdgeData{ID: "ab", Source: "a", Target: "b"}},
{Data: EdgeData{ID: "bc", Source: "b", Target: "c"}},
{Data: EdgeData{ID: "ac", Source: "a", Target: "c"}},
{Data: EdgeData{ID: "cd", Source: "c", Target: "d"}},
{Data: EdgeData{ID: "de", Source: "d", Target: "e"}},
{Data: EdgeData{ID: "df", Source: "d", Target: "f"}},
{Data: EdgeData{ID: "af", Source: "a", Target: "f"}},
},
},
},
{
path: "tokyo-railways.json",
wantNodes: 943,
wantEdges: 860,
firstNode: Node{
Data: NodeData{
ID: "8220",
Attributes: map[string]interface{}{
"station_name": "京成高砂",
"close_ymd": "",
"lon": 139.866875,
"post": "",
"e_status": 0.0,
"SUID": 8220.0,
"station_g_cd": 2300110.0,
"add": "東京都葛飾区高砂五丁目28-1",
"line_cd": 99340.0,
"selected": false,
"open_ymd": "",
"name": "9934001",
"pref_name": "東京都",
"shared_name": "9934001",
"lat": 35.750932,
"x": 1398668.75,
"y": -357509.32,
},
},
Position: Position{
X: 1398668.75,
Y: -357509.32,
},
},
firstEdge: Edge{
Data: EdgeData{
ID: "18417",
Source: "8220",
Target: "8221",
Attributes: map[string]interface{}{
"line_name_k": "ホクソウテツドウホクソウセン",
"is_bullet": false,
"lon": 140.03784499075186,
"company_name_k": "ホクソウテツドウ",
"zoom": 11.0,
"SUID": 18417.0,
"company_type": 0.0,
"company_name_h": "北総鉄道株式会社",
"interaction": "99340",
"shared_interaction": "99340",
"company_url": "http://www.hokuso-railway.co.jp/",
"line_name": "北総鉄道北総線",
"selected": false,
"company_name": "北総鉄道",
"company_cd": 152.0,
"name": "9934001 (99340) 9934002",
"rr_cd": 99.0,
"company_name_r": "北総鉄道",
"e_status_x": 0.0,
"shared_name": "9934001 (99340) 9934002",
"lat": 35.78346285846615,
"e_status_y": 0.0,
"line_name_h": "北総鉄道北総線",
},
},
},
wantNodeAttributes: map[string]bool{
"station_name": true,
"close_ymd": true,
"lon": true,
"post": true,
"e_status": true,
"SUID": true,
"station_g_cd": true,
"add": true,
"line_cd": true,
"selected": true,
"open_ymd": true,
"name": true,
"pref_name": true,
"shared_name": true,
"lat": true,
"x": true,
"y": true,
},
wantEdgeAttributes: map[string]bool{
"line_name_k": true,
"is_bullet": true,
"lon": true,
"company_name_k": true,
"zoom": true,
"SUID": true,
"company_type": true,
"company_name_h": true,
"interaction": true,
"shared_interaction": true,
"company_url": true,
"line_name": true,
"selected": true,
"company_name": true,
"company_cd": true,
"name": true,
"rr_cd": true,
"company_name_r": true,
"e_status_x": true,
"shared_name": true,
"lat": true,
"e_status_y": true,
"line_name_h": true,
},
},
}
func TestUnmarshalNodeEdge(t *testing.T) {
for _, test := range cytoscapejsNodeEdgeTests {
data, err := ioutil.ReadFile(filepath.Join("testdata", test.path))
if err != nil {
t.Errorf("failed to read %q: %v", test.path, err)
continue
}
var got Elements
err = json.Unmarshal(data, &got)
if err != nil {
t.Errorf("failed to unmarshal %q: %v", test.path, err)
continue
}
if len(got.Nodes) != test.wantNodes {
t.Errorf("unexpected result for order of %q: got:%d want:%d", test.path, len(got.Nodes), test.wantNodes)
}
if len(got.Edges) != test.wantEdges {
t.Errorf("unexpected result for size of %q: got:%d want:%d", test.path, len(got.Edges), test.wantEdges)
}
if test.wantGraph != nil {
if !reflect.DeepEqual(&got, test.wantGraph) {
t.Errorf("unexpected result for %q:\ngot:\n%#v\nwant:\n%#v", test.path, got.Nodes, test.wantGraph.Nodes)
}
} else {
if !reflect.DeepEqual(got.Nodes[0], test.firstNode) {
t.Errorf("unexpected result for %q:\ngot:\n%#v\nwant:\n%#v", test.path, got.Nodes[0], test.firstNode)
}
if !reflect.DeepEqual(got.Edges[0], test.firstEdge) {
t.Errorf("unexpected result for %q:\ngot:\n%v\nwant:\n%#v", test.path, got.Edges[0].Data.Source, test.firstEdge.Data.Source)
}
}
if test.wantNodeAttributes != nil {
var paths []string
for _, n := range got.Nodes {
paths = attrPaths(paths, "", n.Data.Attributes)
}
gotAttrs := make(map[string]bool)
for _, p := range paths {
gotAttrs[p] = true
}
if !reflect.DeepEqual(gotAttrs, test.wantNodeAttributes) {
t.Errorf("unexpected result for %q:\ngot:\n%#v\nwant:\n%#v", test.path, gotAttrs, test.wantNodeAttributes)
}
}
if test.wantEdgeAttributes != nil {
var paths []string
for _, e := range got.Edges {
paths = attrPaths(paths, "", e.Data.Attributes)
}
gotAttrs := make(map[string]bool)
for _, p := range paths {
gotAttrs[p] = true
}
if !reflect.DeepEqual(gotAttrs, test.wantEdgeAttributes) {
t.Errorf("unexpected result for %q:\ngot:\n%#v\nwant:\n%#v", test.path, gotAttrs, test.wantEdgeAttributes)
}
}
}
}
func TestMarshalNodeEdge(t *testing.T) {
for _, test := range cytoscapejsNodeEdgeTests {
data, err := ioutil.ReadFile(filepath.Join("testdata", test.path))
if err != nil {
t.Errorf("failed to read %q: %v", test.path, err)
continue
}
var want Elements
err = json.Unmarshal(data, &want)
if err != nil {
t.Errorf("failed to unmarshal %q: %v", test.path, err)
continue
}
marshaled, err := json.Marshal(want)
if err != nil {
t.Errorf("failed to unmarshal %q: %v", test.path, err)
continue
}
var got Elements
err = json.Unmarshal(marshaled, &got)
if err != nil {
t.Errorf("failed to unmarshal %q: %v", test.path, err)
continue
}
if !reflect.DeepEqual(got, want) {
t.Errorf("unexpected result for %q:\ngot:\n%#v\nwant:\n%#v", test.path, got, want)
}
}
}
func attrPaths(dst []string, prefix string, m map[string]interface{}) []string {
for k, v := range m {
path := prefix
if path != "" {
path += "."
}
if v, ok := v.(map[string]interface{}); ok {
dst = attrPaths(dst, path+k, v)
}
dst = append(dst, path+k)
}
return dst
}

View File

@@ -0,0 +1,21 @@
Copyright (c) 2016-2018, The Cytoscape Consortium.
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the “Software”), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,22 @@
{
"nodes": [
{ "data": { "id": "compound-1" } },
{ "data": { "id": "compound-2" } },
{ "data": { "id": "compound-3" } },
{ "data": { "id": "b", "parent": "compound-1" } },
{ "data": { "id": "c", "parent": "compound-1" } },
{ "data": { "id": "a", "parent": "compound-2" } },
{ "data": { "id": "d", "parent": "compound-3" } },
{ "data": { "id": "e", "parent": "compound-3" } },
{ "data": { "id": "f" } }
],
"edges": [
{ "data": { "id": "ab", "source": "a", "target": "b" } },
{ "data": { "id": "bc", "source": "b", "target": "c" } },
{ "data": { "id": "ac", "source": "a", "target": "c" } },
{ "data": { "id": "cd", "source": "c", "target": "d" } },
{ "data": { "id": "de", "source": "d", "target": "e" } },
{ "data": { "id": "df", "source": "d", "target": "f" } },
{ "data": { "id": "af", "source": "a", "target": "f" } }
]
}

View File

@@ -0,0 +1,116 @@
[{
"data": {
"id": "n01",
"type": "bezier"
}
}, {
"data": {
"id": "n02"
}
}, {
"data": {
"id": "e01",
"source": "n01",
"target": "n02"
},
"classes": "bezier"
}, {
"data": {
"id": "e02",
"source": "n01",
"target": "n02"
},
"classes": "bezier"
}, {
"data": {
"id": "e03",
"source": "n02",
"target": "n01"
},
"classes": "bezier"
}, {
"data": {
"id": "n03",
"type": "unbundled-bezier"
}
}, {
"data": {
"id": "n04"
}
}, {
"data": {
"id": "e04",
"source": "n03",
"target": "n04"
},
"classes": "unbundled-bezier"
}, {
"data": {
"id": "n05",
"type": "unbundled-bezier(multiple)"
}
}, {
"data": {
"id": "n06"
}
}, {
"data": {
"id": "e05",
"source": "n05",
"target": "n06"
},
"classes": "multi-unbundled-bezier"
}, {
"data": {
"id": "n07",
"type": "haystack"
}
}, {
"data": {
"id": "n08"
}
}, {
"data": {
"id": "e06",
"source": "n08",
"target": "n07"
},
"classes": "haystack"
}, {
"data": {
"id": "e07",
"source": "n08",
"target": "n07"
},
"classes": "haystack"
}, {
"data": {
"id": "e08",
"source": "n08",
"target": "n07"
},
"classes": "haystack"
}, {
"data": {
"id": "e09",
"source": "n08",
"target": "n07"
},
"classes": "haystack"
}, {
"data": {
"id": "n09",
"type": "segments"
}
}, {
"data": {
"id": "n10"
}
}, {
"data": {
"id": "e10",
"source": "n09",
"target": "n10"
},
"classes": "segments"
}]

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,128 @@
// Copyright ©2018 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 sigmajs implements marshaling and unmarshaling of Sigma.js JSON documents.
//
// See http://sigmajs.org/ for Sigma.js documentation.
package sigmajs // import "gonum.org/v1/gonum/graph/formats/sigmajs"
import (
"encoding/json"
"errors"
"fmt"
)
// Graph is a Sigma.js graph.
type Graph struct {
Nodes []Node `json:"nodes"`
Edges []Edge `json:"edges"`
}
// Node is a Sigma.js node.
type Node struct {
ID string
Attributes map[string]interface{}
}
var (
_ json.Marshaler = (*Node)(nil)
_ json.Unmarshaler = (*Node)(nil)
)
// MarshalJSON implements the json.Marshaler interface.
func (n *Node) MarshalJSON() ([]byte, error) {
if n.Attributes == nil {
type node struct {
ID string `json:"id"`
}
return json.Marshal(node{ID: n.ID})
}
n.Attributes["id"] = n.ID
b, err := json.Marshal(n.Attributes)
delete(n.Attributes, "id")
return b, err
}
// UnmarshalJSON implements the json.Unmarshaler interface.
func (n *Node) UnmarshalJSON(data []byte) error {
var attrs map[string]interface{}
err := json.Unmarshal(data, &attrs)
if err != nil {
return err
}
id, ok := attrs["id"]
if !ok {
return errors.New("sigmajs: no ID")
}
n.ID = fmt.Sprint(id)
delete(attrs, "id")
if len(attrs) != 0 {
n.Attributes = attrs
}
return nil
}
// Edge is a Sigma.js edge.
type Edge struct {
ID string
Source string
Target string
Attributes map[string]interface{}
}
var (
_ json.Marshaler = (*Edge)(nil)
_ json.Unmarshaler = (*Edge)(nil)
)
// MarshalJSON implements the json.Marshaler interface.
func (e *Edge) MarshalJSON() ([]byte, error) {
if e.Attributes == nil {
type edge struct {
ID string `json:"id"`
Source string `json:"source"`
Target string `json:"target"`
}
return json.Marshal(edge{ID: e.ID, Source: e.Source, Target: e.Target})
}
e.Attributes["id"] = e.ID
e.Attributes["source"] = e.Source
e.Attributes["target"] = e.Target
b, err := json.Marshal(e.Attributes)
delete(e.Attributes, "id")
delete(e.Attributes, "source")
delete(e.Attributes, "target")
return b, err
}
// UnmarshalJSON implements the json.Unmarshaler interface.
func (e *Edge) UnmarshalJSON(data []byte) error {
var attrs map[string]interface{}
err := json.Unmarshal(data, &attrs)
if err != nil {
return err
}
id, ok := attrs["id"]
if !ok {
return errors.New("sigmajs: no ID")
}
source, ok := attrs["source"]
if !ok {
return errors.New("sigmajs: no source")
}
target, ok := attrs["target"]
if !ok {
return errors.New("sigmajs: no target")
}
e.ID = fmt.Sprint(id)
e.Source = fmt.Sprint(source)
e.Target = fmt.Sprint(target)
delete(attrs, "id")
delete(attrs, "source")
delete(attrs, "target")
if len(attrs) != 0 {
e.Attributes = attrs
}
return nil
}

View File

@@ -0,0 +1,334 @@
// Copyright ©2018 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 sigmajs
import (
"encoding/json"
"io/ioutil"
"path/filepath"
"reflect"
"testing"
)
var sigmajsExampleTests = []struct {
path string
wantNodes int
wantEdges int
wantGraph *Graph
wantAttributes map[string]bool
}{
{
path: "geolocalized.json",
wantNodes: 17,
wantEdges: 35,
wantGraph: &Graph{
Nodes: []Node{
{
ID: "n1",
Attributes: map[string]interface{}{
"label": "n1",
"longitude": 2.48,
"latitude": 50.93,
"size": "5.5",
"color": "rgb(1,179,255)",
},
},
{
ID: "n2",
Attributes: map[string]interface{}{
"label": "n2",
"latitude": 50.88,
"longitude": 2.0,
"size": "5.0",
"color": "rgb(1,179,255)",
},
},
{
ID: "n4",
Attributes: map[string]interface{}{
"label": "n4",
"latitude": 49.4,
"longitude": 0.19,
"size": "6.0",
"color": "rgb(1,179,255)",
},
},
{
ID: "n5",
Attributes: map[string]interface{}{
"label": "n5",
"latitude": 48.49,
"longitude": -1.92,
"size": "6.0",
"color": "rgb(1,179,255)",
},
},
{
ID: "n6",
Attributes: map[string]interface{}{
"label": "n6",
"latitude": 48.26,
"longitude": -4.38,
"size": "4.5",
"color": "rgb(1,179,255)",
},
},
{
ID: "n7",
Attributes: map[string]interface{}{
"label": "n7",
"latitude": 47.15,
"longitude": -2.09,
"size": "6.5",
"color": "rgb(1,179,255)",
},
},
{
ID: "n8",
Attributes: map[string]interface{}{
"label": "n8",
"latitude": 46.02,
"longitude": -1.04,
"size": "6.5",
"color": "rgb(1,179,255)",
},
},
{
ID: "n9",
Attributes: map[string]interface{}{
"label": "n9",
"latitude": 43.22,
"longitude": -1.85,
"size": "5.0",
"color": "rgb(1,179,255)",
},
},
{
ID: "n10",
Attributes: map[string]interface{}{
"label": "n10",
"latitude": 42.38,
"longitude": 3.18,
"color": "rgb(1,179,255)",
"size": "4.0",
},
},
{
ID: "n11",
Attributes: map[string]interface{}{
"label": "n11",
"latitude": 43.47,
"longitude": 4.04,
"size": "5.5",
"color": "rgb(1,179,255)",
},
},
{
ID: "n12",
Attributes: map[string]interface{}{
"label": "n12",
"latitude": 42.9,
"longitude": 6.59,
"size": "5.0",
"color": "rgb(1,179,255)",
},
},
{
ID: "n13",
Attributes: map[string]interface{}{
"label": "n13",
"latitude": 43.62,
"longitude": 7.66,
"size": "6.0",
"color": "rgb(1,179,255)",
},
},
{
ID: "n14",
Attributes: map[string]interface{}{
"label": "n14",
"latitude": 46.05,
"longitude": 6.19,
"size": "6.5",
"color": "rgb(1,179,255)",
},
},
{
ID: "n15",
Attributes: map[string]interface{}{
"label": "n15",
"latitude": 47.43,
"longitude": 7.65,
"size": "6.0",
"color": "rgb(1,179,255)",
},
},
{
ID: "n16",
Attributes: map[string]interface{}{
"label": "n16",
"latitude": 48.9,
"longitude": 8.32,
"size": "5.5",
"color": "rgb(1,179,255)",
},
},
{
ID: "n17",
Attributes: map[string]interface{}{
"label": "n17",
"latitude": 49.83,
"longitude": 4.94,
"size": "6.5",
"color": "rgb(1,179,255)",
},
},
{
ID: "Paris",
Attributes: map[string]interface{}{
"label": "Paris",
"latitude": 48.72,
"longitude": 2.46,
"size": "9.0",
"color": "rgb(1,179,255)",
},
},
},
Edges: []Edge{
{ID: "8", Source: "n1", Target: "Paris"},
{ID: "7", Source: "n2", Target: "n4"},
{ID: "28", Source: "n4", Target: "n1"},
{ID: "30", Source: "n4", Target: "n7"},
{ID: "26", Source: "n5", Target: "n1"},
{ID: "27", Source: "n5", Target: "n2"},
{ID: "0", Source: "n6", Target: "n5"},
{ID: "29", Source: "n7", Target: "n5"},
{ID: "1", Source: "n7", Target: "n8"},
{ID: "17", Source: "n7", Target: "Paris"},
{ID: "10", Source: "n8", Target: "n13"},
{ID: "18", Source: "n8", Target: "Paris"},
{ID: "15", Source: "n9", Target: "n8"},
{ID: "34", Source: "n10", Target: "n9"},
{ID: "31", Source: "n10", Target: "n11"},
{ID: "11", Source: "n11", Target: "n13"},
{ID: "13", Source: "n11", Target: "n14"},
{ID: "32", Source: "n12", Target: "n10"},
{ID: "12", Source: "n12", Target: "n11"},
{ID: "23", Source: "n12", Target: "n13"},
{ID: "33", Source: "n13", Target: "n10"},
{ID: "25", Source: "n13", Target: "n14"},
{ID: "14", Source: "n14", Target: "n9"},
{ID: "5", Source: "n14", Target: "n17"},
{ID: "19", Source: "n14", Target: "Paris"},
{ID: "6", Source: "n15", Target: "n8"},
{ID: "22", Source: "n15", Target: "n16"},
{ID: "20", Source: "n15", Target: "Paris"},
{ID: "4", Source: "n16", Target: "n15"},
{ID: "24", Source: "n16", Target: "Paris"},
{ID: "9", Source: "n17", Target: "n7"},
{ID: "21", Source: "n17", Target: "n17"},
{ID: "2", Source: "Paris", Target: "n4"},
{ID: "3", Source: "Paris", Target: "n17"},
{ID: "16", Source: "Paris", Target: "Paris"},
},
},
},
{
path: "arctic.json",
wantNodes: 1715,
wantEdges: 6676,
wantAttributes: map[string]bool{
"label": true,
"x": true,
"y": true,
"color": true,
"size": true,
"attributes": true,
"attributes.nodedef": true,
},
},
}
func TestUnmarshal(t *testing.T) {
for _, test := range sigmajsExampleTests {
data, err := ioutil.ReadFile(filepath.Join("testdata", test.path))
if err != nil {
t.Errorf("failed to read %q: %v", test.path, err)
continue
}
var got Graph
err = json.Unmarshal(data, &got)
if err != nil {
t.Errorf("failed to unmarshal %q: %v", test.path, err)
continue
}
if len(got.Nodes) != test.wantNodes {
t.Errorf("unexpected result for order of %q: got:%d want:%d", test.path, len(got.Nodes), test.wantNodes)
}
if len(got.Edges) != test.wantEdges {
t.Errorf("unexpected result for size of %q: got:%d want:%d", test.path, len(got.Edges), test.wantEdges)
}
if test.wantGraph != nil && !reflect.DeepEqual(&got, test.wantGraph) {
t.Errorf("unexpected result for %q:\ngot:\n%#v\nwant:\n%#v", test.path, got, test.wantGraph)
}
if test.wantAttributes != nil {
var paths []string
for _, n := range got.Nodes {
paths = attrPaths(paths, "", n.Attributes)
}
gotAttrs := make(map[string]bool)
for _, p := range paths {
gotAttrs[p] = true
}
if !reflect.DeepEqual(gotAttrs, test.wantAttributes) {
t.Errorf("unexpected result for %q:\ngot:\n%#v\nwant:\n%#v", test.path, gotAttrs, test.wantAttributes)
}
}
}
}
func attrPaths(dst []string, prefix string, m map[string]interface{}) []string {
for k, v := range m {
path := prefix
if path != "" {
path += "."
}
if v, ok := v.(map[string]interface{}); ok {
dst = attrPaths(dst, path+k, v)
}
dst = append(dst, path+k)
}
return dst
}
func TestMarshal(t *testing.T) {
for _, test := range sigmajsExampleTests {
data, err := ioutil.ReadFile(filepath.Join("testdata", test.path))
if err != nil {
t.Errorf("failed to read %q: %v", test.path, err)
continue
}
var want Graph
err = json.Unmarshal(data, &want)
if err != nil {
t.Errorf("failed to unmarshal %q: %v", test.path, err)
continue
}
marshaled, err := json.Marshal(want)
if err != nil {
t.Errorf("failed to unmarshal %q: %v", test.path, err)
continue
}
var got Graph
err = json.Unmarshal(marshaled, &got)
if err != nil {
t.Errorf("failed to unmarshal %q: %v", test.path, err)
continue
}
if !reflect.DeepEqual(got, want) {
t.Errorf("unexpected result for %q:\ngot:\n%#v\nwant:\n%#v", test.path, got, want)
}
}
}

View File

@@ -0,0 +1,12 @@
Copyright (C) 2013-2014, Alexis Jacomy, http://sigmajs.org
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
IN THE SOFTWARE.

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,317 @@
{
"nodes":[
{
"id":"n1",
"label":"n1",
"latitude":50.93,
"longitude":2.48,
"size":"5.5",
"color":"rgb(1,179,255)"
},
{
"id":"n2",
"label":"n2",
"latitude":50.88,
"longitude":2.0,
"size":"5.0",
"color":"rgb(1,179,255)"
},
{
"id":"n4",
"label":"n4",
"latitude":49.4,
"longitude":0.19,
"size":"6.0",
"color":"rgb(1,179,255)"
},
{
"id":"n5",
"label":"n5",
"latitude":48.49,
"longitude":-1.92,
"size":"6.0",
"color":"rgb(1,179,255)"
},
{
"id":"n6",
"label":"n6",
"latitude":48.26,
"longitude":-4.38,
"size":"4.5",
"color":"rgb(1,179,255)"
},
{
"id":"n7",
"label":"n7",
"latitude":47.15,
"longitude":-2.09,
"size":"6.5",
"color":"rgb(1,179,255)"
},
{
"id":"n8",
"label":"n8",
"latitude":46.02,
"longitude":-1.04,
"size":"6.5",
"color":"rgb(1,179,255)"
},
{
"id":"n9",
"label":"n9",
"latitude":43.22,
"longitude":-1.85,
"size":"5.0",
"color":"rgb(1,179,255)"
},
{
"id":"n10",
"label":"n10",
"latitude":42.38,
"longitude":3.18,
"size":"4.0",
"color":"rgb(1,179,255)"
},
{
"id":"n11",
"label":"n11",
"latitude":43.47,
"longitude":4.04,
"size":"5.5",
"color":"rgb(1,179,255)"
},
{
"id":"n12",
"label":"n12",
"latitude":42.9,
"longitude":6.59,
"size":"5.0",
"color":"rgb(1,179,255)"
},
{
"id":"n13",
"label":"n13",
"latitude":43.62,
"longitude":7.66,
"size":"6.0",
"color":"rgb(1,179,255)"
},
{
"id":"n14",
"label":"n14",
"latitude":46.05,
"longitude":6.19,
"size":"6.5",
"color":"rgb(1,179,255)"
},
{
"id":"n15",
"label":"n15",
"latitude":47.43,
"longitude":7.65,
"size":"6.0",
"color":"rgb(1,179,255)"
},
{
"id":"n16",
"label":"n16",
"latitude":48.9,
"longitude":8.32,
"size":"5.5",
"color":"rgb(1,179,255)"
},
{
"id":"n17",
"label":"n17",
"latitude":49.83,
"longitude":4.94,
"size":"6.5",
"color":"rgb(1,179,255)"
},
{
"id":"Paris",
"label":"Paris",
"latitude":48.72,
"longitude":2.46,
"size":"9.0",
"color":"rgb(1,179,255)"
}
],
"edges":[
{
"id":"8",
"source":"n1",
"target":"Paris"
},
{
"id":"7",
"source":"n2",
"target":"n4"
},
{
"id":"28",
"source":"n4",
"target":"n1"
},
{
"id":"30",
"source":"n4",
"target":"n7"
},
{
"id":"26",
"source":"n5",
"target":"n1"
},
{
"id":"27",
"source":"n5",
"target":"n2"
},
{
"id":"0",
"source":"n6",
"target":"n5"
},
{
"id":"29",
"source":"n7",
"target":"n5"
},
{
"id":"1",
"source":"n7",
"target":"n8"
},
{
"id":"17",
"source":"n7",
"target":"Paris"
},
{
"id":"10",
"source":"n8",
"target":"n13"
},
{
"id":"18",
"source":"n8",
"target":"Paris"
},
{
"id":"15",
"source":"n9",
"target":"n8"
},
{
"id":"34",
"source":"n10",
"target":"n9"
},
{
"id":"31",
"source":"n10",
"target":"n11"
},
{
"id":"11",
"source":"n11",
"target":"n13"
},
{
"id":"13",
"source":"n11",
"target":"n14"
},
{
"id":"32",
"source":"n12",
"target":"n10"
},
{
"id":"12",
"source":"n12",
"target":"n11"
},
{
"id":"23",
"source":"n12",
"target":"n13"
},
{
"id":"33",
"source":"n13",
"target":"n10"
},
{
"id":"25",
"source":"n13",
"target":"n14"
},
{
"id":"14",
"source":"n14",
"target":"n9"
},
{
"id":"5",
"source":"n14",
"target":"n17"
},
{
"id":"19",
"source":"n14",
"target":"Paris"
},
{
"id":"6",
"source":"n15",
"target":"n8"
},
{
"id":"22",
"source":"n15",
"target":"n16"
},
{
"id":"20",
"source":"n15",
"target":"Paris"
},
{
"id":"4",
"source":"n16",
"target":"n15"
},
{
"id":"24",
"source":"n16",
"target":"Paris"
},
{
"id":"9",
"source":"n17",
"target":"n7"
},
{
"id":"21",
"source":"n17",
"target":"n17"
},
{
"id":"2",
"source":"Paris",
"target":"n4"
},
{
"id":"3",
"source":"Paris",
"target":"n17"
},
{
"id":"16",
"source":"Paris",
"target":"Paris"
}
]
}