#1295 Fix for intersection calculation for edges to clusters and adding concurrency in stateDiagrams as clusters

This commit is contained in:
Knut Sveidqvist
2020-04-02 19:35:12 +02:00
parent 933cc333cc
commit 365c741864
9 changed files with 194 additions and 98 deletions

View File

@@ -32,13 +32,31 @@
G-->c G-->c
</div> </div>
<div class="mermaid2" style="width: 50%; height: 20%;"> <div class="mermaid2" style="width: 50%; height: 20%;">
flowchart LR stateDiagram-v2
subgraph id1 [Test] [*] --> monkey
b state monkey {
end Sitting
a-->id1 --
Eating
}
</div> </div>
<div class="mermaid mermaid-apa" style="width: 100%; height: 20%;"> <div class="mermaid2" style="width: 50%; height: 20%;">
stateDiagram-v2
state Active {
[*] --> NumLockOff
NumLockOff --> NumLockOn : EvNumLockPressed
NumLockOn --> NumLockOff : EvNumLockPressed
--
[*] --> CapsLockOff
CapsLockOff --> CapsLockOn : EvCapsLockPressed
CapsLockOn --> CapsLockOff : EvCapsLockPressed
--
[*] --> ScrollLockOff
ScrollLockOff --> ScrollLockOn : EvCapsLockPressed
ScrollLockOn --> ScrollLockOff : EvCapsLockPressed
}
</div>
<div class="mermaid2 mermaid-apa" style="width: 100%; height: 20%;">
stateDiagram stateDiagram
[*] --> Still [*] --> Still
Still --> [*] Still --> [*]
@@ -52,15 +70,39 @@
Moving --> Crash Moving --> Crash
Crash --> [*] Crash --> [*]
</div> </div>
<div class="mermaid2" style="width: 100%; height: 100%;">
stateDiagram-v2
[*] --> First
First --> Second
% First --> Third
state First {
[*] --> fir
fir --> [*]
}
state Second {
[*] --> sec
sec --> [*]
}
</div>
<div class="mermaid" style="width: 100%; height: 100%;"> <div class="mermaid" style="width: 100%; height: 100%;">
stateDiagram-v2 stateDiagram-v2
State1: The state with a note [*] --> First
note right of State1 First --> Second
Important information! You can write First --> Third
notes.
end note state First {
State1 --> State2 [*] --> fir
note left of State2 : This is the note to the left. fir --> [*]
}
state Second {
[*] --> sec
sec --> [*]
}
state Third {
[*] --> thi
thi --> [*]
}
</div> </div>
<div class="mermaid2" style="width: 100%; height: 100%;"> <div class="mermaid2" style="width: 100%; height: 100%;">
stateDiagram-v2 stateDiagram-v2

View File

@@ -7,12 +7,10 @@ Explains the representation of various objects used to render the flow charts an
Sample object: Sample object:
```json ```json
{ {
"labelType":"svg",
"labelStyle":"",
"shape":"rect", "shape":"rect",
"label":{},
"labelText":"Test", "labelText":"Test",
"rx":0,"ry":0, "rx":0,
"ry":0,
"class":"default", "class":"default",
"style":"", "style":"",
"id":"Test", "id":"Test",
@@ -24,18 +22,16 @@ This is set by the renderer of the diagram and insert the data that the wrapper
| property | description | | property | description |
| ---------- | ----------------------------------------------------------------------------------------------------------- | | ---------- | ----------------------------------------------------------------------------------------------------------- |
| labelType | If the label should be html label or a svg label. Should we continue to support both? | | labelStyle | Css styles for the label. User for instance for stylling the labels for clusters |
| labelStyle | Css styles for the label. Not currently used. | | shape | The shape of the node. |
| shape | The shape of the node. Currently on rect is suppoerted. This will change. |
| label | ?? |
| labelText | The text on the label | | labelText | The text on the label |
| rx | The corner radius - maybe part of the shape instead? | | rx | The corner radius - maybe part of the shape instead? Used for rects. |
| ry | The corner radius - maybe part of the shape instead? | | ry | The corner radius - maybe part of the shape instead? Used for rects. |
| class | Class to be set for the shape | | classes | Classes to be set for the shape. Not used |
| style | Css styles for the actual shape | | style | Css styles for the actual shape |
| id | id of the shape | | id | id of the shape |
| type | if set to group then this node indicates *a cluster*. | | type | if set to group then this node indicates *a cluster*. |
| padding | Padding. Passed from the renderr as this might differ between react for different diagrams. Maybe obsolete. | | padding | Padding. Passed from the render as this might differ between different diagrams. Maybe obsolete. |
# edge # edge

View File

@@ -1,8 +1,10 @@
const createLabel = (vertexText, style) => { const createLabel = (vertexText, style) => {
const svgLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text'); const svgLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text');
svgLabel.setAttribute('style', style.replace('color:', 'fill:')); svgLabel.setAttribute('style', style.replace('color:', 'fill:'));
let rows = [];
const rows = vertexText.split(/\n|<br\s*\/?>/gi); if (vertexText) {
rows = vertexText.split(/\n|<br\s*\/?>/gi);
}
for (let j = 0; j < rows.length; j++) { for (let j = 0; j < rows.length; j++) {
const tspan = document.createElementNS('http://www.w3.org/2000/svg', 'tspan'); const tspan = document.createElementNS('http://www.w3.org/2000/svg', 'tspan');

View File

@@ -63,34 +63,17 @@ const outsideNode = (node, point) => {
return false; return false;
}; };
// const intersection = (node, outsidePoint, insidePoint) => {
// const x = node.x;
// const y = node.y;
// const dx = Math.abs(x - insidePoint.x);
// const w = node.width / 2;
// let r = w - dx;
// const dy = Math.abs(y - insidePoint.y);
// const h = node.height / 2;
// const q = h - dy;
// const Q = Math.abs(outsidePoint.y - insidePoint.y);
// const R = Math.abs(outsidePoint.x - insidePoint.x);
// r = (R * q) / Q;
// return { x: insidePoint.x + r, y: insidePoint.y + q };
// };
const intersection = (node, outsidePoint, insidePoint) => { const intersection = (node, outsidePoint, insidePoint) => {
// logger.info('intersection', outsidePoint, insidePoint, node); logger.info('intersection o:', outsidePoint, ' i:', insidePoint, node);
const x = node.x; const x = node.x;
const y = node.y; const y = node.y;
const dx = Math.abs(x - insidePoint.x); const dx = Math.abs(x - insidePoint.x);
const w = node.width / 2; const w = node.width / 2;
let r = w - dx; let r = insidePoint.x < outsidePoint.x ? w - dx : w + dx;
const dy = Math.abs(y - insidePoint.y); const dy = Math.abs(y - insidePoint.y);
const h = node.height / 2; const h = node.height / 2;
let q = h - dy; let q = insidePoint.y < outsidePoint.y ? h - dy : h - dy;
const Q = Math.abs(outsidePoint.y - insidePoint.y); const Q = Math.abs(outsidePoint.y - insidePoint.y);
const R = Math.abs(outsidePoint.x - insidePoint.x); const R = Math.abs(outsidePoint.x - insidePoint.x);
@@ -105,9 +88,10 @@ const intersection = (node, outsidePoint, insidePoint) => {
}; };
} else { } else {
q = (Q * r) / R; q = (Q * r) / R;
r = (R * q) / Q;
return { return {
x: insidePoint.x < outsidePoint.x ? insidePoint.x + r : insidePoint.x - r, x: insidePoint.x < outsidePoint.x ? insidePoint.x + r : insidePoint.x + dx - w,
y: insidePoint.y < outsidePoint.y ? insidePoint.y + q : insidePoint.y - q y: insidePoint.y < outsidePoint.y ? insidePoint.y + q : insidePoint.y - q
}; };
} }
@@ -117,8 +101,8 @@ export const insertEdge = function(elem, edge, clusterDb, diagramType) {
logger.info('\n\n\n\n'); logger.info('\n\n\n\n');
let points = edge.points; let points = edge.points;
if (edge.toCluster) { if (edge.toCluster) {
// logger.info('edge', edge); logger.info('edge', edge);
// logger.info('to cluster', clusterDb[edge.toCluster]); logger.info('to cluster', clusterDb[edge.toCluster]);
points = []; points = [];
let lastPointOutside; let lastPointOutside;
let isInside = false; let isInside = false;
@@ -126,13 +110,12 @@ export const insertEdge = function(elem, edge, clusterDb, diagramType) {
const node = clusterDb[edge.toCluster].node; const node = clusterDb[edge.toCluster].node;
if (!outsideNode(node, point) && !isInside) { if (!outsideNode(node, point) && !isInside) {
// logger.info('inside', edge.toCluster, point); logger.info('inside', edge.toCluster, point, lastPointOutside);
// First point inside the rect // First point inside the rect
const insterection = intersection(node, lastPointOutside, point); const insterection = intersection(node, lastPointOutside, point);
// logger.info('intersect', inter.rect(node, lastPointOutside)); logger.info('intersect', insterection);
points.push(insterection); points.push(insterection);
// points.push(insterection);
isInside = true; isInside = true;
} else { } else {
if (!isInside) points.push(point); if (!isInside) points.push(point);
@@ -142,8 +125,8 @@ export const insertEdge = function(elem, edge, clusterDb, diagramType) {
} }
if (edge.fromCluster) { if (edge.fromCluster) {
// logger.info('edge', edge); logger.info('edge', edge);
// logger.info('from cluster', clusterDb[edge.toCluster]); logger.info('from cluster', clusterDb[edge.toCluster]);
const updatedPoints = []; const updatedPoints = [];
let lastPointOutside; let lastPointOutside;
let isInside = false; let isInside = false;
@@ -152,7 +135,7 @@ export const insertEdge = function(elem, edge, clusterDb, diagramType) {
const node = clusterDb[edge.fromCluster].node; const node = clusterDb[edge.fromCluster].node;
if (!outsideNode(node, point) && !isInside) { if (!outsideNode(node, point) && !isInside) {
// logger.info('inside', edge.toCluster, point); logger.info('inside', edge.toCluster, point);
// First point inside the rect // First point inside the rect
const insterection = intersection(node, lastPointOutside, point); const insterection = intersection(node, lastPointOutside, point);
@@ -162,7 +145,7 @@ export const insertEdge = function(elem, edge, clusterDb, diagramType) {
isInside = true; isInside = true;
} else { } else {
// at the outside // at the outside
// logger.info('Outside point', point); logger.info('Outside point', point);
if (!isInside) updatedPoints.unshift(point); if (!isInside) updatedPoints.unshift(point);
} }
lastPointOutside = point; lastPointOutside = point;
@@ -170,10 +153,6 @@ export const insertEdge = function(elem, edge, clusterDb, diagramType) {
points = updatedPoints; points = updatedPoints;
} }
// logger.info('Poibts', points);
// logger.info('Edge', edge);
// The data for our line // The data for our line
const lineData = points.filter(p => !Number.isNaN(p.y)); const lineData = points.filter(p => !Number.isNaN(p.y));

View File

@@ -7,8 +7,28 @@ import { logger } from '../logger';
let clusterDb = {}; let clusterDb = {};
const translateClusterId = id => { const getAnchorId = (id, graph, nodes) => {
if (clusterDb[id]) return clusterDb[id].id; // Only insert an achor once
if (clusterDb[id]) {
// if (!clusterDb[id].inserted) {
// // Create anchor node for cluster
// const anchorData = {
// shape: 'start',
// labelText: '',
// classes: '',
// style: '',
// id: id + '_anchor',
// type: 'anchor',
// padding: 0
// };
// insertNode(nodes, anchorData);
// graph.setNode(anchorData.id, anchorData);
// graph.setParent(anchorData.id, id);
// clusterDb[id].inserted = true;
// }
return clusterDb[id].id;
}
return id; return id;
}; };
@@ -24,24 +44,24 @@ export const render = (elem, graph, markers, diagramtype, id) => {
const edgeLabels = elem.insert('g').attr('class', 'edgeLabels'); const edgeLabels = elem.insert('g').attr('class', 'edgeLabels');
const nodes = elem.insert('g').attr('class', 'nodes'); const nodes = elem.insert('g').attr('class', 'nodes');
logger.warn('graph', graph);
// Insert nodes, this will insert them into the dom and each node will get a size. The size is updated // Insert nodes, this will insert them into the dom and each node will get a size. The size is updated
// to the abstract node and is later used by dagre for the layout // to the abstract node and is later used by dagre for the layout
graph.nodes().forEach(function(v) { graph.nodes().forEach(function(v) {
const node = graph.node(v); const node = graph.node(v);
logger.warn('Node ' + v + ': ' + JSON.stringify(graph.node(v))); logger.info('Node ' + v + ': ' + JSON.stringify(graph.node(v)));
if (node.type !== 'group') { if (node.type !== 'group') {
insertNode(nodes, graph.node(v)); insertNode(nodes, graph.node(v));
} else { } else {
// const width = getClusterTitleWidth(clusters, node); // const width = getClusterTitleWidth(clusters, node);
const children = graph.children(v); const children = graph.children(v);
logger.info('Cluster identified', node.id, children[0]); logger.info('Cluster identified', node.id, children[0]);
// nodes2expand.push({ id: children[0], width }); // nodes2expand.push({ id: children[0], width });
clusterDb[node.id] = { id: children[0] }; clusterDb[node.id] = { id: children[0] };
logger.info('Clusters ', clusterDb); // clusterDb[node.id] = { id: node.id + '_anchor' };
} }
}); });
logger.info('Clusters ', clusterDb);
// Insert labels, this will insert them into the dom so that the width can be calculated // Insert labels, this will insert them into the dom so that the width can be calculated
// Also figure out which edges point to/from clusters and adjust them accordingly // Also figure out which edges point to/from clusters and adjust them accordingly
@@ -49,21 +69,37 @@ export const render = (elem, graph, markers, diagramtype, id) => {
// TODO: pick optimal child in the cluster to us as link anchor // TODO: pick optimal child in the cluster to us as link anchor
graph.edges().forEach(function(e) { graph.edges().forEach(function(e) {
const edge = graph.edge(e); const edge = graph.edge(e);
logger.warn('Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(e)); logger.trace('Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(e));
// logger.info('Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(graph.edge(e))); logger.info('Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(graph.edge(e)));
const v = translateClusterId(e.v);
const w = translateClusterId(e.w); let v = e.v;
if (v !== e.v || w !== e.w) { let w = e.w;
// Check if link is either from or to a cluster
logger.info(
'Fix',
clusterDb,
'ids:',
e.v,
e.w,
'Translateing: ',
clusterDb[e.v],
clusterDb[e.w]
);
if (clusterDb[e.v] || clusterDb[e.w]) {
logger.info('Fixing and trixing - rwemoving', e.v, e.w, e.name);
v = getAnchorId(e.v, graph, nodes);
w = getAnchorId(e.w, graph, nodes);
graph.removeEdge(e.v, e.w, e.name); graph.removeEdge(e.v, e.w, e.name);
if (v !== e.v) edge.fromCluster = e.v; if (v !== e.v) edge.fromCluster = e.v;
if (w !== e.w) edge.toCluster = e.w; if (w !== e.w) edge.toCluster = e.w;
logger.info('Fixing Replacing with', v, w, e.name);
graph.setEdge(v, w, edge, e.name); graph.setEdge(v, w, edge, e.name);
} }
insertEdgeLabel(edgeLabels, edge); insertEdgeLabel(edgeLabels, edge);
}); });
graph.edges().forEach(function(e) { graph.edges().forEach(function(e) {
logger.info('Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(e)); logger.trace('Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(e));
}); });
logger.info('#############################################'); logger.info('#############################################');
logger.info('### Layout ###'); logger.info('### Layout ###');
@@ -74,7 +110,7 @@ export const render = (elem, graph, markers, diagramtype, id) => {
// Move the nodes to the correct place // Move the nodes to the correct place
graph.nodes().forEach(function(v) { graph.nodes().forEach(function(v) {
const node = graph.node(v); const node = graph.node(v);
logger.info('Node ' + v + ': ' + JSON.stringify(graph.node(v))); logger.trace('Node ' + v + ': ' + JSON.stringify(graph.node(v)));
if (node.type !== 'group') { if (node.type !== 'group') {
positionNode(node); positionNode(node);
} else { } else {
@@ -86,7 +122,7 @@ export const render = (elem, graph, markers, diagramtype, id) => {
// Move the edge labels to the correct place after layout // Move the edge labels to the correct place after layout
graph.edges().forEach(function(e) { graph.edges().forEach(function(e) {
const edge = graph.edge(e); const edge = graph.edge(e);
logger.info('Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(edge), edge); logger.trace('Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(edge), edge);
insertEdge(edgePaths, edge, clusterDb, diagramtype); insertEdge(edgePaths, edge, clusterDb, diagramtype);
positionEdgeLabel(edge); positionEdgeLabel(edge);

View File

@@ -130,10 +130,8 @@ export const addVertices = function(vert, g, svgId) {
} }
// Add the node // Add the node
g.setNode(vertex.id, { g.setNode(vertex.id, {
labelType: 'svg',
labelStyle: styles.labelStyle, labelStyle: styles.labelStyle,
shape: _shape, shape: _shape,
label: vertexNode,
labelText: vertexText, labelText: vertexText,
rx: radious, rx: radious,
ry: radious, ry: radious,
@@ -146,10 +144,8 @@ export const addVertices = function(vert, g, svgId) {
}); });
logger.info('setNode', { logger.info('setNode', {
labelType: 'svg',
labelStyle: styles.labelStyle, labelStyle: styles.labelStyle,
shape: _shape, shape: _shape,
label: vertexNode,
labelText: vertexText, labelText: vertexText,
rx: radious, rx: radious,
ry: radious, ry: radious,

View File

@@ -1,4 +1,7 @@
import { logger } from '../../logger'; import { logger } from '../../logger';
import { generateId } from '../../utils';
const clone = o => JSON.parse(JSON.stringify(o));
let rootDoc = []; let rootDoc = [];
const setRootDoc = o => { const setRootDoc = o => {
@@ -22,6 +25,34 @@ const docTranslator = (parent, node, first) => {
} }
if (node.doc) { if (node.doc) {
const doc = [];
// Check for concurrency
let i = 0;
let currentDoc = [];
for (i = 0; i < node.doc.length; i++) {
if (node.doc[i].type === 'divider') {
// debugger;
const newNode = clone(node.doc[i]);
newNode.doc = clone(currentDoc);
doc.push(newNode);
currentDoc = [];
} else {
currentDoc.push(node.doc[i]);
}
}
// If any divider was encountered
if (doc.length > 0 && currentDoc.length > 0) {
const newNode = {
stmt: 'state',
id: generateId(),
type: 'divider',
doc: clone(currentDoc)
};
doc.push(clone(newNode));
node.doc = doc;
}
node.doc.forEach(docNode => docTranslator(node, docNode, true)); node.doc.forEach(docNode => docTranslator(node, docNode, true));
} }
} }
@@ -31,8 +62,14 @@ const getRootDocV2 = () => {
return { id: 'root', doc: rootDoc }; return { id: 'root', doc: rootDoc };
}; };
const extract = doc => { const extract = _doc => {
// const res = { states: [], relations: [] }; // const res = { states: [], relations: [] };
let doc;
if (_doc.doc) {
doc = _doc.doc;
} else {
doc = _doc;
}
// let doc = root.doc; // let doc = root.doc;
// if (!doc) { // if (!doc) {
// doc = root; // doc = root;
@@ -40,6 +77,8 @@ const extract = doc => {
logger.info(doc); logger.info(doc);
clear(); clear();
logger.info('Extract', doc);
doc.forEach(item => { doc.forEach(item => {
if (item.stmt === 'state') { if (item.stmt === 'state') {
addState(item.id, item.type, item.doc, item.description, item.note); addState(item.id, item.type, item.doc, item.description, item.note);

View File

@@ -72,10 +72,8 @@ const setupNode = (g, parent, node, altFlag) => {
} }
const nodeData = { const nodeData = {
labelType: 'svg',
labelStyle: '', labelStyle: '',
shape: nodeDb[node.id].shape, shape: nodeDb[node.id].shape,
label: node.id,
labelText: nodeDb[node.id].description, labelText: nodeDb[node.id].description,
classes: nodeDb[node.id].classes, //classStr, classes: nodeDb[node.id].classes, //classStr,
style: '', //styles.style, style: '', //styles.style,
@@ -87,10 +85,8 @@ const setupNode = (g, parent, node, altFlag) => {
if (node.note) { if (node.note) {
// Todo: set random id // Todo: set random id
const noteData = { const noteData = {
labelType: 'svg',
labelStyle: '', labelStyle: '',
shape: 'note', shape: 'note',
label: node.id,
labelText: node.note.text, labelText: node.note.text,
classes: 'statediagram-note', //classStr, classes: 'statediagram-note', //classStr,
style: '', //styles.style, style: '', //styles.style,
@@ -99,10 +95,8 @@ const setupNode = (g, parent, node, altFlag) => {
padding: 15 //getConfig().flowchart.padding padding: 15 //getConfig().flowchart.padding
}; };
const groupData = { const groupData = {
labelType: 'svg',
labelStyle: '', labelStyle: '',
shape: 'noteGroup', shape: 'noteGroup',
label: node.id + '----parent',
labelText: node.note.text, labelText: node.note.text,
classes: nodeDb[node.id].classes, //classStr, classes: nodeDb[node.id].classes, //classStr,
style: '', //styles.style, style: '', //styles.style,
@@ -133,8 +127,7 @@ const setupNode = (g, parent, node, altFlag) => {
classes: 'note-edge', classes: 'note-edge',
arrowheadStyle: 'fill: #333', arrowheadStyle: 'fill: #333',
labelpos: 'c', labelpos: 'c',
labelType: 'text', labelType: 'text'
label: ''
}); });
} else { } else {
g.setNode(node.id, nodeData); g.setNode(node.id, nodeData);
@@ -143,12 +136,12 @@ const setupNode = (g, parent, node, altFlag) => {
if (parent) { if (parent) {
if (parent.id !== 'root') { if (parent.id !== 'root') {
logger.trace('Setting node ', node.id, ' to be child of its parent ', parent.id); logger.info('Setting node ', node.id, ' to be child of its parent ', parent.id);
g.setParent(node.id, parent.id); g.setParent(node.id, parent.id);
} }
} }
if (node.doc) { if (node.doc) {
logger.trace('Adding nodes children '); logger.info('Adding nodes children ');
setupDoc(g, node, node.doc, !altFlag); setupDoc(g, node, node.doc, !altFlag);
} }
}; };
@@ -168,8 +161,7 @@ const setupDoc = (g, parent, doc, altFlag) => {
labelStyle: '', labelStyle: '',
arrowheadStyle: 'fill: #333', arrowheadStyle: 'fill: #333',
labelpos: 'c', labelpos: 'c',
labelType: 'text', labelType: 'text'
label: ''
}; };
let startId = item.state1.id; let startId = item.state1.id;
let endId = item.state2.id; let endId = item.state2.id;
@@ -214,7 +206,7 @@ export const draw = function(text, id) {
compound: true compound: true
}) })
.setGraph({ .setGraph({
rankdir: 'LR', rankdir: 'TB',
nodesep: nodeSpacing, nodesep: nodeSpacing,
ranksep: rankSpacing, ranksep: rankSpacing,
marginx: 8, marginx: 8,
@@ -224,8 +216,8 @@ export const draw = function(text, id) {
return {}; return {};
}); });
// logger.info(stateDb.getRootDoc()); logger.info(stateDb.getRootDocV2());
stateDb.extract(stateDb.getRootDocV2().doc); stateDb.extract(stateDb.getRootDocV2());
logger.info(stateDb.getRootDocV2()); logger.info(stateDb.getRootDocV2());
setupNode(g, undefined, stateDb.getRootDocV2(), true); setupNode(g, undefined, stateDb.getRootDocV2(), true);

View File

@@ -210,6 +210,19 @@ export const getStylesFromArray = arr => {
return { style: style, labelStyle: labelStyle }; return { style: style, labelStyle: labelStyle };
}; };
let cnt = 0;
export const generateId = () => {
cnt++;
return (
'id-' +
Math.random()
.toString(36)
.substr(2, 12) +
'-' +
cnt
);
};
export default { export default {
detectType, detectType,
isSubstringInArray, isSubstringInArray,
@@ -217,5 +230,6 @@ export default {
calcLabelPosition, calcLabelPosition,
calcCardinalityPosition, calcCardinalityPosition,
formatUrl, formatUrl,
getStylesFromArray getStylesFromArray,
generateId
}; };