diff --git a/cypress/integration/rendering/flowchart-v2.spec.js b/cypress/integration/rendering/flowchart-v2.spec.js index b7583ccf19..e23820ffa3 100644 --- a/cypress/integration/rendering/flowchart-v2.spec.js +++ b/cypress/integration/rendering/flowchart-v2.spec.js @@ -886,4 +886,93 @@ end }); }); }); + describe('Subgraph title margins', () => { + it('Should render subgraphs with title margins set (LR)', () => { + imgSnapshotTest( + `flowchart LR + + subgraph TOP + direction TB + subgraph B1 + direction RL + i1 -->f1 + end + subgraph B2 + direction BT + i2 -->f2 + end + end + A --> TOP --> B + B1 --> B2 + `, + { flowchart: { subGraphTitleMargin: { top: 10, bottom: 5 } } } + ); + }); + it('Should render subgraphs with title margins set (TD)', () => { + imgSnapshotTest( + `flowchart TD + + subgraph TOP + direction LR + subgraph B1 + direction RL + i1 -->f1 + end + subgraph B2 + direction BT + i2 -->f2 + end + end + A --> TOP --> B + B1 --> B2 + `, + { flowchart: { subGraphTitleMargin: { top: 8, bottom: 16 } } } + ); + }); + it('Should render subgraphs with title margins set (LR) and htmlLabels set to false', () => { + imgSnapshotTest( + `flowchart LR + + subgraph TOP + direction TB + subgraph B1 + direction RL + i1 -->f1 + end + subgraph B2 + direction BT + i2 -->f2 + end + end + A --> TOP --> B + B1 --> B2 + `, + { + htmlLabels: false, + flowchart: { htmlLabels: false, subGraphTitleMargin: { top: 10, bottom: 5 } }, + } + ); + }); + it('Should render subgraphs with title margins and edge labels', () => { + imgSnapshotTest( + `flowchart LR + + subgraph TOP + direction TB + subgraph B1 + direction RL + i1 --lb1-->f1 + end + subgraph B2 + direction BT + i2 --lb2-->f2 + end + end + A --lb3--> TOP --lb4--> B + B1 --lb5--> B2 + `, + { flowchart: { subGraphTitleMargin: { top: 10, bottom: 5 } } } + ); + }); + }); }); diff --git a/packages/mermaid/src/config.type.ts b/packages/mermaid/src/config.type.ts index 82d5e1d008..402d9a4d86 100644 --- a/packages/mermaid/src/config.type.ts +++ b/packages/mermaid/src/config.type.ts @@ -1411,6 +1411,14 @@ export interface FlowchartDiagramConfig extends BaseDiagramConfig { * Margin top for the text over the diagram */ titleTopMargin?: number; + /** + * Defines a top/bottom margin for subgraph titles + * + */ + subGraphTitleMargin?: { + top?: number; + bottom?: number; + }; arrowMarkerAbsolute?: boolean; /** * The amount of padding around the diagram as a whole so that embedded diff --git a/packages/mermaid/src/dagre-wrapper/clusters.js b/packages/mermaid/src/dagre-wrapper/clusters.js index 5c6e5a4e05..2de3f2489a 100644 --- a/packages/mermaid/src/dagre-wrapper/clusters.js +++ b/packages/mermaid/src/dagre-wrapper/clusters.js @@ -5,9 +5,11 @@ import { createText } from '../rendering-util/createText.js'; import { select } from 'd3'; import { getConfig } from '../diagram-api/diagramAPI.js'; import { evaluate } from '../diagrams/common/common.js'; +import { getSubGraphTitleMargins } from '../utils/subGraphTitleMargins.js'; const rect = (parent, node) => { log.info('Creating subgraph rect for ', node.id, node); + const siteConfig = getConfig(); // Add outer g element const shapeSvg = parent @@ -18,7 +20,7 @@ const rect = (parent, node) => { // add the rect const rect = shapeSvg.insert('rect', ':first-child'); - const useHtmlLabels = evaluate(getConfig().flowchart.htmlLabels); + const useHtmlLabels = evaluate(siteConfig.flowchart.htmlLabels); // Create the label and insert it after the rect const label = shapeSvg.insert('g').attr('class', 'cluster-label'); @@ -34,7 +36,7 @@ const rect = (parent, node) => { // Get the size of the label let bbox = text.getBBox(); - if (evaluate(getConfig().flowchart.htmlLabels)) { + if (evaluate(siteConfig.flowchart.htmlLabels)) { const div = text.children[0]; const dv = select(text); bbox = div.getBoundingClientRect(); @@ -63,17 +65,18 @@ const rect = (parent, node) => { .attr('width', width) .attr('height', node.height + padding); + const { subGraphTitleTopMargin } = getSubGraphTitleMargins(siteConfig); if (useHtmlLabels) { label.attr( 'transform', // This puts the labal on top of the box instead of inside it - 'translate(' + (node.x - bbox.width / 2) + ', ' + (node.y - node.height / 2) + ')' + `translate(${node.x - bbox.width / 2}, ${node.y - node.height / 2 + subGraphTitleTopMargin})` ); } else { label.attr( 'transform', // This puts the labal on top of the box instead of inside it - 'translate(' + node.x + ', ' + (node.y - node.height / 2) + ')' + `translate(${node.x}, ${node.y - node.height / 2 + subGraphTitleTopMargin})` ); } // Center the label @@ -127,6 +130,8 @@ const noteGroup = (parent, node) => { return shapeSvg; }; const roundedWithTitle = (parent, node) => { + const siteConfig = getConfig(); + // Add outer g element const shapeSvg = parent.insert('g').attr('class', node.classes).attr('id', node.id); @@ -143,7 +148,7 @@ const roundedWithTitle = (parent, node) => { // Get the size of the label let bbox = text.getBBox(); - if (evaluate(getConfig().flowchart.htmlLabels)) { + if (evaluate(siteConfig.flowchart.htmlLabels)) { const div = text.children[0]; const dv = select(text); bbox = div.getBoundingClientRect(); @@ -175,6 +180,7 @@ const roundedWithTitle = (parent, node) => { .attr('width', width + padding) .attr('height', node.height + padding - bbox.height - 3); + const { subGraphTitleTopMargin } = getSubGraphTitleMargins(siteConfig); // Center the label label.attr( 'transform', @@ -184,7 +190,8 @@ const roundedWithTitle = (parent, node) => { (node.y - node.height / 2 - node.padding / 3 + - (evaluate(getConfig().flowchart.htmlLabels) ? 5 : 3)) + + (evaluate(siteConfig.flowchart.htmlLabels) ? 5 : 3)) + + subGraphTitleTopMargin + ')' ); diff --git a/packages/mermaid/src/dagre-wrapper/edges.js b/packages/mermaid/src/dagre-wrapper/edges.js index c3ae814686..3683901285 100644 --- a/packages/mermaid/src/dagre-wrapper/edges.js +++ b/packages/mermaid/src/dagre-wrapper/edges.js @@ -6,6 +6,7 @@ import { getConfig } from '../diagram-api/diagramAPI.js'; import utils from '../utils.js'; import { evaluate } from '../diagrams/common/common.js'; import { getLineFunctionsWithOffset } from '../utils/lineWithOffset.js'; +import { getSubGraphTitleMargins } from '../utils/subGraphTitleMargins.js'; import { addEdgeMarkers } from './edgeMarker.js'; let edgeLabels = {}; @@ -136,6 +137,8 @@ function setTerminalWidth(fo, value) { export const positionEdgeLabel = (edge, paths) => { log.info('Moving label abc78 ', edge.id, edge.label, edgeLabels[edge.id]); let path = paths.updatedPath ? paths.updatedPath : paths.originalPath; + const siteConfig = getConfig(); + const { subGraphTitleTotalMargin } = getSubGraphTitleMargins(siteConfig); if (edge.label) { const el = edgeLabels[edge.id]; let x = edge.x; @@ -159,7 +162,7 @@ export const positionEdgeLabel = (edge, paths) => { y = pos.y; } } - el.attr('transform', 'translate(' + x + ', ' + y + ')'); + el.attr('transform', `translate(${x}, ${y + subGraphTitleTotalMargin / 2})`); } //let path = paths.updatedPath ? paths.updatedPath : paths.originalPath; @@ -173,7 +176,7 @@ export const positionEdgeLabel = (edge, paths) => { x = pos.x; y = pos.y; } - el.attr('transform', 'translate(' + x + ', ' + y + ')'); + el.attr('transform', `translate(${x}, ${y})`); } if (edge.startLabelRight) { const el = terminalLabels[edge.id].startRight; @@ -189,7 +192,7 @@ export const positionEdgeLabel = (edge, paths) => { x = pos.x; y = pos.y; } - el.attr('transform', 'translate(' + x + ', ' + y + ')'); + el.attr('transform', `translate(${x}, ${y})`); } if (edge.endLabelLeft) { const el = terminalLabels[edge.id].endLeft; @@ -201,7 +204,7 @@ export const positionEdgeLabel = (edge, paths) => { x = pos.x; y = pos.y; } - el.attr('transform', 'translate(' + x + ', ' + y + ')'); + el.attr('transform', `translate(${x}, ${y})`); } if (edge.endLabelRight) { const el = terminalLabels[edge.id].endRight; @@ -213,7 +216,7 @@ export const positionEdgeLabel = (edge, paths) => { x = pos.x; y = pos.y; } - el.attr('transform', 'translate(' + x + ', ' + y + ')'); + el.attr('transform', `translate(${x}, ${y})`); } }; diff --git a/packages/mermaid/src/dagre-wrapper/index.js b/packages/mermaid/src/dagre-wrapper/index.js index 9843adb8b0..76685dd7b9 100644 --- a/packages/mermaid/src/dagre-wrapper/index.js +++ b/packages/mermaid/src/dagre-wrapper/index.js @@ -13,8 +13,10 @@ import { insertNode, positionNode, clear as clearNodes, setNodeElem } from './no import { insertCluster, clear as clearClusters } from './clusters.js'; import { insertEdgeLabel, positionEdgeLabel, insertEdge, clear as clearEdges } from './edges.js'; import { log } from '../logger.js'; +import { getSubGraphTitleMargins } from '../utils/subGraphTitleMargins.js'; +import { getConfig } from '../diagram-api/diagramAPI.js'; -const recursiveRender = async (_elem, graph, diagramtype, id, parentCluster) => { +const recursiveRender = async (_elem, graph, diagramtype, id, parentCluster, siteConfig) => { log.info('Graph in recursive render: XXX', graphlibJson.write(graph), parentCluster); const dir = graph.graph().rankdir; log.trace('Dir in recursive render - dir:', dir); @@ -52,7 +54,14 @@ const recursiveRender = async (_elem, graph, diagramtype, id, parentCluster) => if (node && node.clusterNode) { // const children = graph.children(v); log.info('Cluster identified', v, node.width, graph.node(v)); - const o = await recursiveRender(nodes, node.graph, diagramtype, id, graph.node(v)); + const o = await recursiveRender( + nodes, + node.graph, + diagramtype, + id, + graph.node(v), + siteConfig + ); const newEl = o.elem; updateNodeBounds(node, newEl); node.diff = o.diff || 0; @@ -101,6 +110,7 @@ const recursiveRender = async (_elem, graph, diagramtype, id, parentCluster) => log.info('Graph after layout:', graphlibJson.write(graph)); // Move the nodes to the correct place let diff = 0; + const { subGraphTitleTotalMargin } = getSubGraphTitleMargins(siteConfig); sortNodesByHierarchy(graph).forEach(function (v) { const node = graph.node(v); log.info('Position ' + v + ': ' + JSON.stringify(graph.node(v))); @@ -114,16 +124,18 @@ const recursiveRender = async (_elem, graph, diagramtype, id, parentCluster) => ); if (node && node.clusterNode) { // clusterDb[node.id].node = node; - + node.y += subGraphTitleTotalMargin; positionNode(node); } else { // Non cluster node if (graph.children(v).length > 0) { // A cluster in the non-recursive way // positionCluster(node); + node.height += subGraphTitleTotalMargin; insertCluster(clusters, node); clusterDb[node.id].node = node; } else { + node.y += subGraphTitleTotalMargin / 2; positionNode(node); } } @@ -134,6 +146,7 @@ const recursiveRender = async (_elem, graph, diagramtype, id, parentCluster) => const edge = graph.edge(e); log.info('Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(edge), edge); + edge.points.forEach((point) => (point.y += subGraphTitleTotalMargin / 2)); const paths = insertEdge(edgePaths, e, edge, clusterDb, diagramtype, graph, id); positionEdgeLabel(edge, paths); }); @@ -159,7 +172,8 @@ export const render = async (elem, graph, markers, diagramtype, id) => { adjustClustersAndEdges(graph); log.warn('Graph after:', JSON.stringify(graphlibJson.write(graph))); // log.warn('Graph ever after:', graphlibJson.write(graph.node('A').graph)); - await recursiveRender(elem, graph, diagramtype, id); + const siteConfig = getConfig(); + await recursiveRender(elem, graph, diagramtype, id, undefined, siteConfig); }; // const shapeDefinitions = {}; diff --git a/packages/mermaid/src/schemas/config.schema.yaml b/packages/mermaid/src/schemas/config.schema.yaml index ee92b4875f..2791c32d4b 100644 --- a/packages/mermaid/src/schemas/config.schema.yaml +++ b/packages/mermaid/src/schemas/config.schema.yaml @@ -1863,6 +1863,7 @@ $defs: # JSON Schema definition (maybe we should move these to a separate file) unevaluatedProperties: false required: - titleTopMargin + - subGraphTitleMargin - diagramPadding - htmlLabels - nodeSpacing @@ -1875,6 +1876,20 @@ $defs: # JSON Schema definition (maybe we should move these to a separate file) titleTopMargin: $ref: '#/$defs/GitGraphDiagramConfig/properties/titleTopMargin' default: 25 + subGraphTitleMargin: + description: | + Defines a top/bottom margin for subgraph titles + type: object + properties: + top: + type: integer + minimum: 0 + bottom: + type: integer + minimum: 0 + default: + top: 0 + bottom: 0 arrowMarkerAbsolute: type: boolean # TODO, is this actually used here (it has no default value but was in types) diagramPadding: diff --git a/packages/mermaid/src/utils/subGraphTitleMargins.spec.ts b/packages/mermaid/src/utils/subGraphTitleMargins.spec.ts new file mode 100644 index 0000000000..c607f8bce7 --- /dev/null +++ b/packages/mermaid/src/utils/subGraphTitleMargins.spec.ts @@ -0,0 +1,22 @@ +import { getSubGraphTitleMargins } from './subGraphTitleMargins.js'; +import * as configApi from '../config.js'; + +describe('getSubGraphTitleMargins', () => { + it('should get subgraph title margins after config has been set', () => { + const config_0 = { + flowchart: { + subGraphTitleMargin: { + top: 10, + bottom: 5, + }, + }, + }; + + configApi.setSiteConfig(config_0); + expect(getSubGraphTitleMargins(config_0)).toEqual({ + subGraphTitleTopMargin: 10, + subGraphTitleBottomMargin: 5, + subGraphTitleTotalMargin: 15, + }); + }); +}); diff --git a/packages/mermaid/src/utils/subGraphTitleMargins.ts b/packages/mermaid/src/utils/subGraphTitleMargins.ts new file mode 100644 index 0000000000..426f4770d6 --- /dev/null +++ b/packages/mermaid/src/utils/subGraphTitleMargins.ts @@ -0,0 +1,21 @@ +import type { FlowchartDiagramConfig } from '../config.type.js'; + +export const getSubGraphTitleMargins = ({ + flowchart, +}: { + flowchart: FlowchartDiagramConfig; +}): { + subGraphTitleTopMargin: number; + subGraphTitleBottomMargin: number; + subGraphTitleTotalMargin: number; +} => { + const subGraphTitleTopMargin = flowchart?.subGraphTitleMargin?.top ?? 0; + const subGraphTitleBottomMargin = flowchart?.subGraphTitleMargin?.bottom ?? 0; + const subGraphTitleTotalMargin = subGraphTitleTopMargin + subGraphTitleBottomMargin; + + return { + subGraphTitleTopMargin, + subGraphTitleBottomMargin, + subGraphTitleTotalMargin, + }; +};