At last something is working

This commit is contained in:
Nikolay Rozhkov
2023-06-18 01:32:45 +03:00
parent 1782f69c8f
commit afaf87e414
9 changed files with 278 additions and 72 deletions

View File

@@ -17,22 +17,20 @@
<h2>Simple flow</h2> <h2>Simple flow</h2>
<pre class="mermaid"> <pre class="mermaid">
sankey sankey
a--10->b node_a->10->node_b;
c--20->b node_c->20->node_b;
b--15->d node_b->15->node_d;
a-- 7->d node_a-> 7->node_d;
</pre> </pre>
<script type="module"> <script type="module">
import mermaid from './mermaid.esm.mjs'; import mermaid from './mermaid.esm.mjs';
mermaid.initialize({ mermaid.initialize({
theme: 'default', theme: 'default',
// themeCSS: '.node rect { fill: red; }',
logLevel: 3, logLevel: 3,
securityLevel: 'loose', securityLevel: 'loose',
flowchart: { curve: 'basis' }, flowchart: { curve: 'basis' },
gantt: { axisFormat: '%m/%d/%Y' }, gantt: { axisFormat: '%m/%d/%Y' },
sequence: { actorMargin: 50 }, sequence: { actorMargin: 50 },
// sequenceDiagram: { actorMargin: 300 } // deprecated
}); });
</script> </script>
</body> </body>

View File

@@ -13,8 +13,10 @@ services:
- ./:/mermaid - ./:/mermaid
- root_cache:/root/.cache - root_cache:/root/.cache
- root_local:/root/.local - root_local:/root/.local
- root_local:/root/.npm
ports: ports:
- 9000:9000 - 9000:9000
volumes: volumes:
root_cache: root_cache:
root_local: root_local:
root_npm:

View File

@@ -85,7 +85,12 @@
"coveralls": "^3.1.1", "coveralls": "^3.1.1",
"cypress": "^12.10.0", "cypress": "^12.10.0",
"cypress-image-snapshot": "^4.0.1", "cypress-image-snapshot": "^4.0.1",
<<<<<<< HEAD
"esbuild": "^0.18.0", "esbuild": "^0.18.0",
=======
"d3-sankey": "^0.12.3",
"esbuild": "^0.17.18",
>>>>>>> 84c278d0 (At last something is working)
"eslint": "^8.39.0", "eslint": "^8.39.0",
"eslint-config-prettier": "^8.8.0", "eslint-config-prettier": "^8.8.0",
"eslint-plugin-cypress": "^2.13.2", "eslint-plugin-cypress": "^2.13.2",

View File

@@ -32,6 +32,7 @@ export interface MermaidConfig {
mindmap?: MindmapDiagramConfig; mindmap?: MindmapDiagramConfig;
gitGraph?: GitGraphDiagramConfig; gitGraph?: GitGraphDiagramConfig;
c4?: C4DiagramConfig; c4?: C4DiagramConfig;
sankey?: SankeyDiagramConfig;
dompurifyConfig?: DOMPurify.Config; dompurifyConfig?: DOMPurify.Config;
wrap?: boolean; wrap?: boolean;
fontSize?: number; fontSize?: number;
@@ -411,6 +412,8 @@ export interface FlowchartDiagramConfig extends BaseDiagramConfig {
wrappingWidth?: number; wrappingWidth?: number;
} }
export interface SankeyDiagramConfig extends BaseDiagramConfig {}
export interface FontConfig { export interface FontConfig {
fontSize?: string | number; fontSize?: string | number;
fontFamily?: string; fontFamily?: string;

View File

@@ -40,6 +40,7 @@
/lex /lex
%start start %start start
%left ARROW
%% // language grammar %% // language grammar
@@ -66,10 +67,19 @@ attribute: ATTRIBUTE EQUAL value | ATTRIBUTE;
value: VALUE | OPEN_STRING STRING CLOSE_STRING; value: VALUE | OPEN_STRING STRING CLOSE_STRING;
stream: node ARROW AMOUNT ARROW tail { yy.addNode($1); yy.addLink(); }; stream: node[source] ARROW amount ARROW tail[target] {
tail: stream | node; $$=$source;
yy.addLink($source, $target, $amount);
};
node: NODE { yy.addNode($1) }; amount: AMOUNT { $$=parseFloat($AMOUNT); };
tail
: stream { $$ = $stream }
| node { $$ = $node; }
;
node: NODE { $$ = yy.addNode($NODE); };
// : NODE exhaust intake exhaust_chain optional_attributes EOS // : NODE exhaust intake exhaust_chain optional_attributes EOS
// exhaust_chain: ARROW AMOUNT intake_chain | ; // exhaust_chain: ARROW AMOUNT intake_chain | ;

View File

@@ -21,7 +21,7 @@ describe('Sankey diagram', function () {
it('recognizes one flow', () => { it('recognizes one flow', () => {
const str = ` const str = `
sankey sankey
a -> 30 -> b -> 20 -> c node_a -> 30 -> node_b -> 20 -> node_c
`; `;
parser.parse(str); parser.parse(str);
@@ -30,9 +30,9 @@ describe('Sankey diagram', function () {
it('recognizes multiple flows', () => { it('recognizes multiple flows', () => {
const str = ` const str = `
sankey sankey
a -> 30 -> b -> 12 -> e node_a -> 30 -> node_b -> 12 -> node_e
c -> 30 -> d -> 12 -> e node_c -> 30 -> node_d -> 12 -> node_e
c -> 40 -> e -> 12 -> q node_c -> 40 -> node_e -> 12 -> node_q
`; `;
parser.parse(str); parser.parse(str);
@@ -44,7 +44,7 @@ describe('Sankey diagram', function () {
sankey sankey
node[] node[]
node[attr=1] node[attr=1]
a -> 30 -> b node_a -> 30 -> node_b
node[attrWithoutValue] node[attrWithoutValue]
node[attr = 3] node[attr = 3]
node[attr1 = 23413 attr2=1234] node[attr1 = 23413 attr2=1234]

View File

@@ -1,12 +1,12 @@
import { log } from '../../logger.js'; // import { log } from '../../logger.js';
import mermaidAPI from '../../mermaidAPI.js'; // import mermaidAPI from '../../mermaidAPI.js';
import * as configApi from '../../config.js'; import * as configApi from '../../config.js';
import common from '../common/common.js'; import common from '../common/common.js';
import { import {
// setAccTitle, setAccTitle,
// getAccTitle, getAccTitle,
// getAccDescription, getAccDescription,
// setAccDescription, setAccDescription,
setDiagramTitle, setDiagramTitle,
getDiagramTitle, getDiagramTitle,
clear as commonClear, clear as commonClear,
@@ -20,46 +20,54 @@ import {
// return text.trimStart().replace(/^\s*%%(?!{)[^\n]+\n?/gm, ''); // return text.trimStart().replace(/^\s*%%(?!{)[^\n]+\n?/gm, '');
// }; // };
let links: Array<Link> = []; let links: Array<Link> = [];
let nodes: { [id: string]: Node } = {}; let nodes: Array<Node> = [];
let nodesHash: Record<string, Node> = {};
const clear = function () { const clear = () => {
links = []; links = [];
nodes = {}; nodes = [];
nodesHash = {};
commonClear(); commonClear();
}; };
type Nullable<T> = T | null; type Nullable<T> = T | null;
interface ILink {
source?: Node;
target?: Node;
amount?: number;
}
class Link { class Link {
sourceNode: Nullable<Node>; source: Nullable<Node>;
targetNode: Nullable<Node>; target: Nullable<Node>;
amount: Nullable<number>;
constructor() { constructor() {
this.sourceNode = null; this.source = null;
this.targetNode = null; this.target = null;
this.amount = 0;
} }
} }
/** /**
* Adds a stream between two elements on the diagram * Adds a link between two elements on the diagram
* *
* @param sourceNodeID - The id Node where the link starts * @param source - Node where the link starts
* @param targetNodeID - The id Node where the link ends * @param target - Node where the link ends
* @param amount - number, float or integer, describes the amount to be passed
*/ */
// const addLink = ({ source, target, amount }: ILink = {}): Link => {
interface IAddLink { const addLink = (source?: Node, target?: Node, amount?: number): Link => {
sourceNodeID?: string;
targetNodeID?: string;
// amount?: number;
}
const addLink = ({ sourceNodeID, targetNodeID }: IAddLink = {}): Link => {
const link: Link = new Link(); const link: Link = new Link();
if (sourceNodeID !== undefined) { if (source !== undefined) {
link.sourceNode = addNode(sourceNodeID); link.source = source;
} }
if (targetNodeID !== undefined) { if (target !== undefined) {
link.targetNode = addNode(targetNodeID); link.target = target;
}
if (amount !== undefined) {
link.amount = amount;
} }
links.push(link); links.push(link);
@@ -68,11 +76,11 @@ const addLink = ({ sourceNodeID, targetNodeID }: IAddLink = {}): Link => {
}; };
class Node { class Node {
id: string; ID: string;
title: string; title: string;
constructor(id: string) { constructor(ID: string) {
this.id = id; this.ID = ID;
this.title = id; this.title = ID;
} }
} }
@@ -81,25 +89,28 @@ class Node {
* *
* @param id - The id Node * @param id - The id Node
*/ */
const addNode = (id: string): Node => { const addNode = (ID: string): Node => {
id = common.sanitizeText(id, configApi.getConfig()); ID = common.sanitizeText(ID, configApi.getConfig());
if (nodes[id] === undefined) { if (nodesHash[ID] === undefined) {
nodes[id] = new Node(id); nodesHash[ID] = new Node(ID);
} }
const node = nodes[id]; const node = nodesHash[ID];
nodes.push(node);
// debugger;
return node; return node;
}; };
export default { export default {
// sankey interface nodesHash,
nodes,
links,
addLink, addLink,
addNode, addNode,
// common DB interface
// 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,
// getAccDescription, getAccDescription,
// setAccDescription, setAccDescription,
getDiagramTitle, getDiagramTitle,
setDiagramTitle, setDiagramTitle,
clear, clear,

View File

@@ -1,22 +1,202 @@
// @ts-nocheck TODO: fix file
import { Diagram } from '../../Diagram.js'; import { Diagram } from '../../Diagram.js';
import { log } from '../../logger.js';
import * as configApi from '../../config.js';
import {
select as d3select,
scaleOrdinal as d3scaleOrdinal,
schemeTableau10 as d3schemeTableau10,
// rgb as d3rgb,
map as d3map,
} from 'd3';
import {
sankey as d3sankey,
sankeyLinkHorizontal
} from 'd3-sankey';
import { configureSvgSize } from '../../setupGraphViewbox.js';
import sankeyDB from './sankeyDB.js';
/** /**
* 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.
* *
* @param _text - The text of the diagram * @param text - The text of the diagram
* @param id - The id of the diagram which will be used as a DOM element id¨ * @param id - The id of the diagram which will be used as a DOM element id¨
* @param _version - Mermaid version from package.json * @param _version - Mermaid version from package.json
* @param diagObj - A standard diagram containing the db and the text and type etc of the diagram * @param diagObj - A standard diagram containing the db and the text and type etc of the diagram
*/ */
export const draw = function (text: string, id: string, _version: string, diagObj: Diagram) { export const draw = function (text: string, id: string, _version: string, diagObj: Diagram): void {
// debugger;
// diagObj.db.clear(); // First of all parse sankey language
// Everything that is parsed will be stored in diagObj.DB
// That is why we need to clear DB first
//
if (typeof (diagObj?.db?.clear) !== 'undefined') { // why do we need to check for undefined? typescript complains
diagObj?.db?.clear();
}
// Launch parsing
diagObj.parser.parse(text); diagObj.parser.parse(text);
log.debug('Parsed sankey diagram');
// const elem = doc.getElementById(id); // Figure out what is happening there
// The main thing is svg object that is a wrapper from d3 for svg operations
//
const { securityLevel, sequence: conf } = configApi.getConfig();
let sandboxElement;
if (securityLevel === 'sandbox') {
sandboxElement = select('#i' + id);
}
const root =
securityLevel === 'sandbox'
? d3select(sandboxElement.nodes()[0].contentDocument.body)
: d3select('body');
const doc = securityLevel === 'sandbox' ? sandboxElement.nodes()[0].contentDocument : document;
const svg = securityLevel === 'sandbox' ? root.select(`[id="${id}"]`) : d3select(`[id="${id}"]`);
// Establish svg dimensions
//
const elem = doc.getElementById(id);
const width = elem.parentElement.offsetWidth;
const height = 600;
configureSvgSize(svg, height, width, true);
// Prepare data for construction
// This must be a mutable object with 2 properties:
// `nodes` and `links`
//
// let graph = {
// "nodes": [
// { "id": "Alice" },
// { "id": "Bob" },
// { "id": "Carol" }
// ],
// "links": [
// { "source": "Alice", "target": "Bob", "value": 23 },
// { "source": "Bob", "target": "Carol", "value": 43 }
// ]
// };
//
let graph = {
"nodes": [
{ "id": "Alice" },
{ "id": "Bob" },
{ "id": "Carol" }
],
"links": [
{ "source": "Alice", "target": "Bob", "value": 23 },
{ "source": "Bob", "target": "Carol", "value": 43 }
]
};
// Construct and configure a Sankey generator
// That will be a functino that calculates nodes and links dimentions
//
const sankey = d3sankey()
.nodeId((d) => d.id) // we use 'id' property to identify node
.nodeWidth(36)
.nodePadding(290)
.size([width, height]);
// .nodeAlign(d3Sankey.sankeyLeft) // d3.sankeyLeft, etc.
// .nodeWidth(15)
// .nodePadding(10)
// .extent([[1, 5], [width - 1, height - 5]]);
// .nodeId(d => d['id'])
//
// Compute the Sankey layout
// Namely calalculate nodes and links positions
// Our `graph` object will be mutated by this
//
sankey(graph);
// debugger; // debugger;
return 'TEST';
// const node = svg.append("g")
// .selectAll("rect")
// .data(graph.nodes)
// .join("rect")
// .attr("x", d => d.x0)
// .attr("y", d => d.y0)
// .attr("height", d => d.y1 - d.y0)
// .attr("width", d => d.x1 - d.x0);
// // .attr("stroke", nodeStroke)
// // .attr("stroke-width", nodeStrokeWidth)
// // .attr("stroke-opacity", nodeStrokeOpacity)
// // .attr("stroke-linejoin", nodeStrokeLinejoin)
var color = d3scaleOrdinal(d3schemeTableau10);
// Creates the rects that represent the nodes.
const rect = svg.append("g")
.attr("stroke", "#000")
.selectAll("rect")
.data(graph.nodes)
.join("rect")
.attr("x", d => d.x0)
.attr("y", d => d.y0)
.attr("height", d => d.y1 - d.y0)
.attr("width", d => d.x1 - d.x0)
.attr("fill", d => color(d.node));
// // add in the links
// var link = svg.append("g")
// .selectAll(".link")
// .data(graph.links)
// .enter()
// .append("path")
// .attr("class", "link")
// .attr("d", sankeyLinkHorizontal())
// .style("stroke-width", function (d) { return Math.max(1, d.dy); })
// .sort(function (a, b) { return b.dy - a.dy; });
// // add in the nodes
// var node = svg.append("g")
// .selectAll(".node")
// .data(graph.nodes)
// .enter().append("g")
// .attr("class", "node")
// .attr("transform", function (d) { return "translate(" + d.x + "," + d.y + ")"; })
// // .call(d3.drag()
// // .subject(function(d) { return d; })
// // .on("start", function() { this.parentNode.appendChild(this); })
// // .on("drag", dragmove))
// ;
// // add the rectangles for the nodes
// node
// .append("rect")
// .attr("height", function (d) { return d.dy; })
// .attr("width", generator.nodeWidth())
// .style("fill", function (d) { return d.color = color(d.name.replace(/ .*/, "")); })
// .style("stroke", function (d) { return d3rgb(d.color).darker(2); })
// // Add hover text
// .append("title")
// .text(function (d) { return d.name + "\n" + "There is " + d.value + " stuff in this node"; });
// // add in the title for the nodes
// node
// .append("text")
// .attr("x", -6)
// .attr("y", function (d) { return d.dy / 2; })
// .attr("dy", ".35em")
// .attr("text-anchor", "end")
// .attr("transform", null)
// .text(function (d) { return d.name; })
// .filter(function (d) { return d.x < width / 2; })
// .attr("x", 6 + generator.nodeWidth())
// .attr("text-anchor", "start");
// console.log();
// debugger;
// .layout(1);
// const { nodes, links } = generator({
// nodes: graph.nodes,
// links: graph.links,
// });
}; };
export default { export default {

15
run
View File

@@ -10,23 +10,19 @@ args=${@:2}
case $command in case $command in
sh) sh)
$RUN mermaid sh $args $RUN --service-ports mermaid sh $args
;; ;;
i | install) i | install)
$RUN mermaid sh -c "npx pnpm install $args" $RUN mermaid sh -c "npx pnpm install $args"
;; ;;
test) add)
$RUN mermaid sh -c "npx pnpm test $args" $RUN mermaid sh -c "npx pnpm -w add $args"
;; ;;
vitest) test | vitest | e2e )
$RUN mermaid sh -c "npx pnpm vitest $args" $RUN mermaid sh -c "npx pnpm $command $args"
;;
e2e)
$RUN mermaid sh -c "npx pnpm e2e $args"
;; ;;
lint) lint)
@@ -46,6 +42,7 @@ Run commonly used commands within docker containers
\033[1m$name install\033[0m # Equvalent of pnpm install \033[1m$name install\033[0m # Equvalent of pnpm install
\033[1m$name dev\033[0m # Run dev server with examples, open http://localhost:9000 \033[1m$name dev\033[0m # Run dev server with examples, open http://localhost:9000
$name add # Add package, 'run add d3-sankey'
$name lint # Equvalent of pnpm -w run lint:fix $name lint # Equvalent of pnpm -w run lint:fix
$name test # Run unit tests $name test # Run unit tests
$name vitest # Run watcher for unit tests $name vitest # Run watcher for unit tests