links sub-text

This commit is contained in:
Klemek
2019-07-17 11:14:40 +02:00
parent dd94e0b0c4
commit a5da517317
3 changed files with 156 additions and 108 deletions
+2 -2
View File
@@ -114,7 +114,7 @@ const data = {
from: 'node1', from: 'node1',
to: 'node2', to: 'node2',
color: '#333333', color: '#333333',
bottom: 'hello' bottom: '"hello"'
} }
] ]
}; };
@@ -209,7 +209,7 @@ Array of object as following:
| **`from`** | string | **yes** | source node name | | **`from`** | string | **yes** | source node name |
| **`to`** | string | **yes** | destination node name | | **`to`** | string | **yes** | destination node name |
| `type` | string | no | link's appearance (see [Link types](#link-types)) | | `type` | string | no | link's appearance (see [Link types](#link-types)) |
| `top`, `bottom` | string or object | no | see [Sub-elements](#sub-elements) | | `top`, `bottom` **or** `left`, `right` | string or object | no | left and right are relative of the link's direction, top and bottom relative of the link's angle, see [Sub-elements](#sub-elements) |
| `color` | string | no | redefine the color | | `color` | string | no | redefine the color |
| `scale` | number | no | redefine this link scale | | `scale` | number | no | redefine this link scale |
| `size` | number | no | forced size/length of the link | | `size` | number | no | forced size/length of the link |
+1 -1
View File
@@ -3,7 +3,7 @@ const rendering = require('./rendering');
const self = { const self = {
/** /**
* @param {{options: Object|undefined, nodes: Object[]|undefined, links: Object[]|undefined}} data * @param {{options: Object?, nodes: Object[]?, links: Object[]?}} data
* @returns {string} * @returns {string}
*/ */
compute: (data) => { compute: (data) => {
+153 -105
View File
@@ -20,27 +20,19 @@ try {
console.error('fa-diagrams: SVG resources could not be loaded: ' + err); console.error('fa-diagrams: SVG resources could not be loaded: ' + err);
} }
/**
* @typedef Node2
* @property {string} name
* @property {number} x
* @property {number} y
* @property {string|{path:string,width:number:height:number}} icon
* @property {Object|undefined} bottom
* @property {Object|undefined} top
* @property {Object|undefined} left
* @property {Object|undefined} right
*/
/** /**
* @typedef Link2 * @typedef SubElement2
* @property {string} from * @property {string|undefined} text
* @property {string} to * @property {string|{path:string,width:number:height:number}|undefined} icon
* @property {string|undefined} type * @property {string|undefined} color
* @property {Object|undefined} bottom * @property {string|undefined} font
* @property {Object|undefined} top * @property {string|undefined} font-size
* @property {string|undefined} font-style
* @property {number|undefined} margin
* @property {number|undefined} line-height
* @property {number|undefined} scale
*/ */
const SUB_DEF = { const SUB_DEF = {
'_': 'string', '_': 'string',
'text': 'string', 'text': 'string',
@@ -59,6 +51,17 @@ const SUB_DEF = {
'scale': 'number' 'scale': 'number'
}; };
/**
* @typedef Node2
* @property {string} name
* @property {number} x
* @property {number} y
* @property {string|{path:string,width:number:height:number}} icon
* @property {SubElement2|undefined} bottom
* @property {SubElement2|undefined} top
* @property {SubElement2|undefined} left
* @property {SubElement2|undefined} right
*/
const NODE_DEF = { const NODE_DEF = {
'name': '!string', 'name': '!string',
'icon': { 'icon': {
@@ -77,6 +80,16 @@ const NODE_DEF = {
'right': SUB_DEF 'right': SUB_DEF
}; };
/**
* @typedef Link2
* @property {string} from
* @property {string} to
* @property {string|undefined} type
* @property {SubElement2|undefined} bottom
* @property {SubElement2|undefined} top
* @property {SubElement2|undefined} left
* @property {SubElement2|undefined} right
*/
const LINK_DEF = { const LINK_DEF = {
'from': '!string', 'from': '!string',
'to': '!string', 'to': '!string',
@@ -240,7 +253,7 @@ module.exports = (options) => {
* @param {number} lineHeight * @param {number} lineHeight
* @param {number} x * @param {number} x
* @param {string} anchor * @param {string} anchor
* @return {Object} * @return {Object} svg text
*/ */
getSvgText: (text, lineHeight, x, anchor) => { getSvgText: (text, lineHeight, x, anchor) => {
text = text.trim(); text = text.trim();
@@ -260,22 +273,71 @@ module.exports = (options) => {
return {'tspan': list}; return {'tspan': list};
}, },
/**
* @param {Node2|Link2} element
* @param {string} side
* @param {SubElement2} subE
* @param {boolean?} reverse
* @param {boolean?} link
* @returns {Object} svg group
*/
renderSubText: (element, side, subE, reverse, link) => {
const fontSize = subE['font-size'] || options['texts']['font-size'];
const margin = (subE['margin'] || options['texts']['margin']) / (link ? 4 : 1);
let pos;
let anchor;
switch (side) {
case 'bottom':
pos = {x: 0, y: 1};
anchor = 'middle';
break;
case 'top':
pos = {x: 0, y: -1};
anchor = 'middle';
break;
case 'left':
pos = {x: -1, y: 0};
anchor = 'end';
break;
case 'right':
pos = {x: 1, y: 0};
break;
}
const lineHeight = subE['line-height'] || options['texts']['line-height'];
const text = self.getSvgText(subE.text, lineHeight, pos.x * fontSize / 2, anchor);
const textHeight = text['tspan'] ? text['tspan'].length - 1 : 0;
text['_attributes'] = {
'font-family': subE['font'],
'font-size': subE['font-size'],
'text-anchor': anchor,
'x': pos.x * fontSize / 2,
'y': (pos.y + 0.25) * fontSize - (1 - pos.y) * textHeight * fontSize * lineHeight / 2
};
return {
'_attributes': {
'transform': `${reverse ? 'rotate(180) ' : ''}translate(${pos.x * margin} ${pos.y * margin}) scale(${1 / options['scale']} ${1 / options['scale']})`,
'fill': (subE['color'] || element['color'] || options['texts']['color'] || options[link ? 'links' : 'icons']['color'] || undefined),
},
'text': text
};
},
/** /**
* @param {Node2} node * @param {Node2} node
* @return {Object} svg group
*/ */
renderNode: (node) => { renderNode: (node) => {
const groups = [];
const icon = self.getIcon(node.icon); const icon = self.getIcon(node.icon);
if (!icon) if (icon) {
return null; const scale = (node['scale'] || options['icons']['scale']) * DEFAULT_SCALE;
const scale = (node['scale'] || options['icons']['scale']) * DEFAULT_SCALE; groups.push({
const g = {
'_attributes': {
'transform': `translate(${(node.x + 0.5) * options['h-spacing']} ${node.y + 0.5})`,
},
'g': [{
'_attributes': { '_attributes': {
'transform': `scale(${scale / icon.height} ${scale / icon.height}) translate(${-icon.width / 2} ${-icon.height / 2})`, 'transform': `scale(${scale / icon.height} ${scale / icon.height}) translate(${-icon.width / 2} ${-icon.height / 2})`,
'stroke': (node['color'] || options['icons']['color'] || undefined),
'fill': (node['color'] || options['icons']['color'] || undefined) 'fill': (node['color'] || options['icons']['color'] || undefined)
}, },
'path': { 'path': {
@@ -283,64 +345,27 @@ module.exports = (options) => {
'd': icon.path, 'd': icon.path,
} }
} }
}] });
}; }
['bottom', 'top', 'left', 'right'].forEach(side => { ['bottom', 'top', 'left', 'right'].forEach(side => {
const subE = node[side]; const subE = node[side];
if (subE && subE.text) { if (subE && subE.text)
const fontSize = subE['font-size'] || options['texts']['font-size']; groups.push(self.renderSubText(node, side, subE));
const margin = subE['margin'] || options['texts']['margin'];
let pos;
let anchor;
switch (side) {
case 'bottom':
pos = {x: 0, y: 1};
anchor = 'middle';
break;
case 'top':
pos = {x: 0, y: -1};
anchor = 'middle';
break;
case 'left':
pos = {x: -1, y: 0};
anchor = 'end';
break;
case 'right':
pos = {x: 1, y: 0};
anchor = 'start';
break;
}
const lineHeight = subE['line-height'] || options['texts']['line-height'];
const text = self.getSvgText(subE.text, lineHeight, pos.x * fontSize / 2, anchor);
const textHeight = text['tspan'] ? text['tspan'].length - 1 : 0;
text['_attributes'] = {
'font-family': subE['font'] || options['texts']['font'],
'font-size': fontSize,
'text-anchor': anchor,
'x': pos.x * fontSize / 2,
'y': (pos.y + 0.25) * fontSize - (1 - pos.y) * textHeight * fontSize * lineHeight / 2
};
g['g'].push({
'_attributes': {
'transform': `translate(${pos.x * margin} ${pos.y * margin}) scale(${1 / options['scale']} ${1 / options['scale']})`,
'stroke': (subE['color'] || node['color'] || options['texts']['color'] || options['icons']['color'] || undefined),
'fill': (subE['color'] || node['color'] || options['texts']['color'] || options['icons']['color'] || undefined)
},
'text': text
});
}
}); });
return g; return !groups.length ? null : {
'_attributes': {
'transform': `translate(${(node.x + 0.5) * options['h-spacing']} ${node.y + 0.5})`,
},
'g': groups
};
}, },
/** /**
* @param {Object<string,Node2>} nodes * @param {Object<string,Node2>} nodes
* @param {Link2} link * @param {Link2} link
* @return {Object} svg group
*/ */
renderLink: (nodes, link) => { renderLink: (nodes, link) => {
const src = nodes[link.from]; const src = nodes[link.from];
@@ -350,8 +375,12 @@ module.exports = (options) => {
const posY = (src.y + dst.y) / 2 + 0.5; const posY = (src.y + dst.y) / 2 + 0.5;
const angle = Math.atan2(dst.y - src.y, (dst.x - src.x) * options['h-spacing']) * 180 / Math.PI; const angle = Math.atan2(dst.y - src.y, (dst.x - src.x) * options['h-spacing']) * 180 / Math.PI;
const scale = (link['scale'] || options['links']['scale']) * DEFAULT_SCALE;
console.log(angle);
const groups = [];
const scale = (link['scale'] || options['links']['scale']) * DEFAULT_SCALE;
let size = link['size'] || options['links']['size']; let size = link['size'] || options['links']['size'];
if (!size) { if (!size) {
@@ -367,17 +396,10 @@ module.exports = (options) => {
const path = self.getLinkPath(link.type, size); const path = self.getLinkPath(link.type, size);
if (!path) if (path) {
return null; groups.push({
return {
'_attributes': {
'transform': `translate(${posX} ${posY}) rotate(${angle})`
},
'g': {
'_attributes': { '_attributes': {
'transform': `scale(${scale / 512} ${scale / 512}) translate(${(-256 * size)} ${-256})`, 'transform': `scale(${scale / 512} ${scale / 512}) translate(${(-256 * size)} ${-256})`,
'stroke': (link['color'] || options['links']['color'] || undefined),
'fill': (link['color'] || options['links']['color'] || undefined) 'fill': (link['color'] || options['links']['color'] || undefined)
}, },
'path': { 'path': {
@@ -385,7 +407,30 @@ module.exports = (options) => {
'd': path 'd': path
} }
} }
} });
}
const reverse = Math.abs(angle) > 90;
if (!reverse) {
link.top = link.top || link.left;
link.bottom = link.bottom || link.right;
} else {
link.top = link.top || link.right;
link.bottom = link.bottom || link.left;
}
['bottom', 'top'].forEach(side => {
const subE = link[side];
if (subE && subE.text)
groups.push(self.renderSubText(link, side, subE, reverse, true));
});
return !groups.length ? null : {
'_attributes': {
'transform': `translate(${posX} ${posY}) rotate(${angle})`
},
'g': groups
}; };
}, },
@@ -393,7 +438,7 @@ module.exports = (options) => {
* Convert xml-js data into correct svg xml string * Convert xml-js data into correct svg xml string
* @param {Object} data * @param {Object} data
* @param {{w:number, h:number}} bounds * @param {{w:number, h:number}} bounds
* @returns {string} * @returns {string} SVG data
*/ */
toXML: (data, bounds) => { toXML: (data, bounds) => {
const xml = { const xml = {
@@ -403,8 +448,10 @@ module.exports = (options) => {
'viewBox': `0 0 ${bounds.w * options['h-spacing']} ${bounds.h}`, 'viewBox': `0 0 ${bounds.w * options['h-spacing']} ${bounds.h}`,
'width': bounds.w * options['h-spacing'] * options['scale'] / DEFAULT_SCALE, 'width': bounds.w * options['h-spacing'] * options['scale'] / DEFAULT_SCALE,
'height': bounds.h * options['scale'] / DEFAULT_SCALE, 'height': bounds.h * options['scale'] / DEFAULT_SCALE,
'stroke': options['color'], 'font-family': options['texts']['font'],
'fill': options['color'] 'font-size': options['texts']['font-size'],
'fill': options['color'],
'stroke-width': 0
} }
} }
}; };
@@ -420,26 +467,12 @@ module.exports = (options) => {
/** /**
* @param {Object<string,Node2>} nodes * @param {Object<string,Node2>} nodes
* @param {Link2[]} links * @param {Link2[]} links
* @returns {string} SVG data
*/ */
compute: (nodes, links) => { compute: (nodes, links) => {
const data = {'g': []}; const data = {'g': []};
Object.keys(nodes).forEach(key => {
const res = utils.isValid(nodes[key], NODE_DEF);
if (res)
throw `Node '${key}' is invalid at key '${res}'`;
['bottom', 'top', 'left', 'right'].forEach(sub => {
if (typeof nodes[key][sub] === 'string')
nodes[key][sub] = {text: nodes[key][sub]};
});
const group = self.renderNode(nodes[key]);
if (group)
data['g'].push(group);
});
links.forEach((link, i) => { links.forEach((link, i) => {
const res = utils.isValid(link, LINK_DEF); const res = utils.isValid(link, LINK_DEF);
if (res) if (res)
@@ -455,6 +488,21 @@ module.exports = (options) => {
data['g'].push(group); data['g'].push(group);
}); });
Object.keys(nodes).forEach(key => {
const res = utils.isValid(nodes[key], NODE_DEF);
if (res)
throw `Node '${key}' is invalid at key '${res}'`;
['bottom', 'top', 'left', 'right'].forEach(sub => {
if (typeof nodes[key][sub] === 'string')
nodes[key][sub] = {text: nodes[key][sub]};
});
const group = self.renderNode(nodes[key]);
if (group)
data['g'].push(group);
});
const bounds = self.getBounds(nodes); const bounds = self.getBounds(nodes);
return self.toXML(data, bounds); return self.toXML(data, bounds);