GraphStream
This commit is contained in:
52
lib/classes/Color.ts
Normal file
52
lib/classes/Color.ts
Normal 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
35
lib/classes/Edge.ts
Normal 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
34
lib/classes/Element.ts
Normal 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
109
lib/classes/Graph.ts
Normal 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
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
|
||||
]
|
||||
});
|
||||
}
|
||||
}
|
66
lib/classes/Node.ts
Normal file
66
lib/classes/Node.ts
Normal 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}`;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user