GraphStream
This commit is contained in:
parent
acf8004497
commit
8d6d91e562
2
Jenkinsfile
vendored
2
Jenkinsfile
vendored
@ -5,7 +5,7 @@ pipeline {
|
||||
VERSION = VersionNumber([
|
||||
versionNumberString:
|
||||
'${BUILDS_ALL_TIME}',
|
||||
versionPrefix: '1.0.',
|
||||
versionPrefix: '1.1.',
|
||||
worstResultForIncrement: 'SUCCESS'
|
||||
])
|
||||
}
|
||||
|
@ -1,10 +0,0 @@
|
||||
export enum GraphLayouts {
|
||||
neato = 'neato',
|
||||
dot = 'dot',
|
||||
circo = 'circo',
|
||||
fdp = 'fdp',
|
||||
sfdp = 'sfdp',
|
||||
osage = 'osage',
|
||||
twopi = 'twopi',
|
||||
patchwork = 'patchwork'
|
||||
}
|
10
lib/Helper.ts
Normal file
10
lib/Helper.ts
Normal file
@ -0,0 +1,10 @@
|
||||
function validate_name (name: string): string {
|
||||
const new_name = name
|
||||
.replace (/[^a-z0-9]/giu, '')
|
||||
.replace (/^[0-9]+/iu, '');
|
||||
if (new_name === '')
|
||||
throw new Error (`invalid node name ${name}`);
|
||||
return new_name;
|
||||
}
|
||||
|
||||
export { validate_name };
|
@ -1,33 +0,0 @@
|
||||
enum EdgeStyles {
|
||||
default = '',
|
||||
solid = 'solid',
|
||||
dashed = 'dashed',
|
||||
dotted='dotted',
|
||||
bold='bold'
|
||||
}
|
||||
|
||||
enum NodeStyles {
|
||||
default = '',
|
||||
solid='solid',
|
||||
dashed='dashed',
|
||||
dotted='dotted',
|
||||
bold='bold',
|
||||
rounded='rounded',
|
||||
diagonals='diagonals',
|
||||
filled='filled',
|
||||
striped='striped',
|
||||
wedged='wedged',
|
||||
invisible='invis'
|
||||
}
|
||||
|
||||
enum GraphStyles {
|
||||
solid = 'solid',
|
||||
dashed = 'dashed',
|
||||
dotted = 'dotted',
|
||||
bold = 'bold',
|
||||
rounded = 'rounded',
|
||||
filled = 'filled',
|
||||
striped = 'striped'
|
||||
}
|
||||
|
||||
export { EdgeStyles, NodeStyles, GraphStyles };
|
@ -1,4 +1,4 @@
|
||||
import { EdgeStyles } from './Styles';
|
||||
import { EdgeStyles } from '../enums/Styles';
|
||||
import { Color } from './Color';
|
||||
|
||||
export class Edge {
|
@ -1,3 +1,5 @@
|
||||
import { validate_name } from '../Helper';
|
||||
|
||||
export class Element {
|
||||
private _name = '';
|
||||
protected parent_name: string;
|
||||
@ -13,11 +15,7 @@ export class Element {
|
||||
}
|
||||
|
||||
public set name (val: string) {
|
||||
const new_name = val.replace (/[^a-z0-9]/giu, '')
|
||||
.replace (/^[0-9]+/iu, '');
|
||||
if (new_name === '')
|
||||
throw new Error (`invalid node name ${val}`);
|
||||
this._name = new_name;
|
||||
this._name = validate_name (val);
|
||||
}
|
||||
|
||||
public get parent (): string {
|
@ -1,9 +1,9 @@
|
||||
import { GraphStyles, NodeStyles } from '../enums/Styles';
|
||||
import { GraphLayouts } from '../enums/GraphLayouts';
|
||||
import { Element } from './Element';
|
||||
import { Edge } from './Edge';
|
||||
import { Node } from './Node';
|
||||
import { GraphStyles, NodeStyles } from './Styles';
|
||||
import { Color } from './Color';
|
||||
import { GraphLayouts } from './GraphLayouts';
|
||||
|
||||
interface NodeOptions {
|
||||
name: string;
|
160
lib/classes/GraphStream.ts
Normal file
160
lib/classes/GraphStream.ts
Normal file
@ -0,0 +1,160 @@
|
||||
/* eslint-disable line-comment-position */
|
||||
/* eslint-disable no-inline-comments */
|
||||
import { Transform } from 'stream';
|
||||
import { GraphStreamJSON } from '../interfaces/GraphStreamJSON';
|
||||
import { GraphStreamCommand } from '../enums/GraphStreamCommand';
|
||||
|
||||
interface Stringable {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
toString(): string;
|
||||
}
|
||||
|
||||
export class GraphStream extends Transform {
|
||||
private _path: string[] = [];
|
||||
private _state = '';
|
||||
private _directional = false;
|
||||
|
||||
public get path (): string {
|
||||
return this._path.join ('_');
|
||||
}
|
||||
|
||||
private get level (): string {
|
||||
return ' '.repeat (this._path.length);
|
||||
}
|
||||
|
||||
private expect_state (instr: GraphStreamCommand): void {
|
||||
const states = [];
|
||||
if ([
|
||||
'cug',
|
||||
'cdg'
|
||||
].includes (instr))
|
||||
states.push ('');
|
||||
|
||||
if ([
|
||||
'eg',
|
||||
'cn',
|
||||
'csg',
|
||||
'ce'
|
||||
].includes (instr))
|
||||
states.push ('en', 'at', 'eg', 'cug', 'cdg', 'csg', 'ce');
|
||||
|
||||
switch (instr) {
|
||||
case 'en':
|
||||
states.push ('cn', 'at');
|
||||
break;
|
||||
case 'at':
|
||||
states.push ('cn', 'cug', 'cdg', 'csg');
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (!states.includes (this._state)) {
|
||||
throw new Error (`invalid state to execute command ${instr}
|
||||
expected: ${states.join (', ')}`);
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line max-len
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention, complexity, max-lines-per-function
|
||||
public _transform (
|
||||
chunk: string,
|
||||
encoding: string,
|
||||
callback: ((error?: Error) => unknown)
|
||||
): void {
|
||||
const instr = JSON.parse (chunk) as GraphStreamJSON;
|
||||
this.expect_state (instr.type);
|
||||
switch (instr.type) {
|
||||
case 'cug': // create unordered graph
|
||||
this._path.push (instr.args[0]);
|
||||
this.push (`graph ${this.path} {\n`);
|
||||
this._directional = false;
|
||||
break;
|
||||
case 'cdg': // create directional graph
|
||||
this._path.push (instr.args[0]);
|
||||
this.push (`digraph ${this.path} {\n`);
|
||||
this._directional = true;
|
||||
break;
|
||||
case 'csg': // create subgraph
|
||||
this.push (
|
||||
`${this.level}subgraph cluster_${this.path}_${instr.args[0]} {\n`
|
||||
);
|
||||
this._path.push (instr.args[0]);
|
||||
break;
|
||||
case 'eg': // end graph
|
||||
this._path.pop ();
|
||||
this.push (`${this.level}}\n\n`);
|
||||
break;
|
||||
case 'cn': // create node
|
||||
this.push (`${this.level}${this.path}_${instr.args[0]}`);
|
||||
break;
|
||||
case 'en': // end node
|
||||
this.push ('\n');
|
||||
break;
|
||||
case 'at': // add attributes
|
||||
if (this._state === 'cn') {
|
||||
this.push (` [${instr.args.join (', ')}]`);
|
||||
}
|
||||
else {
|
||||
this.push (`${this.level}${
|
||||
instr.args.join (`\n${this.level}`)
|
||||
.replace (/"/gu, '')
|
||||
}\n\n`);
|
||||
}
|
||||
break;
|
||||
case 'ce':
|
||||
this.push (`${this.level}${
|
||||
instr.args[0]
|
||||
} -${this._directional ? '>' : '-'} ${instr.args[1]}\n`);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
this._state = instr.type;
|
||||
callback ();
|
||||
}
|
||||
|
||||
public write (
|
||||
instr: GraphStreamJSON
|
||||
): boolean {
|
||||
return super.write (JSON.stringify (instr), 'utf-8');
|
||||
}
|
||||
|
||||
public create_node (name: string): boolean {
|
||||
return this.write ({ type: 'cn', args: [ name ] });
|
||||
}
|
||||
|
||||
public end_node (): boolean {
|
||||
return this.write ({ type: 'en', args: [] });
|
||||
}
|
||||
|
||||
public create_graph (name: string, type: 'u'|'d'|'s' = 's'): boolean {
|
||||
const instr_type = `c${type}g` as GraphStreamCommand;
|
||||
return this.write ({ type: instr_type, args: [ name ] });
|
||||
}
|
||||
|
||||
public end_graph (): boolean {
|
||||
return this.write ({ type: 'eg', args: [] });
|
||||
}
|
||||
|
||||
public attributes (attrs: Record<string, Stringable>): boolean {
|
||||
const solved = [];
|
||||
for (const attr of Object.keys (attrs)) {
|
||||
const val = attrs[attr].toString ();
|
||||
if ((/\n/u).test (val))
|
||||
solved.push (`${attr} = <${val}>`);
|
||||
else
|
||||
solved.push (`${attr} = "${val}"`);
|
||||
}
|
||||
return this.write ({ type: 'at', args: solved });
|
||||
}
|
||||
|
||||
public create_edge (origin: string, target: string): boolean {
|
||||
return this.write ({
|
||||
type: 'ce',
|
||||
args: [
|
||||
origin,
|
||||
target
|
||||
]
|
||||
});
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import { NodeStyles } from '../enums/Styles';
|
||||
import { Element } from './Element';
|
||||
import { NodeStyles } from './Styles';
|
||||
import { Color } from './Color';
|
||||
|
||||
export class Node extends Element {
|
9
lib/enums/GraphLayouts.ts
Normal file
9
lib/enums/GraphLayouts.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export type GraphLayouts =
|
||||
'neato'
|
||||
| 'dot'
|
||||
| 'circo'
|
||||
| 'fdp'
|
||||
| 'sfdp'
|
||||
| 'osage'
|
||||
| 'twopi'
|
||||
| 'patchwork'
|
11
lib/enums/GraphStreamCommand.ts
Normal file
11
lib/enums/GraphStreamCommand.ts
Normal file
@ -0,0 +1,11 @@
|
||||
/* eslint-disable line-comment-position */
|
||||
/* eslint-disable no-inline-comments */
|
||||
export type GraphStreamCommand =
|
||||
'cn'| // create node
|
||||
'en'| // end node
|
||||
'cug'| // create unordered graph
|
||||
'cdg'| // create directional graph
|
||||
'csg'| // create subgraph
|
||||
'eg'| // end graph
|
||||
'at'| // add attributes
|
||||
'ce' // create edge
|
32
lib/enums/Styles.ts
Normal file
32
lib/enums/Styles.ts
Normal file
@ -0,0 +1,32 @@
|
||||
type EdgeStyles =
|
||||
''
|
||||
|'solid'
|
||||
|'dashed'
|
||||
|'dotted'
|
||||
|'bold'
|
||||
|
||||
|
||||
type NodeStyles =
|
||||
''
|
||||
|'solid'
|
||||
|'dashed'
|
||||
|'dotted'
|
||||
|'bold'
|
||||
|'rounded'
|
||||
|'diagonals'
|
||||
|'filled'
|
||||
|'striped'
|
||||
|'wedged'
|
||||
|'invis'
|
||||
|
||||
|
||||
type GraphStyles =
|
||||
'solid'
|
||||
| 'dashed'
|
||||
| 'dotted'
|
||||
| 'bold'
|
||||
| 'rounded'
|
||||
| 'filled'
|
||||
| 'striped'
|
||||
|
||||
export { EdgeStyles, NodeStyles, GraphStyles };
|
20
lib/index.ts
20
lib/index.ts
@ -1,8 +1,12 @@
|
||||
export { Graph } from './Graph';
|
||||
export { Node } from './Node';
|
||||
export { Element } from './Element';
|
||||
export { Edge } from './Edge';
|
||||
export { Color } from './Color';
|
||||
export { EdgeStyles, NodeStyles, GraphStyles } from './Styles';
|
||||
export { GraphLayouts } from './GraphLayouts';
|
||||
export { Graphable } from './Graphable';
|
||||
import { Graph } from './classes/Graph';
|
||||
|
||||
export * from './classes/Color';
|
||||
export * from './classes/Edge';
|
||||
export * from './classes/Element';
|
||||
export { Graph };
|
||||
export * from './classes/GraphStream';
|
||||
export * from './classes/Node';
|
||||
export * from './enums/GraphLayouts';
|
||||
export * from './enums/Styles';
|
||||
export * from './interfaces/Graphable';
|
||||
export default Graph;
|
||||
|
6
lib/interfaces/GraphStreamJSON.ts
Normal file
6
lib/interfaces/GraphStreamJSON.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { GraphStreamCommand } from '../enums/GraphStreamCommand';
|
||||
|
||||
export interface GraphStreamJSON {
|
||||
type: GraphStreamCommand;
|
||||
args: string[];
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
import { Graph } from './Graph';
|
||||
import { Graph } from '../classes/Graph';
|
||||
|
||||
export interface Graphable {
|
||||
to_graph(g: Graph, ...args: unknown[]): unknown;
|
@ -1,11 +1,11 @@
|
||||
import test from 'ava';
|
||||
import { Edge, Color, EdgeStyles } from '../lib';
|
||||
import { Edge, Color } from '../lib';
|
||||
|
||||
test ('serialize', (t) => {
|
||||
const e = new Edge ('foo', 'bar', false);
|
||||
|
||||
e.color = Color.white;
|
||||
e.style = EdgeStyles.dashed;
|
||||
e.style = 'dashed';
|
||||
const serialized = e.toString ();
|
||||
|
||||
t.is (serialized, 'foo -- bar [style="dashed",color="#ffffff"]');
|
||||
@ -15,7 +15,7 @@ test ('serialize directional', (t) => {
|
||||
const e = new Edge ('foo', 'bar', true);
|
||||
|
||||
e.color = Color.white;
|
||||
e.style = EdgeStyles.dashed;
|
||||
e.style = 'dashed';
|
||||
const serialized = e.toString ();
|
||||
|
||||
t.is (serialized, 'foo -> bar [style="dashed",color="#ffffff"]');
|
||||
|
@ -1,5 +1,5 @@
|
||||
import test from 'ava';
|
||||
import { Graph, GraphStyles, Color, NodeStyles, GraphLayouts } from '../lib';
|
||||
import { Graph, Color } from '../lib';
|
||||
|
||||
const result = `digraph foo {
|
||||
subgraph cluster_foo_baz {
|
||||
@ -56,23 +56,23 @@ test ('serialize', (t) => {
|
||||
graph.add_node ('asd');
|
||||
graph.add_node ((n) => {
|
||||
n.name = 'test';
|
||||
n.style = NodeStyles.bold;
|
||||
n.style = 'bold';
|
||||
n.color = Color.gray;
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-shadow
|
||||
graph.add_graph ((g) => {
|
||||
g.style = GraphStyles.dotted;
|
||||
g.style = 'dotted';
|
||||
g.color = Color.gray;
|
||||
g.name = 'nested';
|
||||
|
||||
// eslint-disable-next-line no-shadow, max-nested-callbacks
|
||||
g.add_graph ((g) => {
|
||||
g.style = GraphStyles.dotted;
|
||||
g.style = 'dotted';
|
||||
g.color = Color.gray;
|
||||
});
|
||||
});
|
||||
graph.style = GraphStyles.bold;
|
||||
graph.style = 'bold';
|
||||
graph.color = Color.red;
|
||||
});
|
||||
|
||||
@ -111,11 +111,11 @@ test ('non directional', (t) => {
|
||||
test ('attributes', (t) => {
|
||||
const g = new Graph ('attr');
|
||||
|
||||
g.layout = GraphLayouts.neato;
|
||||
g.layout = 'neato';
|
||||
g.overlap = false;
|
||||
g.splines = true;
|
||||
g.color = Color.black;
|
||||
g.style = GraphStyles.bold;
|
||||
g.style = 'bold';
|
||||
|
||||
t.is (g.toString (), attributes);
|
||||
});
|
||||
|
@ -1,5 +1,5 @@
|
||||
import test from 'ava';
|
||||
import { NodeStyles, Node, Color } from '../lib';
|
||||
import { Node, Color } from '../lib';
|
||||
|
||||
const serialized_simple
|
||||
= 'bar_foo [label="baz", style="dashed", color="#00ff00"]';
|
||||
@ -13,7 +13,7 @@ test ('serialize simple', (t) => {
|
||||
const g = new Node ('foo', 'bar', 'baz');
|
||||
|
||||
g.color = Color.green;
|
||||
g.style = NodeStyles.dashed;
|
||||
g.style = 'dashed';
|
||||
|
||||
const serialized = g.toString ();
|
||||
|
||||
@ -25,7 +25,7 @@ test ('serialize table', (t) => {
|
||||
const g = new Node ('foo', 'bar', 'baz');
|
||||
|
||||
g.color = Color.green;
|
||||
g.style = NodeStyles.invisible;
|
||||
g.style = 'invis';
|
||||
|
||||
g.table_contents = [
|
||||
[
|
||||
|
89
test/Stream.ts
Normal file
89
test/Stream.ts
Normal file
@ -0,0 +1,89 @@
|
||||
import test from 'ava';
|
||||
import { GraphStream, Color } from '../lib/index';
|
||||
|
||||
const simple = `graph foo {
|
||||
}
|
||||
|
||||
`;
|
||||
|
||||
const complex = `digraph foo {
|
||||
subgraph cluster_foo_baz {
|
||||
color = #ff0000
|
||||
style = bold
|
||||
|
||||
subgraph cluster_foo_baz_nested {
|
||||
color = #808080
|
||||
style = dotted
|
||||
|
||||
subgraph cluster_foo_baz_nested_unnamed {
|
||||
color = #808080
|
||||
style = dotted
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
foo_baz_asd [label = "asd"]
|
||||
foo_baz_test [style = "bold", color = "#808080"]
|
||||
}
|
||||
|
||||
foo_baz [label = "baz"]
|
||||
foo_foo [label = "foo"]
|
||||
foo_foo -> foo_baz
|
||||
}
|
||||
|
||||
`;
|
||||
|
||||
test ('stream graph', (t) => new Promise ((resolve) => {
|
||||
let output = '';
|
||||
const stream = (new GraphStream);
|
||||
stream.on ('data', (data) => {
|
||||
output += data;
|
||||
});
|
||||
stream.on ('end', () => {
|
||||
t.is (output, simple);
|
||||
resolve ();
|
||||
});
|
||||
stream.create_graph ('foo', 'u');
|
||||
stream.end_graph ();
|
||||
stream.end ();
|
||||
}));
|
||||
|
||||
// eslint-disable-next-line max-statements
|
||||
test ('complex stream graph', (t) => new Promise ((resolve) => {
|
||||
let output = '';
|
||||
const stream = (new GraphStream);
|
||||
stream.on ('data', (data) => {
|
||||
output += data;
|
||||
});
|
||||
stream.on ('end', () => {
|
||||
t.is (output, complex);
|
||||
t.log (output);
|
||||
resolve ();
|
||||
});
|
||||
stream.create_graph ('foo', 'd');
|
||||
stream.create_graph ('baz');
|
||||
stream.attributes ({ color: Color.red, style: 'bold' });
|
||||
stream.create_graph ('nested');
|
||||
stream.attributes ({ color: Color.gray, style: 'dotted' });
|
||||
stream.create_graph ('unnamed');
|
||||
stream.attributes ({ color: Color.gray, style: 'dotted' });
|
||||
stream.end_graph ();
|
||||
stream.end_graph ();
|
||||
stream.create_node ('asd');
|
||||
stream.attributes ({ label: 'asd' });
|
||||
stream.end_node ();
|
||||
stream.create_node ('test');
|
||||
stream.attributes ({ style: 'bold', color: Color.gray });
|
||||
stream.end_node ();
|
||||
stream.end_graph ();
|
||||
stream.create_node ('baz');
|
||||
stream.attributes ({ label: 'baz' });
|
||||
stream.end_node ();
|
||||
stream.create_node ('foo');
|
||||
stream.attributes ({ label: 'foo' });
|
||||
stream.end_node ();
|
||||
stream.create_edge (`${stream.path}_foo`, `${stream.path}_baz`);
|
||||
stream.end_graph ();
|
||||
stream.end ();
|
||||
}));
|
12
yarn.lock
12
yarn.lock
@ -241,18 +241,18 @@
|
||||
eslint-plugin-import "^2.20.1"
|
||||
|
||||
"@sapphirecode/eslint-config-ts@^1.0.22":
|
||||
version "1.0.38"
|
||||
resolved "https://registry.yarnpkg.com/@sapphirecode/eslint-config-ts/-/eslint-config-ts-1.0.38.tgz#9c2c80be2b6bee6a0c8dc2a9d0504869d46f5882"
|
||||
integrity sha512-sOzBGUmpc12/j5Z2opMTqpU/XztZOdRQy+/+0wLMLATs9PVHW55oprvWLDQoWttH763v2DwU3PNvoXILq1eHGw==
|
||||
version "1.0.40"
|
||||
resolved "https://registry.yarnpkg.com/@sapphirecode/eslint-config-ts/-/eslint-config-ts-1.0.40.tgz#e646383a933ff64d2604bd6ddd2f89e1aa7dfb1a"
|
||||
integrity sha512-h0m5ohVZB8jKubT567gjykpkVN/ybVkbKNWT9q5GhltECVB9g/Y6Jh6E9Fqg1pTz+DE+iHqr7CO9+stVVLwc4w==
|
||||
dependencies:
|
||||
"@sapphirecode/eslint-config-es6" "^1.0.1"
|
||||
"@typescript-eslint/eslint-plugin" "^2.26.0"
|
||||
"@typescript-eslint/parser" "^2.26.0"
|
||||
|
||||
"@sapphirecode/eslint-config@^2.0.1":
|
||||
version "2.0.23"
|
||||
resolved "https://registry.yarnpkg.com/@sapphirecode/eslint-config/-/eslint-config-2.0.23.tgz#1669742219dab81ac83914d8826d88f06492ba2c"
|
||||
integrity sha512-GO5KDZ58z59raCUkX7CzlATkqGETsOts2S/kOoxNhZR6+rdXQqkrA8BI4/degzZiWnKYn3KkQhyYvXv7fXKkFg==
|
||||
version "2.0.24"
|
||||
resolved "https://registry.yarnpkg.com/@sapphirecode/eslint-config/-/eslint-config-2.0.24.tgz#e9a033767ce0ab1242dc3af700abb58b1c68ab65"
|
||||
integrity sha512-Fk5opga9EEMT4mXhmcg2KVKV1bnB8JAQWklMQay7ROw3QK5oP7A0KIJMlamYhXmHgctjGjlEEuJeFi10pO5QuA==
|
||||
dependencies:
|
||||
eslint-plugin-node "^11.0.0"
|
||||
eslint-plugin-sort-requires-by-path "^1.0.2"
|
||||
|
Loading…
x
Reference in New Issue
Block a user