From dabb4e25cca99e6c3a666c9351b1013dab0014e0 Mon Sep 17 00:00:00 2001 From: Klemek Date: Tue, 16 Jul 2019 16:39:47 +0200 Subject: [PATCH] [skip CI] WIP node sub text --- README.md | 8 +++- build_preview.js | 10 ++++- src/rendering.js | 111 ++++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 118 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 6f12dba..6c21647 100644 --- a/README.md +++ b/README.md @@ -175,7 +175,7 @@ Will produce the following diagram: | `placing.max-link-length` | 3 | maximum stretching of links between nodes | no | | `placing.diagonals` | `true` | allow diagonal links to be made | no | | `rendering.beautify` | `false` | output a readable SVG file | no | -| `rendering.scale` | 128 | (in pixels) final icons size | no | +| `rendering.scale` | 256 | (in pixels) final icons size | no | | `rendering.color` | `black` | color of all elements | no | | `rendering.h-spacing` | 1.3 | how width is stretched comparing to height | no | | `rendering.icons.scale` | 1 | default scaling of icons | in node or sub-icon | @@ -184,9 +184,11 @@ Will produce the following diagram: | `rendering.links.color` | `''` | color of all links (might be redefined in link definition) | in link | | `rendering.links.size` | 0 | forced size/length of the links (0 means it will be computed from the distance between the nodes) | in link | | `rendering.texts.font` | `'sans-serif'` | font family of the texts (might be redefined in sub-elements definition) | in text | -| `rendering.texts.font-size` | 20 | font size of the texts | in sub-text | +| `rendering.texts.font-size` | 12 | font size of the texts | in sub-text | | `rendering.texts.font-style` | `'normal'` | font style of the texts (see [Font styles](#font-styles)) | in sub-text | | `rendering.texts.color` | `''` | color of all texts | in sub-text | +| `rendering.texts.margin` | 0.35 | margin between texts and elements | in sub-text | +| `rendering.texts.line-height` | 1.2 | height of each line in font size | in sub-text | ### `nodes` @@ -231,6 +233,8 @@ You can **just enter a string** to be considered a text but you can define a tex | `font` | string | no | redefine the font family | | `font-size` | number | no | redefine the font size | | `font-style` | string | no | redefine the font style (see [Font styles](#font-styles)) | +| `margin` | number | no | redefine the margin with the parent element | +| `line-height` | number | no | height of each line in font size | ### Icons diff --git a/build_preview.js b/build_preview.js index 8b64edd..9a878a4 100644 --- a/build_preview.js +++ b/build_preview.js @@ -40,6 +40,7 @@ const faDiagrams = require('./src/index'); const data = { options: { rendering: { + beautify: true, icons: { color: '#4E342E' } @@ -48,12 +49,16 @@ const data = { nodes: [ { name: 'node1', - icon: 'server', + icon: 'laptop-code', + bottom: 'my app', + top:'my app', + left:'my\napp' }, { name: 'node2', icon: 'globe', color: '#455A64', + bottom: 'world' } ], links: [ @@ -61,10 +66,11 @@ const data = { from: 'node1', to: 'node2', color: '#333333', + bottom: 'hello' } ] }; -fs.writeFileSync('docs/sample.json', JSON.stringify(data), {encoding: 'utf-8'}); +fs.writeFileSync('docs/sample.json', JSON.stringify(data, null, 4), {encoding: 'utf-8'}); fs.writeFileSync('preview/sample.svg', faDiagrams.compute(data), {encoding: 'utf-8'}); \ No newline at end of file diff --git a/src/rendering.js b/src/rendering.js index 9a82f79..2217cec 100644 --- a/src/rendering.js +++ b/src/rendering.js @@ -26,6 +26,10 @@ try { * @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 */ /** @@ -33,6 +37,8 @@ try { * @property {string} from * @property {string} to * @property {string|undefined} type + * @property {Object|undefined} bottom + * @property {Object|undefined} top */ const SUB_DEF = { @@ -48,7 +54,9 @@ const SUB_DEF = { 'font': 'string', 'font-size': 'number', 'font-style': 'string', - 'scale': 'number', + 'margin': 'number', + 'line-height': 'number', + 'scale': 'number' }; const NODE_DEF = { @@ -82,7 +90,7 @@ const LINK_DEF = { const DEFAULT_OPTIONS = { 'beautify': false, - 'scale': 128, + 'scale': 256, 'h-spacing': 1.3, 'color': 'black', 'icons': { @@ -96,9 +104,11 @@ const DEFAULT_OPTIONS = { }, 'texts': { 'font': 'sans-serif', - 'font-size': '20', + 'font-size': 12, 'font-style': 'normal', - 'color': '' + 'color': '', + 'margin': 0.2, + 'line-height': 1.2 } }; @@ -225,6 +235,30 @@ module.exports = (options) => { return {w: maxX + 1, h: maxY + 1}; }, + /** + * @param {string} text + * @param {number} lineHeight + * @param {string} anchor + * @return {Object} + */ + getSvgText: (text, lineHeight, anchor) => { + text = text.trim(); + if (!text.includes('\n')) + return {'_text': text}; + const list = []; + text.split('\n').map(t => t.trim()).forEach(line => { + list.push({ + '_attributes': { + 'x': 0, + 'dy': `${lineHeight}em`, + 'text-anchor': anchor + }, + '_text': line + }); + }); + return {'tspan': list}; + }, + /** * @param {Node2} node */ @@ -233,11 +267,11 @@ module.exports = (options) => { if (!icon) return null; const scale = (node['scale'] || options['icons']['scale']) * DEFAULT_SCALE; - return { + const g = { '_attributes': { 'transform': `translate(${(node.x + 0.5) * options['h-spacing']} ${node.y + 0.5})`, }, - 'g': { + 'g': [{ '_attributes': { 'transform': `scale(${scale / icon.height} ${scale / icon.height}) translate(${-icon.width / 2} ${-icon.height / 2})`, 'stroke': (node['color'] || options['icons']['color'] || undefined), @@ -248,8 +282,59 @@ module.exports = (options) => { 'd': icon.path, } } - } + }] }; + + ['bottom', 'top', 'left', 'right'].forEach(side => { + const subE = node[side]; + if (subE && subE.text) { + const fontSize = subE['font-size'] || options['texts']['font-size']; + 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 text = self.getSvgText(subE.text, subE['line-height'] || options['texts']['line-height'], anchor); + + const textHeight = text['tspan'] ? text['tspan'].length : 0; + + text['_attributes'] = { + 'font-family': subE['font'] || options['texts']['font'], + 'font-size': fontSize, + 'text-anchor': anchor, + 'x': pos.x * fontSize, + 'y': (pos.y + 0.25) * fontSize - textHeight * fontSize + }; + + g['g'].push({ + '_attributes': { + 'transform': `translate(${pos.x * margin} ${pos.y * margin}) scale(${1 / (options['scale'] * DEFAULT_SCALE)} ${1 / (options['scale'] * DEFAULT_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; }, /** @@ -343,6 +428,12 @@ module.exports = (options) => { 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); @@ -352,6 +443,12 @@ module.exports = (options) => { const res = utils.isValid(link, LINK_DEF); if (res) throw `Link ${i} (${link.from}->${link.to}) is invalid at key '${res}'`; + + ['bottom', 'top'].forEach(sub => { + if (typeof link[sub] === 'string') + link[sub] = {text: link[sub]}; + }); + const group = self.renderLink(nodes, link); if (group) data['g'].push(group);