diff --git a/src/rendering.js b/src/rendering.js index 8ec2a2c..43809f1 100644 --- a/src/rendering.js +++ b/src/rendering.js @@ -110,6 +110,11 @@ const DEFAULT_OPTIONS = { 'scale': 1, 'color': '' }, + 'sub-icons': { + 'scale': 0.4, + 'color': '', + 'margin': 0.3 + }, 'links': { 'scale': 1, 'color': '', @@ -329,14 +334,14 @@ module.exports = (options) => { * Generate a svg group from the given sub-element * @param {Node2|Link2} element * @param {string} side - * @param {SubElement2} subE * @param {boolean?} reverse * @param {boolean?} link - * @returns {Object} svg group + * @returns {Object|null} svg group */ - renderSubText: (element, side, subE, reverse = false, link = false) => { - const fontSize = subE['font-size'] || options['texts']['font-size']; - const margin = (subE['margin'] || options['texts']['margin']) / (link ? 4 : 1); + renderSubElement: (element, side, reverse = false, link = false) => { + if (!element[side]) + return null; + const subE = element[side]; let pos; let anchor; switch (side) { @@ -357,28 +362,50 @@ module.exports = (options) => { break; } - const lineHeight = subE['line-height'] || options['texts']['line-height']; - const text = self.renderSvgText(subE.text, lineHeight, pos.x * fontSize / 2, anchor); - const textHeight = text['tspan'] ? text['tspan'].length - 1 : 0; + if (subE.text) { + const margin = (subE['margin'] || options['texts']['margin']) / (link ? 4 : 1); + const fontSize = subE['font-size'] || options['texts']['font-size']; + const lineHeight = subE['line-height'] || options['texts']['line-height']; + const text = self.renderSvgText(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'], - 'font-weight': self.getFontWeight(subE['font-style'], true), - 'font-style': self.getFontStyle(subE['font-style'], true), - 'text-decoration': self.getTextDecoration(subE['font-style'] || options['texts']['font-style']), - 'text-anchor': anchor, - 'x': pos.x * fontSize / 2, - 'y': (pos.y + 0.25) * fontSize - (1 - pos.y) * textHeight * fontSize * lineHeight / 2 - }; + text['_attributes'] = { + 'font-family': subE['font'], + 'font-size': subE['font-size'], + 'font-weight': self.getFontWeight(subE['font-style'], true), + 'font-style': self.getFontStyle(subE['font-style'], true), + 'text-decoration': self.getTextDecoration(subE['font-style'] || options['texts']['font-style']), + '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 + }; + } else { + const margin = (subE['margin'] || options['sub-icons']['margin']) / (link ? 2 : 1); + const icon = self.getIcon(subE.icon); + if (!icon) + return null; + const scale = (subE['scale'] || options['sub-icons']['scale']) * DEFAULT_SCALE; + return { + '_attributes': { + 'transform': `${reverse ? 'rotate(180) ' : ''}translate(${pos.x * margin} ${pos.y * margin}) scale(${scale / icon.height} ${scale / icon.height}) translate(${-icon.width / 2} ${-icon.height / 2})`, + 'fill': (subE['color'] || element['color'] || options['sub-icons']['color'] || options[link ? 'links' : 'icons']['color'] || undefined), + }, + 'path': { + '_attributes': { + 'd': icon.path, + } + } + }; + } - 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 - }; }, /** @@ -406,9 +433,9 @@ module.exports = (options) => { } ['bottom', 'top', 'left', 'right'].forEach(side => { - const subE = node[side]; - if (subE && subE.text) - groups.push(self.renderSubText(node, side, subE)); + const group = self.renderSubElement(node, side); + if (group) + groups.push(group); }); return !groups.length ? null : { @@ -476,9 +503,9 @@ module.exports = (options) => { } ['bottom', 'top'].forEach(side => { - const subE = link[side]; - if (subE && subE.text) - groups.push(self.renderSubText(link, side, subE, reverse, true)); + const group = self.renderSubElement(link, side, reverse, true); + if (group) + groups.push(group); }); diff --git a/test/rendering.test.js b/test/rendering.test.js index 27074e1..8e9ebff 100644 --- a/test/rendering.test.js +++ b/test/rendering.test.js @@ -284,11 +284,21 @@ describe('font style', () => { }); }); -describe('renderSubText', () => { +describe('renderSubElement/text', () => { + test('no sub element', () => { + const res = rendering({scale: 1}).renderSubElement({ + top: { + text: 'test' + } + }, 'bottom'); + expect(res).toBe(null); + }); test('simple text bottom', () => { - const res = rendering({scale: 1}).renderSubText({}, 'bottom', { - text: 'test' - }); + const res = rendering({scale: 1}).renderSubElement({ + bottom: { + text: 'test' + } + }, 'bottom'); expect(res).toEqual({ '_attributes': { 'transform': `translate(0 0.2) scale(1 1)`, @@ -310,9 +320,11 @@ describe('renderSubText', () => { }); }); test('simple text top', () => { - const res = rendering({scale: 1}).renderSubText({}, 'top', { - text: 'test' - }); + const res = rendering({scale: 1}).renderSubElement({ + top: { + text: 'test' + } + }, 'top'); expect(res).toEqual({ '_attributes': { 'transform': `translate(0 -0.2) scale(1 1)`, @@ -334,9 +346,11 @@ describe('renderSubText', () => { }); }); test('simple text left', () => { - const res = rendering({scale: 1}).renderSubText({}, 'left', { - text: 'test' - }); + const res = rendering({scale: 1}).renderSubElement({ + left: { + text: 'test' + } + }, 'left'); expect(res).toEqual({ '_attributes': { 'transform': `translate(-0.2 0) scale(1 1)`, @@ -358,9 +372,11 @@ describe('renderSubText', () => { }); }); test('simple text right', () => { - const res = rendering({scale: 1}).renderSubText({}, 'right', { - text: 'test' - }); + const res = rendering({scale: 1}).renderSubElement({ + right: { + text: 'test' + } + }, 'right'); expect(res).toEqual({ '_attributes': { 'transform': `translate(0.2 0) scale(1 1)`, @@ -382,9 +398,11 @@ describe('renderSubText', () => { }); }); test('link text', () => { - const res = rendering({scale: 1}).renderSubText({}, 'bottom', { - text: 'test' - }, false, true); + const res = rendering({scale: 1}).renderSubElement({ + bottom: { + text: 'test' + } + }, 'bottom', false, true); expect(res).toEqual({ '_attributes': { 'transform': `translate(0 0.05) scale(1 1)`, @@ -406,9 +424,11 @@ describe('renderSubText', () => { }); }); test('multi-line text', () => { - const res = rendering({scale: 1}).renderSubText({}, 'left', { - text: 'test1\ntest2\ntest3' - }); + const res = rendering({scale: 1}).renderSubElement({ + left: { + text: 'test1\ntest2\ntest3' + } + }, 'left'); expect(res).toEqual({ '_attributes': { 'transform': `translate(-0.2 0) scale(1 1)`, @@ -451,9 +471,11 @@ describe('renderSubText', () => { }); }); test('link text reversed', () => { - const res = rendering({scale: 1}).renderSubText({}, 'bottom', { - text: 'test' - }, true, true); + const res = rendering({scale: 1}).renderSubElement({ + bottom: { + text: 'test' + } + }, 'bottom', true, true); expect(res).toEqual({ '_attributes': { 'transform': `rotate(180) translate(0 0.05) scale(1 1)`, @@ -475,10 +497,12 @@ describe('renderSubText', () => { }); }); test('local font size', () => { - const res = rendering({scale: 1, texts: {'font-size': 20}}).renderSubText({}, 'bottom', { - text: 'test', - 'font-size': 10 - }); + const res = rendering({scale: 1, texts: {'font-size': 20}}).renderSubElement({ + bottom: { + text: 'test', + 'font-size': 10 + } + }, 'bottom'); expect(res).toEqual({ '_attributes': { 'transform': `translate(0 0.2) scale(1 1)`, @@ -500,9 +524,11 @@ describe('renderSubText', () => { }); }); test('global font size', () => { - const res = rendering({scale: 1, texts: {'font-size': 20}}).renderSubText({}, 'bottom', { - text: 'test' - }); + const res = rendering({scale: 1, texts: {'font-size': 20}}).renderSubElement({ + bottom: { + text: 'test' + } + }, 'bottom'); expect(res).toEqual({ '_attributes': { 'transform': `translate(0 0.2) scale(1 1)`, @@ -523,11 +549,13 @@ describe('renderSubText', () => { } }); }); - test('local margin', () => { - const res = rendering({scale: 1, texts: {margin: 0.1}}).renderSubText({}, 'bottom', { - text: 'test', - margin: 0.3 - }); + test('local text margin', () => { + const res = rendering({scale: 1, texts: {margin: 0.1}}).renderSubElement({ + bottom: { + text: 'test', + margin: 0.3 + } + }, 'bottom'); expect(res).toEqual({ '_attributes': { 'transform': `translate(0 0.3) scale(1 1)`, @@ -548,10 +576,12 @@ describe('renderSubText', () => { } }); }); - test('global margin', () => { - const res = rendering({scale: 1, texts: {margin: 0.1}}).renderSubText({}, 'bottom', { - text: 'test' - }); + test('global text margin', () => { + const res = rendering({scale: 1, texts: {margin: 0.1}}).renderSubElement({ + bottom: { + text: 'test' + } + }, 'bottom'); expect(res).toEqual({ '_attributes': { 'transform': `translate(0 0.1) scale(1 1)`, @@ -572,11 +602,13 @@ describe('renderSubText', () => { } }); }); - test('local style', () => { - const res = rendering({scale: 1, texts: {'font-style': 'italic underlined'}}).renderSubText({}, 'bottom', { - text: 'test', - 'font-style': 'bold' - }); + test('local text style', () => { + const res = rendering({scale: 1, texts: {'font-style': 'italic underlined'}}).renderSubElement({ + bottom: { + text: 'test', + 'font-style': 'bold' + } + }, 'bottom'); expect(res).toEqual({ '_attributes': { 'transform': `translate(0 0.2) scale(1 1)`, @@ -597,10 +629,12 @@ describe('renderSubText', () => { } }); }); - test('global style', () => { - const res = rendering({scale: 1, texts: {'font-style': 'bold italic underlined'}}).renderSubText({}, 'bottom', { - text: 'test' - }); + test('global text style', () => { + const res = rendering({scale: 1, texts: {'font-style': 'bold italic underlined'}}).renderSubElement({ + bottom: { + text: 'test' + } + }, 'bottom'); expect(res).toEqual({ '_attributes': { 'transform': `translate(0 0.2) scale(1 1)`, @@ -622,10 +656,12 @@ describe('renderSubText', () => { }); }); test('local color', () => { - const res = rendering({scale: 1, icons: {color: 'red'}, texts: {color: 'black'}}).renderSubText({color: 'green'}, 'bottom', { - text: 'test', - color: 'blue' - }); + const res = rendering({scale: 1, icons: {color: 'red'}, texts: {color: 'black'}}).renderSubElement({ + color: 'green', bottom: { + text: 'test', + color: 'blue' + } + }, 'bottom'); expect(res).toEqual({ '_attributes': { 'transform': `translate(0 0.2) scale(1 1)`, @@ -647,9 +683,11 @@ describe('renderSubText', () => { }); }); test('node/link color', () => { - const res = rendering({scale: 1, icons: {color: 'red'}, texts: {color: 'black'}}).renderSubText({color: 'green'}, 'bottom', { - text: 'test' - }); + const res = rendering({scale: 1, icons: {color: 'red'}, texts: {color: 'black'}}).renderSubElement({ + color: 'green', bottom: { + text: 'test' + } + }, 'bottom'); expect(res).toEqual({ '_attributes': { 'transform': `translate(0 0.2) scale(1 1)`, @@ -671,9 +709,11 @@ describe('renderSubText', () => { }); }); test('global texts color', () => { - const res = rendering({scale: 1, icons: {color: 'red'}, texts: {color: 'black'}}).renderSubText({}, 'bottom', { - text: 'test' - }); + const res = rendering({scale: 1, icons: {color: 'red'}, texts: {color: 'black'}}).renderSubElement({ + bottom: { + text: 'test' + } + }, 'bottom'); expect(res).toEqual({ '_attributes': { 'transform': `translate(0 0.2) scale(1 1)`, @@ -694,10 +734,12 @@ describe('renderSubText', () => { } }); }); - test('global node color', () => { - const res = rendering({scale: 1, icons: {color: 'red'}}).renderSubText({}, 'bottom', { - text: 'test' - }); + test('global icons color', () => { + const res = rendering({scale: 1, icons: {color: 'red'}}).renderSubElement({ + bottom: { + text: 'test' + } + }, 'bottom'); expect(res).toEqual({ '_attributes': { 'transform': `translate(0 0.2) scale(1 1)`, @@ -719,9 +761,11 @@ describe('renderSubText', () => { }); }); test('global link color', () => { - const res = rendering({scale: 1, links: {color: 'red'}}).renderSubText({}, 'bottom', { - text: 'test' - }, false, true); + const res = rendering({scale: 1, links: {color: 'red'}}).renderSubElement({ + bottom: { + text: 'test' + } + }, 'bottom', false, true); expect(res).toEqual({ '_attributes': { 'transform': `translate(0 0.05) scale(1 1)`, @@ -743,10 +787,12 @@ describe('renderSubText', () => { }); }); test('local line-height', () => { - const res = rendering({scale: 1, texts: {'line-height': 1}}).renderSubText({}, 'left', { - text: 'test1\ntest2', - 'line-height': 1.1 - }); + const res = rendering({scale: 1, texts: {'line-height': 1}}).renderSubElement({ + left: { + text: 'test1\ntest2', + 'line-height': 1.1 + } + }, 'left'); expect(res).toEqual({ '_attributes': { 'transform': `translate(-0.2 0) scale(1 1)`, @@ -782,9 +828,11 @@ describe('renderSubText', () => { }); }); test('global line-height', () => { - const res = rendering({scale: 1, texts: {'line-height': 1}}).renderSubText({}, 'left', { - text: 'test1\ntest2' - }); + const res = rendering({scale: 1, texts: {'line-height': 1}}).renderSubElement({ + left: { + text: 'test1\ntest2' + } + }, 'left'); expect(res).toEqual({ '_attributes': { 'transform': `translate(-0.2 0) scale(1 1)`, @@ -821,6 +869,292 @@ describe('renderSubText', () => { }); }); +describe('renderSubElement/icon', () => { + test('no icon', () => { + const res = rendering({scale: 1}).renderSubElement({ + bottom: { + icon: 'dqzfqzf' + } + }, 'bottom'); + expect(res).toBe(null); + }); + test('simple icon bottom', () => { + const res = rendering({scale: 1}).renderSubElement({ + bottom: { + icon: 'circle' + } + }, 'bottom'); + expect(res).toEqual({ + '_attributes': { + 'transform': `translate(0 0.3) scale(0.00031250000000000006 0.00031250000000000006) translate(-256 -256)`, + 'fill': undefined, + }, + 'path': { + '_attributes': { + 'd': solidCirclePath, + } + } + }); + }); + test('simple icon top', () => { + const res = rendering({scale: 1}).renderSubElement({ + top: { + icon: 'circle' + } + }, 'top'); + expect(res).toEqual({ + '_attributes': { + 'transform': `translate(0 -0.3) scale(0.00031250000000000006 0.00031250000000000006) translate(-256 -256)`, + 'fill': undefined, + }, + 'path': { + '_attributes': { + 'd': solidCirclePath, + } + } + }); + }); + test('simple icon left', () => { + const res = rendering({scale: 1}).renderSubElement({ + left: { + icon: 'circle' + } + }, 'left'); + expect(res).toEqual({ + '_attributes': { + 'transform': `translate(-0.3 0) scale(0.00031250000000000006 0.00031250000000000006) translate(-256 -256)`, + 'fill': undefined, + }, + 'path': { + '_attributes': { + 'd': solidCirclePath, + } + } + }); + }); + test('simple icon right', () => { + const res = rendering({scale: 1}).renderSubElement({ + right: { + icon: 'circle' + } + }, 'right'); + expect(res).toEqual({ + '_attributes': { + 'transform': `translate(0.3 0) scale(0.00031250000000000006 0.00031250000000000006) translate(-256 -256)`, + 'fill': undefined, + }, + 'path': { + '_attributes': { + 'd': solidCirclePath, + } + } + }); + }); + test('link icon', () => { + const res = rendering({scale: 1}).renderSubElement({ + bottom: { + icon: 'circle' + } + }, 'bottom', false, true); + expect(res).toEqual({ + '_attributes': { + 'transform': `translate(0 0.15) scale(0.00031250000000000006 0.00031250000000000006) translate(-256 -256)`, + 'fill': undefined, + }, + 'path': { + '_attributes': { + 'd': solidCirclePath, + } + } + }); + }); + test('link icon reversed', () => { + const res = rendering({scale: 1}).renderSubElement({ + bottom: { + icon: 'circle' + } + }, 'bottom', true, true); + expect(res).toEqual({ + '_attributes': { + 'transform': `rotate(180) translate(0 0.15) scale(0.00031250000000000006 0.00031250000000000006) translate(-256 -256)`, + 'fill': undefined, + }, + 'path': { + '_attributes': { + 'd': solidCirclePath, + } + } + }); + }); + test('local color', () => { + const res = rendering({scale: 1, icons: {color: 'red'}, 'sub-icons': {color: 'green'}}).renderSubElement({ + color: 'grey', + bottom: { + icon: 'circle', + color: 'black' + } + }, 'bottom'); + expect(res).toEqual({ + '_attributes': { + 'transform': `translate(0 0.3) scale(0.00031250000000000006 0.00031250000000000006) translate(-256 -256)`, + 'fill': 'black', + }, + 'path': { + '_attributes': { + 'd': solidCirclePath, + } + } + }); + }); + test('node/link color', () => { + const res = rendering({scale: 1, icons: {color: 'red'}, 'sub-icons': {color: 'green'}}).renderSubElement({ + color: 'grey', + bottom: { + icon: 'circle' + } + }, 'bottom'); + expect(res).toEqual({ + '_attributes': { + 'transform': `translate(0 0.3) scale(0.00031250000000000006 0.00031250000000000006) translate(-256 -256)`, + 'fill': 'grey', + }, + 'path': { + '_attributes': { + 'd': solidCirclePath, + } + } + }); + }); + test('global sub-icons color', () => { + const res = rendering({scale: 1, icons: {color: 'red'}, 'sub-icons': {color: 'green'}}).renderSubElement({ + bottom: { + icon: 'circle' + } + }, 'bottom'); + expect(res).toEqual({ + '_attributes': { + 'transform': `translate(0 0.3) scale(0.00031250000000000006 0.00031250000000000006) translate(-256 -256)`, + 'fill': 'green', + }, + 'path': { + '_attributes': { + 'd': solidCirclePath, + } + } + }); + }); + test('global icons color', () => { + const res = rendering({scale: 1, icons: {color: 'red'}}).renderSubElement({ + bottom: { + icon: 'circle' + } + }, 'bottom'); + expect(res).toEqual({ + '_attributes': { + 'transform': `translate(0 0.3) scale(0.00031250000000000006 0.00031250000000000006) translate(-256 -256)`, + 'fill': 'red', + }, + 'path': { + '_attributes': { + 'd': solidCirclePath, + } + } + }); + }); + test('global link color', () => { + const res = rendering({scale: 1, links: {color: 'red'}}).renderSubElement({ + bottom: { + icon: 'circle' + } + }, 'bottom', false, true); + expect(res).toEqual({ + '_attributes': { + 'transform': `translate(0 0.15) scale(0.00031250000000000006 0.00031250000000000006) translate(-256 -256)`, + 'fill': 'red', + }, + 'path': { + '_attributes': { + 'd': solidCirclePath, + } + } + }); + }); + test('global scale', () => { + const res = rendering({scale: 1, 'sub-icons': {scale: 512 / 0.4}}).renderSubElement({ + bottom: { + icon: 'circle' + } + }, 'bottom'); + expect(res).toEqual({ + '_attributes': { + 'transform': `translate(0 0.3) scale(1 1) translate(-256 -256)`, + 'fill': undefined, + }, + 'path': { + '_attributes': { + 'd': solidCirclePath, + } + } + }); + }); + test('local scale', () => { + const res = rendering({scale: 1, 'sub-icons': {scale: 512 / 0.4}}).renderSubElement({ + bottom: { + icon: 'circle', + scale: 2 * 512 / 0.4 + } + }, 'bottom'); + expect(res).toEqual({ + '_attributes': { + 'transform': `translate(0 0.3) scale(2 2) translate(-256 -256)`, + 'fill': undefined, + }, + 'path': { + '_attributes': { + 'd': solidCirclePath, + } + } + }); + }); + test('local margin', () => { + const res = rendering({scale: 1, 'sub-icons': {margin: 0.5}}).renderSubElement({ + bottom: { + icon: 'circle', + margin: 0.1 + } + }, 'bottom'); + expect(res).toEqual({ + '_attributes': { + 'transform': `translate(0 0.1) scale(0.00031250000000000006 0.00031250000000000006) translate(-256 -256)`, + 'fill': undefined, + }, + 'path': { + '_attributes': { + 'd': solidCirclePath, + } + } + }); + }); + test('global margin', () => { + const res = rendering({scale: 1, 'sub-icons': {margin: 0.5}}).renderSubElement({ + bottom: { + icon: 'circle' + } + }, 'bottom'); + expect(res).toEqual({ + '_attributes': { + 'transform': `translate(0 0.5) scale(0.00031250000000000006 0.00031250000000000006) translate(-256 -256)`, + 'fill': undefined, + }, + 'path': { + '_attributes': { + 'd': solidCirclePath, + } + } + }); + }); +}); + describe('renderNode', () => { test('no icon', () => { const res = rendering({'h-spacing': 1}).renderNode({