You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

756 lines
18 KiB

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