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 */ /** mermaid */
%lex %lex
TOKEN \w+
NUM \d+(.\d+)?
%options case-insensitive %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 %x attributes
// or if we use "" we are expecting a string containing value %x attr_value
%x string %x string
%x value
%% %%
//--------------------------------------------------------------
// skip all whitespace EXCEPT newlines, but not within a string // skip all whitespace EXCEPT newlines, but not within a string
<INITIAL,attributes,value>[^\S\r\n]+ {} //--------------------------------------------------------------
// main <INITIAL,link_value,attributes,attr_value>[^\S\r\n]+ {}
"sankey" { return 'SANKEY'; }
\d+(.\d+)? { return 'AMOUNT'; } //--------------
"->" { return 'ARROW'; } // basic tokens
\w+ { return 'NODE'; } //--------------
(?:<<EOF>>|[\n;])+ { return 'EOS'; } // end of statement is semicolon ; new line \n or end of file
(<<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 // attributes
"[" { this.pushState('attributes'); return 'OPEN_ATTRIBUTES'; } //------------
<attributes>"]" { this.popState(); return 'CLOSE_ATTRIBUTES'; }
<attributes>\w+ { return 'ATTRIBUTE'; } "[" { this.pushState('attributes'); return 'OPEN_ATTRIBUTES'; }
<attributes>\= { this.pushState('value'); return 'EQUAL'; } <attributes>"]" { this.popState(); return 'CLOSE_ATTRIBUTES'; }
<value>\w+ { this.popState(); return 'VALUE'; } <attributes>{TOKEN} { return 'ATTRIBUTE'; }
<attributes>\= { this.pushState('attr_value'); return 'EQUAL'; }
<attr_value>{TOKEN} { this.popState(); return 'VALUE'; }
//------------
// strings // strings
<INITIAL,attributes,value>\" { this.pushState('string'); return 'OPEN_STRING'; } //------------
<string>(?!\\)\" {
if(this.topState()==='string') this.popState(); <INITIAL,attributes,attr_value>\" { this.pushState('string'); return 'OPEN_STRING'; }
if(this.topState()==='value') this.popState(); <string>(?!\\)\" {
return 'CLOSE_STRING'; if(this.topState()==='string') this.popState();
} if(this.topState()==='attr_value') this.popState();
<string>([^"\\]|\\\"|\\\\)+ { return 'STRING'; } return 'CLOSE_STRING';
}
<string>([^"\\]|\\\"|\\\\)+ { return 'STRING'; }
/lex /lex
@@ -43,20 +62,20 @@
%% // language grammar %% // language grammar
start start
: EOS SANKEY document : EOS SANKEY document
| SANKEY document | SANKEY document
; ;
document document
: line document : line document
| |
; ;
line line
: stream optional_attributes EOS : node optional_attributes EOS
| node optional_attributes EOS | stream optional_attributes EOS
| EOS | EOS
; ;
optional_attributes: OPEN_ATTRIBUTES attributes CLOSE_ATTRIBUTES | ; optional_attributes: OPEN_ATTRIBUTES attributes CLOSE_ATTRIBUTES | ;
@@ -65,20 +84,22 @@ attribute: ATTRIBUTE EQUAL value | ATTRIBUTE;
value: VALUE | OPEN_STRING STRING CLOSE_STRING; value: VALUE | OPEN_STRING STRING CLOSE_STRING;
stream: node[source] ARROW amount ARROW tail[target] { stream
$$=$source; : node\[source] ARROW amount ARROW tail\[target] {
yy.addLink($source, $target, $amount); $$=$source;
}; yy.addLink($source, $target, $amount);
}
;
tail
: stream { $$ = $stream }
| node { $$ = $node; }
;
amount: AMOUNT { $$=parseFloat($AMOUNT); }; amount: AMOUNT { $$=parseFloat($AMOUNT); };
tail
: stream { $$ = $stream }
| node { $$ = $node; }
;
node node
: NODE { $$ = yy.addNode($NODE); } : NODE_ID { $$ = yy.findOrCreateNode($NODE_ID); }
| OPEN_STRING STRING[title] CLOSE_STRING { $$ = yy.addNode($title); /* TODO: add title and id separately?*/ } | 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', () => { 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', () => { it('parses different quotless variations', () => {
const str = ` const str = `
sankey sankey
node[] node[]
node[attr=1] node[attr=1]
node_a -> 30 -> node_b node_a -> 30 -> node_b
node[attrWithoutValue] node[attrWithoutValue]
@@ -149,6 +158,7 @@ describe('Sankey diagram', function () {
"Wave" -> 19.013 -> "Electricity grid" "Wave" -> 19.013 -> "Electricity grid"
"Wind" -> 289.366 -> "Electricity grid" "Wind" -> 289.366 -> "Electricity grid"
`; `;
parser.parse(str); parser.parse(str);
}); });
}); });

View File

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

View File

@@ -1,5 +1,5 @@
import { DiagramDefinition } from '../../diagram-api/types.js'; 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 parser from './parser/sankey.jison';
import db from './sankeyDB.js'; import db from './sankeyDB.js';
import styles from './styles.js'; import styles from './styles.js';

View File

@@ -2,6 +2,7 @@
import { Diagram } from '../../Diagram.js'; import { Diagram } from '../../Diagram.js';
// import { log } from '../../logger.js'; // import { log } from '../../logger.js';
import * as configApi from '../../config.js'; import * as configApi from '../../config.js';
import { import {
select as d3select, select as d3select,
scaleOrdinal as d3scaleOrdinal, scaleOrdinal as d3scaleOrdinal,
@@ -19,9 +20,7 @@ import {
sankeyJustify as d3SankeyJustify, sankeyJustify as d3SankeyJustify,
} from 'd3-sankey'; } from 'd3-sankey';
import { configureSvgSize } from '../../setupGraphViewbox.js'; import { configureSvgSize } from '../../setupGraphViewbox.js';
import sankeyDB from './sankeyDB.js'; // import { debug } from 'console';
import { db } from '../info/infoDb.js';
import { debug } from 'console';
/** /**
* Draws a sequenceDiagram in the tag with id: id based on the graph definition in text. * 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(); const { securityLevel, sequence: conf } = configApi.getConfig();
let sandboxElement; let sandboxElement;
if (securityLevel === 'sandbox') { if (securityLevel === 'sandbox') {
sandboxElement = select('#i' + id); sandboxElement = d3select('#i' + id);
} }
const root = const root =
securityLevel === 'sandbox' 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) => { const graph = diagObj.db.getGraph();
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 },
// ],
// };
// Construct and configure a Sankey generator // Construct and configure a Sankey generator
// That will be a function that calculates nodes and links dimensions // 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 // Get color scheme for the graph
const color = d3scaleOrdinal(d3schemeTableau10); const color = d3scaleOrdinal(d3schemeTableau10);
// Creates the groups for nodes // Create groups for nodes
svg svg
.append('g') .append('g')
.attr('class', 'nodes') .attr('class', 'nodes')
.attr('stroke', '#000')
.selectAll('.node') .selectAll('.node')
.data(graph.nodes) .data(graph.nodes)
.join('g') .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('width', (d) => d.x1 - d.x0)
.attr('fill', (d) => color(d.id)); .attr('fill', (d) => color(d.id));
// Create text for nodes // Create labels for nodes
svg svg
.append('g') .append('g')
.attr('class', 'node-labels') .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('y', (d) => (d.y1 + d.y0) / 2)
.attr('dy', '0.35em') .attr('dy', '0.35em')
.attr('text-anchor', (d) => (d.x0 < width / 2 ? 'start' : 'end')) .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. // Creates the paths that represent the links.
const link_g = svg const link_g = svg