Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.
 
 
 

748 linhas
18 KiB

  1. import { getBarHeightAndYAttr, truncateString, shortenLargeNumber, getSplineCurvePointsStr } from './draw-utils';
  2. import { getStringWidth, isValidNumber } from './helpers';
  3. import { DOT_OVERLAY_SIZE_INCR, PERCENTAGE_BAR_DEFAULT_DEPTH } from './constants';
  4. import { lightenDarkenColor } from './colors';
  5. export const AXIS_TICK_LENGTH = 6;
  6. const LABEL_MARGIN = 4;
  7. const LABEL_MAX_CHARS = 15;
  8. export const FONT_SIZE = 10;
  9. const BASE_LINE_COLOR = '#E2E6E9';
  10. const FONT_FILL = '#313B44';
  11. function $(expr, con) {
  12. return typeof expr === "string"? (con || document).querySelector(expr) : expr || null;
  13. }
  14. export function createSVG(tag, o) {
  15. var element = document.createElementNS("http://www.w3.org/2000/svg", tag);
  16. for (var i in o) {
  17. var val = o[i];
  18. if (i === "inside") {
  19. $(val).appendChild(element);
  20. }
  21. else if (i === "around") {
  22. var ref = $(val);
  23. ref.parentNode.insertBefore(element, ref);
  24. element.appendChild(ref);
  25. } else if (i === "styles") {
  26. if(typeof val === "object") {
  27. Object.keys(val).map(prop => {
  28. element.style[prop] = val[prop];
  29. });
  30. }
  31. } else {
  32. if(i === "className") { i = "class"; }
  33. if(i === "innerHTML") {
  34. element['textContent'] = val;
  35. } else {
  36. element.setAttribute(i, val);
  37. }
  38. }
  39. }
  40. return element;
  41. }
  42. function renderVerticalGradient(svgDefElem, gradientId) {
  43. return createSVG('linearGradient', {
  44. inside: svgDefElem,
  45. id: gradientId,
  46. x1: 0,
  47. x2: 0,
  48. y1: 0,
  49. y2: 1
  50. });
  51. }
  52. function setGradientStop(gradElem, offset, color, opacity) {
  53. return createSVG('stop', {
  54. 'inside': gradElem,
  55. 'style': `stop-color: ${color}`,
  56. 'offset': offset,
  57. 'stop-opacity': opacity
  58. });
  59. }
  60. export function makeSVGContainer(parent, className, width, height) {
  61. return createSVG('svg', {
  62. className: className,
  63. inside: parent,
  64. width: width,
  65. height: height
  66. });
  67. }
  68. export function makeSVGDefs(svgContainer) {
  69. return createSVG('defs', {
  70. inside: svgContainer,
  71. });
  72. }
  73. export function makeSVGGroup(className, transform='', parent=undefined) {
  74. let args = {
  75. className: className,
  76. transform: transform
  77. };
  78. if(parent) args.inside = parent;
  79. return createSVG('g', args);
  80. }
  81. export function wrapInSVGGroup(elements, className='') {
  82. let g = createSVG('g', {
  83. className: className
  84. });
  85. elements.forEach(e => g.appendChild(e));
  86. return g;
  87. }
  88. export function makePath(pathStr, className='', stroke='none', fill='none', strokeWidth=2) {
  89. return createSVG('path', {
  90. className: className,
  91. d: pathStr,
  92. styles: {
  93. stroke: stroke,
  94. fill: fill,
  95. 'stroke-width': strokeWidth
  96. }
  97. });
  98. }
  99. export function makeArcPathStr(startPosition, endPosition, center, radius, clockWise=1, largeArc=0){
  100. let [arcStartX, arcStartY] = [center.x + startPosition.x, center.y + startPosition.y];
  101. let [arcEndX, arcEndY] = [center.x + endPosition.x, center.y + endPosition.y];
  102. return `M${center.x} ${center.y}
  103. L${arcStartX} ${arcStartY}
  104. A ${radius} ${radius} 0 ${largeArc} ${clockWise ? 1 : 0}
  105. ${arcEndX} ${arcEndY} z`;
  106. }
  107. export function makeCircleStr(startPosition, endPosition, center, radius, clockWise=1, largeArc=0){
  108. let [arcStartX, arcStartY] = [center.x + startPosition.x, center.y + startPosition.y];
  109. let [arcEndX, midArc, arcEndY] = [center.x + endPosition.x, center.y * 2, center.y + endPosition.y];
  110. return `M${center.x} ${center.y}
  111. L${arcStartX} ${arcStartY}
  112. A ${radius} ${radius} 0 ${largeArc} ${clockWise ? 1 : 0}
  113. ${arcEndX} ${midArc} z
  114. L${arcStartX} ${midArc}
  115. A ${radius} ${radius} 0 ${largeArc} ${clockWise ? 1 : 0}
  116. ${arcEndX} ${arcEndY} z`;
  117. }
  118. export function makeArcStrokePathStr(startPosition, endPosition, center, radius, clockWise=1, largeArc=0){
  119. let [arcStartX, arcStartY] = [center.x + startPosition.x, center.y + startPosition.y];
  120. let [arcEndX, arcEndY] = [center.x + endPosition.x, center.y + endPosition.y];
  121. return `M${arcStartX} ${arcStartY}
  122. A ${radius} ${radius} 0 ${largeArc} ${clockWise ? 1 : 0}
  123. ${arcEndX} ${arcEndY}`;
  124. }
  125. export function makeStrokeCircleStr(startPosition, endPosition, center, radius, clockWise=1, largeArc=0){
  126. let [arcStartX, arcStartY] = [center.x + startPosition.x, center.y + startPosition.y];
  127. let [arcEndX, midArc, arcEndY] = [center.x + endPosition.x, radius * 2 + arcStartY, center.y + startPosition.y];
  128. return `M${arcStartX} ${arcStartY}
  129. A ${radius} ${radius} 0 ${largeArc} ${clockWise ? 1 : 0}
  130. ${arcEndX} ${midArc}
  131. M${arcStartX} ${midArc}
  132. A ${radius} ${radius} 0 ${largeArc} ${clockWise ? 1 : 0}
  133. ${arcEndX} ${arcEndY}`;
  134. }
  135. export function makeGradient(svgDefElem, color, lighter = false) {
  136. let gradientId ='path-fill-gradient' + '-' + color + '-' +(lighter ? 'lighter' : 'default');
  137. let gradientDef = renderVerticalGradient(svgDefElem, gradientId);
  138. let opacities = [1, 0.6, 0.2];
  139. if(lighter) {
  140. opacities = [0.4, 0.2, 0];
  141. }
  142. setGradientStop(gradientDef, "0%", color, opacities[0]);
  143. setGradientStop(gradientDef, "50%", color, opacities[1]);
  144. setGradientStop(gradientDef, "100%", color, opacities[2]);
  145. return gradientId;
  146. }
  147. export function percentageBar(x, y, width, height,
  148. depth=PERCENTAGE_BAR_DEFAULT_DEPTH, fill='none') {
  149. let args = {
  150. className: 'percentage-bar',
  151. x: x,
  152. y: y,
  153. width: width,
  154. height: height,
  155. fill: fill,
  156. styles: {
  157. 'stroke': lightenDarkenColor(fill, -25),
  158. // Diabolically good: https://stackoverflow.com/a/9000859
  159. // https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/stroke-dasharray
  160. 'stroke-dasharray': `0, ${height + width}, ${width}, ${height}`,
  161. 'stroke-width': depth
  162. },
  163. };
  164. return createSVG("rect", args);
  165. }
  166. export function heatSquare(className, x, y, size, radius, fill='none', data={}) {
  167. let args = {
  168. className: className,
  169. x: x,
  170. y: y,
  171. width: size,
  172. height: size,
  173. rx: radius,
  174. fill: fill
  175. };
  176. Object.keys(data).map(key => {
  177. args[key] = data[key];
  178. });
  179. return createSVG("rect", args);
  180. }
  181. export function legendBar(x, y, size, fill='none', label, truncate=false) {
  182. label = truncate ? truncateString(label, LABEL_MAX_CHARS) : label;
  183. let args = {
  184. className: 'legend-bar',
  185. x: 0,
  186. y: 0,
  187. width: size,
  188. height: '2px',
  189. fill: fill
  190. };
  191. let text = createSVG('text', {
  192. className: 'legend-dataset-text',
  193. x: 0,
  194. y: 0,
  195. dy: (FONT_SIZE * 2) + 'px',
  196. 'font-size': (FONT_SIZE * 1.2) + 'px',
  197. 'text-anchor': 'start',
  198. fill: FONT_FILL,
  199. innerHTML: label
  200. });
  201. let group = createSVG('g', {
  202. transform: `translate(${x}, ${y})`
  203. });
  204. group.appendChild(createSVG("rect", args));
  205. group.appendChild(text);
  206. return group;
  207. }
  208. export function legendDot(x, y, size, radius, fill='none', label, value, truncate=false) {
  209. label = truncate ? truncateString(label, LABEL_MAX_CHARS) : label;
  210. let args = {
  211. className: 'legend-dot',
  212. x: 0,
  213. y: 4 - size,
  214. height: size,
  215. width: size,
  216. rx: radius,
  217. fill: fill
  218. };
  219. let textLabel = createSVG('text', {
  220. className: 'legend-dataset-label',
  221. x: size,
  222. y: 0,
  223. dx: (FONT_SIZE) + 'px',
  224. dy: (FONT_SIZE/3) + 'px',
  225. 'font-size': (FONT_SIZE * 1.6) + 'px',
  226. 'text-anchor': 'start',
  227. fill: FONT_FILL,
  228. innerHTML: label
  229. });
  230. let textValue = createSVG('text', {
  231. className: 'legend-dataset-value',
  232. x: size,
  233. y: FONT_SIZE + 10,
  234. dx: (FONT_SIZE) + 'px',
  235. dy: (FONT_SIZE/3) + 'px',
  236. 'font-size': (FONT_SIZE * 1.2) + 'px',
  237. 'text-anchor': 'start',
  238. fill: FONT_FILL,
  239. innerHTML: value
  240. });
  241. let group = createSVG('g', {
  242. transform: `translate(${x}, ${y})`
  243. });
  244. group.appendChild(createSVG("rect", args));
  245. group.appendChild(textLabel);
  246. group.appendChild(textValue);
  247. return group;
  248. }
  249. export function makeText(className, x, y, content, options = {}) {
  250. let fontSize = options.fontSize || FONT_SIZE;
  251. let dy = options.dy !== undefined ? options.dy : (fontSize / 2);
  252. let fill = options.fill || FONT_FILL;
  253. let textAnchor = options.textAnchor || 'start';
  254. return createSVG('text', {
  255. className: className,
  256. x: x,
  257. y: y,
  258. dy: dy + 'px',
  259. 'font-size': fontSize + 'px',
  260. fill: fill,
  261. 'text-anchor': textAnchor,
  262. innerHTML: content
  263. });
  264. }
  265. function makeVertLine(x, label, y1, y2, options={}) {
  266. if(!options.stroke) options.stroke = BASE_LINE_COLOR;
  267. let l = createSVG('line', {
  268. className: 'line-vertical ' + options.className,
  269. x1: 0,
  270. x2: 0,
  271. y1: y1,
  272. y2: y2,
  273. styles: {
  274. stroke: options.stroke
  275. }
  276. });
  277. let text = createSVG('text', {
  278. x: 0,
  279. y: y1 > y2 ? y1 + LABEL_MARGIN : y1 - LABEL_MARGIN - FONT_SIZE,
  280. dy: FONT_SIZE + 'px',
  281. 'font-size': FONT_SIZE + 'px',
  282. 'text-anchor': 'middle',
  283. innerHTML: label + ""
  284. });
  285. let line = createSVG('g', {
  286. transform: `translate(${ x }, 0)`
  287. });
  288. line.appendChild(l);
  289. line.appendChild(text);
  290. return line;
  291. }
  292. function makeHoriLine(y, label, x1, x2, options={}) {
  293. if(!options.stroke) options.stroke = BASE_LINE_COLOR;
  294. if(!options.lineType) options.lineType = '';
  295. if (options.shortenNumbers) label = shortenLargeNumber(label);
  296. let className = 'line-horizontal ' + options.className +
  297. (options.lineType === "dashed" ? "dashed": "");
  298. let l = createSVG('line', {
  299. className: className,
  300. x1: x1,
  301. x2: x2,
  302. y1: 0,
  303. y2: 0,
  304. styles: {
  305. stroke: options.stroke
  306. }
  307. });
  308. let text = createSVG('text', {
  309. x: x1 < x2 ? x1 - LABEL_MARGIN : x1 + LABEL_MARGIN,
  310. y: 0,
  311. dy: (FONT_SIZE / 2 - 2) + 'px',
  312. 'font-size': FONT_SIZE + 'px',
  313. 'text-anchor': x1 < x2 ? 'end' : 'start',
  314. innerHTML: label+""
  315. });
  316. let line = createSVG('g', {
  317. transform: `translate(0, ${y})`,
  318. 'stroke-opacity': 1
  319. });
  320. if(text === 0 || text === '0') {
  321. line.style.stroke = "rgba(27, 31, 35, 0.6)";
  322. }
  323. line.appendChild(l);
  324. line.appendChild(text);
  325. return line;
  326. }
  327. export function yLine(y, label, width, options={}) {
  328. if (!isValidNumber(y)) y = 0;
  329. if(!options.pos) options.pos = 'left';
  330. if(!options.offset) options.offset = 0;
  331. if(!options.mode) options.mode = 'span';
  332. if(!options.stroke) options.stroke = BASE_LINE_COLOR;
  333. if(!options.className) options.className = '';
  334. let x1 = -1 * AXIS_TICK_LENGTH;
  335. let x2 = options.mode === 'span' ? width + AXIS_TICK_LENGTH : 0;
  336. if(options.mode === 'tick' && options.pos === 'right') {
  337. x1 = width + AXIS_TICK_LENGTH;
  338. x2 = width;
  339. }
  340. // let offset = options.pos === 'left' ? -1 * options.offset : options.offset;
  341. x1 += options.offset;
  342. x2 += options.offset;
  343. return makeHoriLine(y, label, x1, x2, {
  344. stroke: options.stroke,
  345. className: options.className,
  346. lineType: options.lineType,
  347. shortenNumbers: options.shortenNumbers
  348. });
  349. }
  350. export function xLine(x, label, height, options={}) {
  351. if (!isValidNumber(x)) x = 0;
  352. if(!options.pos) options.pos = 'bottom';
  353. if(!options.offset) options.offset = 0;
  354. if(!options.mode) options.mode = 'span';
  355. if(!options.stroke) options.stroke = BASE_LINE_COLOR;
  356. if(!options.className) options.className = '';
  357. // Draw X axis line in span/tick mode with optional label
  358. // y2(span)
  359. // |
  360. // |
  361. // x line |
  362. // |
  363. // |
  364. // ---------------------+-- y2(tick)
  365. // |
  366. // y1
  367. let y1 = height + AXIS_TICK_LENGTH;
  368. let y2 = options.mode === 'span' ? -1 * AXIS_TICK_LENGTH : height;
  369. if(options.mode === 'tick' && options.pos === 'top') {
  370. // top axis ticks
  371. y1 = -1 * AXIS_TICK_LENGTH;
  372. y2 = 0;
  373. }
  374. return makeVertLine(x, label, y1, y2, {
  375. stroke: options.stroke,
  376. className: options.className,
  377. lineType: options.lineType
  378. });
  379. }
  380. export function yMarker(y, label, width, options={}) {
  381. if(!options.labelPos) options.labelPos = 'right';
  382. let x = options.labelPos === 'left' ? LABEL_MARGIN
  383. : width - getStringWidth(label, 5) - LABEL_MARGIN;
  384. let labelSvg = createSVG('text', {
  385. className: 'chart-label',
  386. x: x,
  387. y: 0,
  388. dy: (FONT_SIZE / -2) + 'px',
  389. 'font-size': FONT_SIZE + 'px',
  390. 'text-anchor': 'start',
  391. innerHTML: label+""
  392. });
  393. let line = makeHoriLine(y, '', 0, width, {
  394. stroke: options.stroke || BASE_LINE_COLOR,
  395. className: options.className || '',
  396. lineType: options.lineType
  397. });
  398. line.appendChild(labelSvg);
  399. return line;
  400. }
  401. export function yRegion(y1, y2, width, label, options={}) {
  402. // return a group
  403. let height = y1 - y2;
  404. let rect = createSVG('rect', {
  405. className: `bar mini`, // remove class
  406. styles: {
  407. fill: `rgba(228, 234, 239, 0.49)`,
  408. stroke: BASE_LINE_COLOR,
  409. 'stroke-dasharray': `${width}, ${height}`
  410. },
  411. // 'data-point-index': index,
  412. x: 0,
  413. y: 0,
  414. width: width,
  415. height: height
  416. });
  417. if(!options.labelPos) options.labelPos = 'right';
  418. let x = options.labelPos === 'left' ? LABEL_MARGIN
  419. : width - getStringWidth(label+"", 4.5) - LABEL_MARGIN;
  420. let labelSvg = createSVG('text', {
  421. className: 'chart-label',
  422. x: x,
  423. y: 0,
  424. dy: (FONT_SIZE / -2) + 'px',
  425. 'font-size': FONT_SIZE + 'px',
  426. 'text-anchor': 'start',
  427. innerHTML: label+""
  428. });
  429. let region = createSVG('g', {
  430. transform: `translate(0, ${y2})`
  431. });
  432. region.appendChild(rect);
  433. region.appendChild(labelSvg);
  434. return region;
  435. }
  436. export function datasetBar(x, yTop, width, color, label='', index=0, offset=0, meta={}) {
  437. let [height, y] = getBarHeightAndYAttr(yTop, meta.zeroLine);
  438. y -= offset;
  439. if(height === 0) {
  440. height = meta.minHeight;
  441. y -= meta.minHeight;
  442. }
  443. // Preprocess numbers to avoid svg building errors
  444. if (!isValidNumber(x)) x = 0;
  445. if (!isValidNumber(y)) y = 0;
  446. if (!isValidNumber(height, true)) height = 0;
  447. if (!isValidNumber(width, true)) width = 0;
  448. let rect = createSVG('rect', {
  449. className: `bar mini`,
  450. style: `fill: ${color}`,
  451. 'data-point-index': index,
  452. x: x,
  453. y: y,
  454. width: width,
  455. height: height
  456. });
  457. label += "";
  458. if(!label && !label.length) {
  459. return rect;
  460. } else {
  461. rect.setAttribute('y', 0);
  462. rect.setAttribute('x', 0);
  463. let text = createSVG('text', {
  464. className: 'data-point-value',
  465. x: width/2,
  466. y: 0,
  467. dy: (FONT_SIZE / 2 * -1) + 'px',
  468. 'font-size': FONT_SIZE + 'px',
  469. 'text-anchor': 'middle',
  470. innerHTML: label
  471. });
  472. let group = createSVG('g', {
  473. 'data-point-index': index,
  474. transform: `translate(${x}, ${y})`
  475. });
  476. group.appendChild(rect);
  477. group.appendChild(text);
  478. return group;
  479. }
  480. }
  481. export function datasetDot(x, y, radius, color, label='', index=0) {
  482. let dot = createSVG('circle', {
  483. style: `fill: ${color}`,
  484. 'data-point-index': index,
  485. cx: x,
  486. cy: y,
  487. r: radius
  488. });
  489. label += "";
  490. if(!label && !label.length) {
  491. return dot;
  492. } else {
  493. dot.setAttribute('cy', 0);
  494. dot.setAttribute('cx', 0);
  495. let text = createSVG('text', {
  496. className: 'data-point-value',
  497. x: 0,
  498. y: 0,
  499. dy: (FONT_SIZE / 2 * -1 - radius) + 'px',
  500. 'font-size': FONT_SIZE + 'px',
  501. 'text-anchor': 'middle',
  502. innerHTML: label
  503. });
  504. let group = createSVG('g', {
  505. 'data-point-index': index,
  506. transform: `translate(${x}, ${y})`
  507. });
  508. group.appendChild(dot);
  509. group.appendChild(text);
  510. return group;
  511. }
  512. }
  513. export function getPaths(xList, yList, color, options={}, meta={}) {
  514. let pointsList = yList.map((y, i) => (xList[i] + ',' + y));
  515. let pointsStr = pointsList.join("L");
  516. // Spline
  517. if (options.spline)
  518. pointsStr = getSplineCurvePointsStr(xList, yList);
  519. let path = makePath("M"+pointsStr, 'line-graph-path', color);
  520. // HeatLine
  521. if(options.heatline) {
  522. let gradient_id = makeGradient(meta.svgDefs, color);
  523. path.style.stroke = `url(#${gradient_id})`;
  524. }
  525. let paths = {
  526. path: path
  527. };
  528. // Region
  529. if(options.regionFill) {
  530. let gradient_id_region = makeGradient(meta.svgDefs, color, true);
  531. let pathStr = "M" + `${xList[0]},${meta.zeroLine}L` + pointsStr + `L${xList.slice(-1)[0]},${meta.zeroLine}`;
  532. paths.region = makePath(pathStr, `region-fill`, 'none', `url(#${gradient_id_region})`);
  533. }
  534. return paths;
  535. }
  536. export let makeOverlay = {
  537. 'bar': (unit) => {
  538. let transformValue;
  539. if(unit.nodeName !== 'rect') {
  540. transformValue = unit.getAttribute('transform');
  541. unit = unit.childNodes[0];
  542. }
  543. let overlay = unit.cloneNode();
  544. overlay.style.fill = '#000000';
  545. overlay.style.opacity = '0.4';
  546. if(transformValue) {
  547. overlay.setAttribute('transform', transformValue);
  548. }
  549. return overlay;
  550. },
  551. 'dot': (unit) => {
  552. let transformValue;
  553. if(unit.nodeName !== 'circle') {
  554. transformValue = unit.getAttribute('transform');
  555. unit = unit.childNodes[0];
  556. }
  557. let overlay = unit.cloneNode();
  558. let radius = unit.getAttribute('r');
  559. let fill = unit.getAttribute('fill');
  560. overlay.setAttribute('r', parseInt(radius) + DOT_OVERLAY_SIZE_INCR);
  561. overlay.setAttribute('fill', fill);
  562. overlay.style.opacity = '0.6';
  563. if(transformValue) {
  564. overlay.setAttribute('transform', transformValue);
  565. }
  566. return overlay;
  567. },
  568. 'heat_square': (unit) => {
  569. let transformValue;
  570. if(unit.nodeName !== 'circle') {
  571. transformValue = unit.getAttribute('transform');
  572. unit = unit.childNodes[0];
  573. }
  574. let overlay = unit.cloneNode();
  575. let radius = unit.getAttribute('r');
  576. let fill = unit.getAttribute('fill');
  577. overlay.setAttribute('r', parseInt(radius) + DOT_OVERLAY_SIZE_INCR);
  578. overlay.setAttribute('fill', fill);
  579. overlay.style.opacity = '0.6';
  580. if(transformValue) {
  581. overlay.setAttribute('transform', transformValue);
  582. }
  583. return overlay;
  584. }
  585. };
  586. export let updateOverlay = {
  587. 'bar': (unit, overlay) => {
  588. let transformValue;
  589. if(unit.nodeName !== 'rect') {
  590. transformValue = unit.getAttribute('transform');
  591. unit = unit.childNodes[0];
  592. }
  593. let attributes = ['x', 'y', 'width', 'height'];
  594. Object.values(unit.attributes)
  595. .filter(attr => attributes.includes(attr.name) && attr.specified)
  596. .map(attr => {
  597. overlay.setAttribute(attr.name, attr.nodeValue);
  598. });
  599. if(transformValue) {
  600. overlay.setAttribute('transform', transformValue);
  601. }
  602. },
  603. 'dot': (unit, overlay) => {
  604. let transformValue;
  605. if(unit.nodeName !== 'circle') {
  606. transformValue = unit.getAttribute('transform');
  607. unit = unit.childNodes[0];
  608. }
  609. let attributes = ['cx', 'cy'];
  610. Object.values(unit.attributes)
  611. .filter(attr => attributes.includes(attr.name) && attr.specified)
  612. .map(attr => {
  613. overlay.setAttribute(attr.name, attr.nodeValue);
  614. });
  615. if(transformValue) {
  616. overlay.setAttribute('transform', transformValue);
  617. }
  618. },
  619. 'heat_square': (unit, overlay) => {
  620. let transformValue;
  621. if(unit.nodeName !== 'circle') {
  622. transformValue = unit.getAttribute('transform');
  623. unit = unit.childNodes[0];
  624. }
  625. let attributes = ['cx', 'cy'];
  626. Object.values(unit.attributes)
  627. .filter(attr => attributes.includes(attr.name) && attr.specified)
  628. .map(attr => {
  629. overlay.setAttribute(attr.name, attr.nodeValue);
  630. });
  631. if(transformValue) {
  632. overlay.setAttribute('transform', transformValue);
  633. }
  634. },
  635. };