Updated syntax and fixed comments from review

This commit is contained in:
Nikolay Rozhkov
2023-06-20 02:37:27 +03:00
parent fd3ffdc22a
commit 6077ba5405
5 changed files with 114 additions and 140 deletions

View File

@@ -1,39 +1,58 @@
/** mermaid */
%lex
TOKEN \w+
NUM \d+(.\d+)?
%options case-insensitive
%options easy_keyword_rules
%options easy_keword_rules
%s link_value
// when we are inside [] section we are defining attrubutes
%x attributes
// or if we use "" we are expecting a string containing value
%x attr_value
%x string
%x value
%%
//--------------------------------------------------------------
// skip all whitespace EXCEPT newlines, but not within a string
<INITIAL,attributes,value>[^\S\r\n]+ {}
//--------------------------------------------------------------
// main
"sankey" { return 'SANKEY'; }
\d+(.\d+)? { return 'AMOUNT'; }
"->" { return 'ARROW'; }
\w+ { return 'NODE'; }
(?:<<EOF>>|[\n;])+ { return 'EOS'; } // end of statement is semicolon ; new line \n or end of file
<INITIAL,link_value,attributes,attr_value>[^\S\r\n]+ {}
//--------------
// basic tokens
//--------------
(<<EOF>>|[\n;])+ { return 'EOS'; } // end of statement is semicolon ; new line \n or end of file
"sankey" { return 'SANKEY'; }
<INITIAL>{TOKEN} { return 'NODE_ID'; }
<link_value>{NUM} { return 'AMOUNT'; }
"->" {
if(this.topState()!=='link_value') this.pushState('link_value');
else this.popState();
return 'ARROW';
}
//------------
// attributes
"[" { this.pushState('attributes'); return 'OPEN_ATTRIBUTES'; }
<attributes>"]" { this.popState(); return 'CLOSE_ATTRIBUTES'; }
<attributes>\w+ { return 'ATTRIBUTE'; }
<attributes>\= { this.pushState('value'); return 'EQUAL'; }
<value>\w+ { this.popState(); return 'VALUE'; }
//------------
"[" { this.pushState('attributes'); return 'OPEN_ATTRIBUTES'; }
<attributes>"]" { this.popState(); return 'CLOSE_ATTRIBUTES'; }
<attributes>{TOKEN} { return 'ATTRIBUTE'; }
<attributes>\= { this.pushState('attr_value'); return 'EQUAL'; }
<attr_value>{TOKEN} { this.popState(); return 'VALUE'; }
//------------
// strings
<INITIAL,attributes,value>\" { this.pushState('string'); return 'OPEN_STRING'; }
<string>(?!\\)\" {
if(this.topState()==='string') this.popState();
if(this.topState()==='value') this.popState();
return 'CLOSE_STRING';
}
<string>([^"\\]|\\\"|\\\\)+ { return 'STRING'; }
//------------
<INITIAL,attributes,attr_value>\" { this.pushState('string'); return 'OPEN_STRING'; }
<string>(?!\\)\" {
if(this.topState()==='string') this.popState();
if(this.topState()==='attr_value') this.popState();
return 'CLOSE_STRING';
}
<string>([^"\\]|\\\"|\\\\)+ { return 'STRING'; }
/lex
@@ -43,20 +62,20 @@
%% // language grammar
start
: EOS SANKEY document
| SANKEY document
;
: EOS SANKEY document
| SANKEY document
;
document
: line document
|
;
: line document
|
;
line
: stream optional_attributes EOS
| node optional_attributes EOS
| EOS
;
: node optional_attributes EOS
| stream optional_attributes EOS
| EOS
;
optional_attributes: OPEN_ATTRIBUTES attributes CLOSE_ATTRIBUTES | ;
@@ -65,20 +84,22 @@ attribute: ATTRIBUTE EQUAL value | ATTRIBUTE;
value: VALUE | OPEN_STRING STRING CLOSE_STRING;
stream: node[source] ARROW amount ARROW tail[target] {
$$=$source;
yy.addLink($source, $target, $amount);
};
stream
: node\[source] ARROW amount ARROW tail\[target] {
$$=$source;
yy.addLink($source, $target, $amount);
}
;
tail
: stream { $$ = $stream }
| node { $$ = $node; }
;
amount: AMOUNT { $$=parseFloat($AMOUNT); };
tail
: stream { $$ = $stream }
| node { $$ = $node; }
;
node
: NODE { $$ = yy.addNode($NODE); }
| OPEN_STRING STRING[title] CLOSE_STRING { $$ = yy.addNode($title); /* TODO: add title and id separately?*/ }
;
: NODE_ID { $$ = yy.findOrCreateNode($NODE_ID); }
| OPEN_STRING STRING\[node_label] CLOSE_STRING { $$ = yy.findOrCreateNode($node_label); }
;

View File

@@ -50,11 +50,20 @@ describe('Sankey diagram', function () {
});
describe('while attributes parsing', () => {
it('recognized node and attribute ids starting with numbers', () => {
const str = `
sankey
1st -> 200 -> 2nd -> 180 -> 3rd;
`;
parser.parse(str);
});
it('parses different quotless variations', () => {
const str = `
sankey
node[]
node[attr=1]
node_a -> 30 -> node_b
node[attrWithoutValue]
@@ -149,6 +158,7 @@ describe('Sankey diagram', function () {
"Wave" -> 19.013 -> "Electricity grid"
"Wind" -> 289.366 -> "Electricity grid"
`;
parser.parse(str);
});
});

View File

@@ -12,16 +12,13 @@ import {
clear as commonClear,
} from '../../commonDb.js';
// export const parseDirective = function (statement, context, type) {
// mermaidAPI.parseDirective(this, statement, context, type);
// };
// export const cleanupComments = (text: string): string => {
// return text.trimStart().replace(/^\s*%%(?!{)[^\n]+\n?/gm, '');
// };
let links: Array<Link> = [];
let nodes: Array<Node> = [];
let nodesHash: Record<string, Node> = {};
// Variables where graph data is stored
// Sankey diagram represented by nodes and links between those nodes
// We have to track nodes uniqueness (by ID), thats why we need hash also
//
let links: Array<SankeyLink> = [];
let nodes: Array<SankeyNode> = [];
let nodesHash: Record<string, SankeyNode> = {};
const clear = function () {
links = [];
@@ -30,71 +27,35 @@ const clear = function () {
commonClear();
};
type Nullable<T> = T | null;
// interface ILink {
// source?: Node;
// target?: Node;
// value?: number;
// }
class Link {
source: Nullable<Node>;
target: Nullable<Node>;
value: Nullable<number>;
constructor() {
this.source = null;
this.target = null;
this.value = 0;
}
class SankeyLink {
constructor(public source: SankeyNode, public target: SankeyNode, public value: number = 0) {}
}
/**
* Adds a link between two elements on the diagram
*
* @param source - Node where the link starts
* @param target - Node where the link ends
* @param value - number, float or integer, describes the amount to be passed
*/
// const addLink = ({ source, target, amount }: ILink = {}): Link => {
const addLink = function (source?: Node, target?: Node, value?: number): Link {
const link: Link = new Link();
// TODO: make attribute setters
if (source !== undefined) {
link.source = source;
}
if (target !== undefined) {
link.target = target;
}
if (value !== undefined) {
link.value = value;
}
const addLink = function (source: SankeyNode, target: SankeyNode, value: number): SankeyLink {
const link: SankeyLink = new SankeyLink(source, target, value);
links.push(link);
return link;
};
class Node {
ID: string;
title: string;
constructor(ID: string, title: string = ID) {
this.ID = ID;
this.title = title;
}
class SankeyNode {
constructor(public ID: string, public label: string = ID) {}
}
/**
* Finds or creates a new Node by ID
*
* @param id - The id Node
* @param ID - The id of the node
*/
const addNode = function (ID: string): Node {
const findOrCreateNode = function (ID: string): SankeyNode {
ID = common.sanitizeText(ID, configApi.getConfig());
let node: Node;
let node: SankeyNode;
if (nodesHash[ID] === undefined) {
node = new Node(ID);
node = new SankeyNode(ID);
nodesHash[ID] = node;
nodes.push(node);
} else {
@@ -103,16 +64,27 @@ const addNode = function (ID: string): Node {
return node;
};
// TODO: this will be better using getters in typescript
const getNodes = () => nodes;
const getLinks = () => links;
const getGraph = () => ({
nodes: nodes.map((node) => ({ id: node.ID, label: node.label })),
links: links.map((link) => ({
source: link.source.ID,
target: link.target.ID,
value: link.value,
})),
});
export default {
nodesHash,
getConfig: () => configApi.getConfig().sankey,
getNodes,
getLinks,
getGraph,
addLink,
addNode,
findOrCreateNode,
// TODO: If this is a must this probably should be an interface
getAccTitle,
setAccTitle,

View File

@@ -1,5 +1,5 @@
import { DiagramDefinition } from '../../diagram-api/types.js';
// @ts-ignore: TODO Fix ts errors
// @ts-ignore: jison doesn't export types
import parser from './parser/sankey.jison';
import db from './sankeyDB.js';
import styles from './styles.js';

View File

@@ -2,6 +2,7 @@
import { Diagram } from '../../Diagram.js';
// import { log } from '../../logger.js';
import * as configApi from '../../config.js';
import {
select as d3select,
scaleOrdinal as d3scaleOrdinal,
@@ -19,9 +20,7 @@ import {
sankeyJustify as d3SankeyJustify,
} from 'd3-sankey';
import { configureSvgSize } from '../../setupGraphViewbox.js';
import sankeyDB from './sankeyDB.js';
import { db } from '../info/infoDb.js';
import { debug } from 'console';
// import { debug } from 'console';
/**
* Draws a sequenceDiagram in the tag with id: id based on the graph definition in text.
@@ -51,7 +50,7 @@ export const draw = function (text: string, id: string, _version: string, diagOb
const { securityLevel, sequence: conf } = configApi.getConfig();
let sandboxElement;
if (securityLevel === 'sandbox') {
sandboxElement = select('#i' + id);
sandboxElement = d3select('#i' + id);
}
const root =
securityLevel === 'sandbox'
@@ -86,35 +85,8 @@ export const draw = function (text: string, id: string, _version: string, diagOb
// ]
// };
//
const graph = {
nodes: [],
links: [],
};
diagObj.db.getNodes().forEach((node) => {
graph.nodes.push({ id: node.ID, title: node.title });
});
diagObj.db.getLinks().forEach((link) => {
graph.links.push({ source: link.source.ID, target: link.target.ID, value: link.value });
});
// debugger;
// const graph = {
// nodes: [
// { id: 'Alice' },
// { id: 'Bob' },
// { id: 'Carol' },
// { id: 'Andrew' },
// { id: 'Peter' }
// ],
// links: [
// { source: 'Alice', target: 'Andrew', value: 11 },
// { source: 'Alice', target: 'Bob', value: 23 },
// { source: 'Bob', target: 'Carol', value: 43 },
// { source: 'Peter', target: 'Carol', value: 15 },
// ],
// };
const graph = diagObj.db.getGraph();
// Construct and configure a Sankey generator
// That will be a function that calculates nodes and links dimensions
@@ -145,11 +117,10 @@ export const draw = function (text: string, id: string, _version: string, diagOb
// Get color scheme for the graph
const color = d3scaleOrdinal(d3schemeTableau10);
// Creates the groups for nodes
// Create groups for nodes
svg
.append('g')
.attr('class', 'nodes')
.attr('stroke', '#000')
.selectAll('.node')
.data(graph.nodes)
.join('g')
@@ -166,7 +137,7 @@ export const draw = function (text: string, id: string, _version: string, diagOb
.attr('width', (d) => d.x1 - d.x0)
.attr('fill', (d) => color(d.id));
// Create text for nodes
// Create labels for nodes
svg
.append('g')
.attr('class', 'node-labels')
@@ -179,7 +150,7 @@ export const draw = function (text: string, id: string, _version: string, diagOb
.attr('y', (d) => (d.y1 + d.y0) / 2)
.attr('dy', '0.35em')
.attr('text-anchor', (d) => (d.x0 < width / 2 ? 'start' : 'end'))
.text((d) => d.title);
.text((d) => d.label);
// Creates the paths that represent the links.
const link_g = svg