GraphStream

This commit is contained in:
2020-05-06 20:24:37 +02:00
parent acf8004497
commit 8d6d91e562
21 changed files with 357 additions and 81 deletions

52
lib/classes/Color.ts Normal file
View File

@ -0,0 +1,52 @@
/* eslint-disable no-magic-numbers */
import { num_to_hex } from '@sapphirecode/encoding-helper';
export class Color {
public static readonly black = new Color (0, 0, 0);
public static readonly red = new Color (255, 0, 0);
public static readonly green = new Color (0, 255, 0);
public static readonly yellow = new Color (255, 255, 0);
public static readonly blue = new Color (0, 0, 255);
public static readonly magenta = new Color (255, 0, 255);
public static readonly cyan = new Color (0, 255, 255);
public static readonly white = new Color (255, 255, 255);
public static readonly transparent = new Color (0, 0, 0, 0);
public static readonly gray = new Color (128, 128, 128);
private _red: number;
private _green: number;
private _blue: number;
private _alpha: number;
private check_range (n: number): void {
if (n < 0 || n > 255)
throw new Error ('number out of range');
}
public constructor (red: number, green: number, blue: number, alpha = 255) {
this.check_range (red);
this.check_range (green);
this.check_range (blue);
this.check_range (alpha);
this._red = red;
this._green = green;
this._blue = blue;
this._alpha = alpha;
}
// eslint-disable-next-line @typescript-eslint/naming-convention
public toString (): string {
return `#${num_to_hex (
this._red,
2
)}${num_to_hex (
this._green,
2
)}${num_to_hex (
this._blue,
2
)}${this._alpha === 255
? ''
: num_to_hex (this._alpha, 2)}`;
}
}

35
lib/classes/Edge.ts Normal file
View File

@ -0,0 +1,35 @@
import { EdgeStyles } from '../enums/Styles';
import { Color } from './Color';
export class Edge {
public origin: string;
public target: string;
public style?: EdgeStyles;
public color?: Color;
private _directional: boolean;
public constructor (origin: string, target: string, directional: boolean) {
this.origin = origin;
this.target = target;
this._directional = directional;
}
// eslint-disable-next-line @typescript-eslint/naming-convention
public toString (): string {
const attributes = [];
if (this.style)
attributes.push ({ name: 'style', value: this.style.toString () });
if (this.color)
attributes.push ({ name: 'color', value: this.color.toString () });
const attr_string = ` [${attributes.map ((v) => `${v.name}="${v.value}"`)
.join (',')}]`;
return `${this.origin} -${
this._directional ? '>' : '-'
} ${this.target}${attributes.length > 0
? attr_string
: ''}`;
}
}

34
lib/classes/Element.ts Normal file
View File

@ -0,0 +1,34 @@
import { validate_name } from '../Helper';
export class Element {
private _name = '';
protected parent_name: string;
public get full_name (): string {
if (this.parent_name)
return `${this.parent_name}_${this.name}`;
return this.name;
}
public get name (): string {
return this._name;
}
public set name (val: string) {
this._name = validate_name (val);
}
public get parent (): string {
return this.parent_name;
}
public constructor (name: string, parent = '') {
this.name = name;
this.parent_name = parent;
}
// eslint-disable-next-line @typescript-eslint/naming-convention
public toString (): string {
return this.full_name;
}
}

109
lib/classes/Graph.ts Normal file
View File

@ -0,0 +1,109 @@
import { GraphStyles, NodeStyles } from '../enums/Styles';
import { GraphLayouts } from '../enums/GraphLayouts';
import { Element } from './Element';
import { Edge } from './Edge';
import { Node } from './Node';
import { Color } from './Color';
interface NodeOptions {
name: string;
label: string;
style: NodeStyles;
color: Color;
}
export class Graph extends Element {
public children: Array<Graph> = [];
public nodes: Array<Node> = [];
public is_root = false;
public edges: Array<Edge> = [];
public style?: GraphStyles;
public color?: Color;
public directional = true;
public overlap?: boolean | string;
public splines?: boolean | string;
public layout?: GraphLayouts;
private get attributes (): Array<{name: string; value: string}> {
const attributes = [];
if (typeof this.color !== 'undefined')
attributes.push ({ name: 'color', value: this.color.toString () });
if (typeof this.style !== 'undefined')
attributes.push ({ name: 'style', value: this.style.toString () });
if (typeof this.overlap !== 'undefined')
attributes.push ({ name: 'overlap', value: this.overlap.toString () });
if (typeof this.splines !== 'undefined')
attributes.push ({ name: 'splines', value: this.splines.toString () });
if (typeof this.layout !== 'undefined')
attributes.push ({ name: 'layout', value: this.layout.toString () });
return attributes;
}
// eslint-disable-next-line @typescript-eslint/naming-convention
public toString (): string {
const header = this.parent
? `subgraph cluster_${this.full_name}`
: `${this.directional ? 'di' : ''}graph ${this.full_name}`;
let attrs = `\n${this.attributes.map ((v) => `${v.name} = ${v.value}`)
.join ('\n')}\n`;
let children = `\n${this.children.map ((c) => c.toString ())
.join ('\n')}\n`;
let nodes = `\n${this.nodes.map ((c) => c.toString ())
.join ('\n')}\n`;
let edges = `\n${this.edges.map ((c) => c.toString ())
.join ('\n')}\n`;
if (attrs === '\n\n')
attrs = '';
if (children === '\n\n')
children = '';
if (nodes === '\n\n')
nodes = '';
if (edges === '\n\n')
edges = '';
const indented = `${attrs}${children}${nodes}${edges}`
.replace (/\n$/u, '')
.replace (/^/gmu, ' ')
.split ('\n')
.map ((v) => v.trimRight ())
.join ('\n');
return `${header} {${indented}\n}`
.replace (/^\s+$/gmu, '');
}
public add_node (constructor: ((n: Node) => void) | string): string {
const node = new Node ('unnamed', this.full_name);
if (typeof constructor === 'string') {
node.name = constructor;
node.label = constructor;
}
else { constructor (node); }
this.nodes.push (node);
return node.full_name;
}
public add_graph (constructor: ((g: Graph) => void) | string): string {
const graph = new Graph ('unnamed', this.full_name);
graph.directional = this.directional;
if (typeof constructor === 'string')
graph.name = constructor;
else
constructor (graph);
this.children.push (graph);
return graph.full_name;
}
public add_edge (origin: string, target: string): void {
this.edges.push (new Edge (origin, target, this.directional));
}
}

160
lib/classes/GraphStream.ts Normal file
View 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
]
});
}
}

66
lib/classes/Node.ts Normal file
View File

@ -0,0 +1,66 @@
import { NodeStyles } from '../enums/Styles';
import { Element } from './Element';
import { Color } from './Color';
export class Node extends Element {
public label?: string;
public is_table = false;
public table_contents?: Array<Array<string>>;
public style?: NodeStyles;
public color?: Color;
public constructor (name: string, parent: string, label?: string) {
super (name, parent);
this.label = label;
}
private get serialized_table (): string {
if (typeof this.table_contents === 'undefined')
throw new Error ('table contents are undefined');
const mapped_columns = this.table_contents
.map ((val) => `<td>${val.join ('</td><td>')}</td>`);
return `<table>\n <tr>${
mapped_columns.join ('</tr>\n <tr>')
}</tr>\n</table>`;
}
// eslint-disable-next-line @typescript-eslint/naming-convention
public toString (): string {
const attributes = [];
if (this.label || this.is_table) {
attributes.push ({
name: 'label',
value: this.is_table
? this.serialized_table
: this.label
});
}
if (this.style)
attributes.push ({ name: 'style', value: this.style.toString () });
if (this.color)
attributes.push ({ name: 'color', value: this.color.toString () });
const attrs = attributes.map ((v) => {
const d = (/\n/u).test (v.value as string)
? [
'<',
'>'
]
: [
'"',
'"'
];
return `${v.name}=${d[0]}${v.value}${d[1]}`;
})
.join (', ');
if (attributes.length > 0)
return `${this.full_name} [${attrs}]`;
return `${this.full_name}`;
}
}