diff --git a/demos/sankey.html b/demos/sankey.html index fa06b37dd..b06ead15e 100644 --- a/demos/sankey.html +++ b/demos/sankey.html @@ -17,22 +17,20 @@

Simple flow

       sankey
-      a--10->b
-      c--20->b
-      b--15->d
-      a-- 7->d
+      node_a->10->node_b;
+      node_c->20->node_b;
+      node_b->15->node_d;
+      node_a-> 7->node_d;
     
diff --git a/docker-compose.yml b/docker-compose.yml index 21da2edd5..5cf97ef6d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,8 +13,10 @@ services: - ./:/mermaid - root_cache:/root/.cache - root_local:/root/.local + - root_local:/root/.npm ports: - 9000:9000 volumes: root_cache: root_local: + root_npm: diff --git a/package.json b/package.json index ac197712c..6a291f0d6 100644 --- a/package.json +++ b/package.json @@ -85,7 +85,12 @@ "coveralls": "^3.1.1", "cypress": "^12.10.0", "cypress-image-snapshot": "^4.0.1", +<<<<<<< HEAD "esbuild": "^0.18.0", +======= + "d3-sankey": "^0.12.3", + "esbuild": "^0.17.18", +>>>>>>> 84c278d0 (At last something is working) "eslint": "^8.39.0", "eslint-config-prettier": "^8.8.0", "eslint-plugin-cypress": "^2.13.2", diff --git a/packages/mermaid/src/config.type.ts b/packages/mermaid/src/config.type.ts index 140c200fb..3f3716928 100644 --- a/packages/mermaid/src/config.type.ts +++ b/packages/mermaid/src/config.type.ts @@ -32,6 +32,7 @@ export interface MermaidConfig { mindmap?: MindmapDiagramConfig; gitGraph?: GitGraphDiagramConfig; c4?: C4DiagramConfig; + sankey?: SankeyDiagramConfig; dompurifyConfig?: DOMPurify.Config; wrap?: boolean; fontSize?: number; @@ -411,6 +412,8 @@ export interface FlowchartDiagramConfig extends BaseDiagramConfig { wrappingWidth?: number; } +export interface SankeyDiagramConfig extends BaseDiagramConfig {} + export interface FontConfig { fontSize?: string | number; fontFamily?: string; diff --git a/packages/mermaid/src/diagrams/sankey/parser/sankey.jison b/packages/mermaid/src/diagrams/sankey/parser/sankey.jison index cab630252..231c3fd50 100644 --- a/packages/mermaid/src/diagrams/sankey/parser/sankey.jison +++ b/packages/mermaid/src/diagrams/sankey/parser/sankey.jison @@ -40,6 +40,7 @@ /lex %start start +%left ARROW %% // language grammar @@ -66,10 +67,19 @@ attribute: ATTRIBUTE EQUAL value | ATTRIBUTE; value: VALUE | OPEN_STRING STRING CLOSE_STRING; -stream: node ARROW AMOUNT ARROW tail { yy.addNode($1); yy.addLink(); }; -tail: stream | node; +stream: node[source] ARROW amount ARROW tail[target] { + $$=$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 // exhaust_chain: ARROW AMOUNT intake_chain | ; diff --git a/packages/mermaid/src/diagrams/sankey/parser/sankey.spec.js b/packages/mermaid/src/diagrams/sankey/parser/sankey.spec.js index 47345d1b6..ee52efefc 100644 --- a/packages/mermaid/src/diagrams/sankey/parser/sankey.spec.js +++ b/packages/mermaid/src/diagrams/sankey/parser/sankey.spec.js @@ -21,7 +21,7 @@ describe('Sankey diagram', function () { it('recognizes one flow', () => { const str = ` sankey - a -> 30 -> b -> 20 -> c + node_a -> 30 -> node_b -> 20 -> node_c `; parser.parse(str); @@ -30,9 +30,9 @@ describe('Sankey diagram', function () { it('recognizes multiple flows', () => { const str = ` sankey - a -> 30 -> b -> 12 -> e - c -> 30 -> d -> 12 -> e - c -> 40 -> e -> 12 -> q + node_a -> 30 -> node_b -> 12 -> node_e + node_c -> 30 -> node_d -> 12 -> node_e + node_c -> 40 -> node_e -> 12 -> node_q `; parser.parse(str); @@ -44,7 +44,7 @@ describe('Sankey diagram', function () { sankey node[] node[attr=1] - a -> 30 -> b + node_a -> 30 -> node_b node[attrWithoutValue] node[attr = 3] node[attr1 = 23413 attr2=1234] diff --git a/packages/mermaid/src/diagrams/sankey/sankeyDB.ts b/packages/mermaid/src/diagrams/sankey/sankeyDB.ts index f43f3514a..4f50fa4cc 100644 --- a/packages/mermaid/src/diagrams/sankey/sankeyDB.ts +++ b/packages/mermaid/src/diagrams/sankey/sankeyDB.ts @@ -1,12 +1,12 @@ -import { log } from '../../logger.js'; -import mermaidAPI from '../../mermaidAPI.js'; +// import { log } from '../../logger.js'; +// import mermaidAPI from '../../mermaidAPI.js'; import * as configApi from '../../config.js'; import common from '../common/common.js'; import { - // setAccTitle, - // getAccTitle, - // getAccDescription, - // setAccDescription, + setAccTitle, + getAccTitle, + getAccDescription, + setAccDescription, setDiagramTitle, getDiagramTitle, clear as commonClear, @@ -20,46 +20,54 @@ import { // return text.trimStart().replace(/^\s*%%(?!{)[^\n]+\n?/gm, ''); // }; let links: Array = []; -let nodes: { [id: string]: Node } = {}; +let nodes: Array = []; +let nodesHash: Record = {}; -const clear = function () { +const clear = () => { links = []; - nodes = {}; + nodes = []; + nodesHash = {}; commonClear(); }; type Nullable = T | null; +interface ILink { + source?: Node; + target?: Node; + amount?: number; +} + class Link { - sourceNode: Nullable; - targetNode: Nullable; + source: Nullable; + target: Nullable; + amount: Nullable; constructor() { - this.sourceNode = null; - this.targetNode = null; + this.source = 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 targetNodeID - The id Node where the link ends + * @param source - Node where the link starts + * @param target - Node where the link ends + * @param amount - number, float or integer, describes the amount to be passed */ - -interface IAddLink { - sourceNodeID?: string; - targetNodeID?: string; - // amount?: number; -} - -const addLink = ({ sourceNodeID, targetNodeID }: IAddLink = {}): Link => { +// const addLink = ({ source, target, amount }: ILink = {}): Link => { +const addLink = (source?: Node, target?: Node, amount?: number): Link => { const link: Link = new Link(); - if (sourceNodeID !== undefined) { - link.sourceNode = addNode(sourceNodeID); + if (source !== undefined) { + link.source = source; } - if (targetNodeID !== undefined) { - link.targetNode = addNode(targetNodeID); + if (target !== undefined) { + link.target = target; + } + if (amount !== undefined) { + link.amount = amount; } links.push(link); @@ -68,11 +76,11 @@ const addLink = ({ sourceNodeID, targetNodeID }: IAddLink = {}): Link => { }; class Node { - id: string; + ID: string; title: string; - constructor(id: string) { - this.id = id; - this.title = id; + constructor(ID: string) { + this.ID = ID; + this.title = ID; } } @@ -81,25 +89,28 @@ class Node { * * @param id - The id Node */ -const addNode = (id: string): Node => { - id = common.sanitizeText(id, configApi.getConfig()); - if (nodes[id] === undefined) { - nodes[id] = new Node(id); +const addNode = (ID: string): Node => { + ID = common.sanitizeText(ID, configApi.getConfig()); + if (nodesHash[ID] === undefined) { + nodesHash[ID] = new Node(ID); } - const node = nodes[id]; + const node = nodesHash[ID]; + nodes.push(node); + // debugger; return node; }; export default { - // sankey interface + nodesHash, + nodes, + links, addLink, addNode, - // common DB interface // TODO: If this is a must this probably should be an interface - // getAccTitle, - // setAccTitle, - // getAccDescription, - // setAccDescription, + getAccTitle, + setAccTitle, + getAccDescription, + setAccDescription, getDiagramTitle, setDiagramTitle, clear, diff --git a/packages/mermaid/src/diagrams/sankey/sankeyRenderer.ts b/packages/mermaid/src/diagrams/sankey/sankeyRenderer.ts index 916fd7ed0..5d7fb2f64 100644 --- a/packages/mermaid/src/diagrams/sankey/sankeyRenderer.ts +++ b/packages/mermaid/src/diagrams/sankey/sankeyRenderer.ts @@ -1,22 +1,202 @@ +// @ts-nocheck TODO: fix file 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. * - * @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 _version - Mermaid version from package.json * @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) { - // debugger; - // diagObj.db.clear(); +export const draw = function (text: string, id: string, _version: string, diagObj: Diagram): void { + + // 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); + 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; - 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 { diff --git a/run b/run index 9b02b0741..09f63cb82 100755 --- a/run +++ b/run @@ -10,23 +10,19 @@ args=${@:2} case $command in sh) -$RUN mermaid sh $args +$RUN --service-ports mermaid sh $args ;; i | install) $RUN mermaid sh -c "npx pnpm install $args" ;; -test) -$RUN mermaid sh -c "npx pnpm test $args" +add) +$RUN mermaid sh -c "npx pnpm -w add $args" ;; -vitest) -$RUN mermaid sh -c "npx pnpm vitest $args" -;; - -e2e) -$RUN mermaid sh -c "npx pnpm e2e $args" +test | vitest | e2e ) +$RUN mermaid sh -c "npx pnpm $command $args" ;; 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 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 test # Run unit tests $name vitest # Run watcher for unit tests