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.
 
 
 

782 lines
19 KiB

  1. import { getBarHeightAndYAttr, truncateString, shortenLargeNumber, getSplineCurvePointsStr } from './draw-utils';
  2. import { getStringWidth, isValidNumber } 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 = 15;
  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 legendBar(x, y, fill = 'none', label, truncate = true) {
  192. label = truncate ? truncateString(label, LABEL_MAX_CHARS) : label;
  193. let args = {
  194. className: 'legend-bar',
  195. x: 0,
  196. y: 0,
  197. width: '12px',
  198. height: '12px',
  199. rx: '2px',
  200. ry: '2px',
  201. fill: fill
  202. };
  203. let text = createSVG('text', {
  204. className: 'legend-dataset-text',
  205. x: 0,
  206. y: 0,
  207. dy: (FONT_SIZE) + 'px',
  208. dx: (FONT_SIZE * 1.5) + 'px',
  209. 'font-size': (FONT_SIZE * 1.2) + 'px',
  210. 'text-anchor': 'start',
  211. fill: FONT_FILL,
  212. innerHTML: label
  213. });
  214. let group = createSVG('g', {
  215. transform: `translate(${x}, ${y})`
  216. });
  217. group.appendChild(createSVG("rect", args));
  218. group.appendChild(text);
  219. return group;
  220. }
  221. export function legendDot(x, y, size, radius, fill='none', label, value, truncate=false) {
  222. label = truncate ? truncateString(label, LABEL_MAX_CHARS) : label;
  223. let args = {
  224. className: 'legend-dot',
  225. x: 0,
  226. y: 4 - size,
  227. height: size,
  228. width: size,
  229. rx: radius,
  230. fill: fill
  231. };
  232. let textLabel = createSVG('text', {
  233. className: 'legend-dataset-label',
  234. x: size,
  235. y: 0,
  236. dx: (FONT_SIZE) + 'px',
  237. dy: (FONT_SIZE/3) + 'px',
  238. 'font-size': (FONT_SIZE * 1.6) + 'px',
  239. 'text-anchor': 'start',
  240. fill: FONT_FILL,
  241. innerHTML: label
  242. });
  243. let textValue = createSVG('text', {
  244. className: 'legend-dataset-value',
  245. x: size,
  246. y: FONT_SIZE + 10,
  247. dx: (FONT_SIZE) + 'px',
  248. dy: (FONT_SIZE/3) + 'px',
  249. 'font-size': (FONT_SIZE * 1.2) + 'px',
  250. 'text-anchor': 'start',
  251. fill: FONT_FILL,
  252. innerHTML: value
  253. });
  254. let group = createSVG('g', {
  255. transform: `translate(${x}, ${y})`
  256. });
  257. group.appendChild(createSVG("rect", args));
  258. group.appendChild(textLabel);
  259. group.appendChild(textValue);
  260. return group;
  261. }
  262. export function makeText(className, x, y, content, options = {}) {
  263. let fontSize = options.fontSize || FONT_SIZE;
  264. let dy = options.dy !== undefined ? options.dy : (fontSize / 2);
  265. let fill = options.fill || FONT_FILL;
  266. let textAnchor = options.textAnchor || 'start';
  267. return createSVG('text', {
  268. className: className,
  269. x: x,
  270. y: y,
  271. dy: dy + 'px',
  272. 'font-size': fontSize + 'px',
  273. fill: fill,
  274. 'text-anchor': textAnchor,
  275. innerHTML: content
  276. });
  277. }
  278. function makeVertLine(x, label, y1, y2, options={}) {
  279. let l = createSVG('line', {
  280. className: 'line-vertical ' + options.className,
  281. x1: 0,
  282. x2: 0,
  283. y1: y1,
  284. y2: y2,
  285. styles: {
  286. stroke: options.stroke
  287. }
  288. });
  289. let text = createSVG('text', {
  290. x: 0,
  291. y: y1 > y2 ? y1 + LABEL_MARGIN : y1 - LABEL_MARGIN - FONT_SIZE,
  292. dy: FONT_SIZE + 'px',
  293. 'font-size': FONT_SIZE + 'px',
  294. 'text-anchor': 'middle',
  295. innerHTML: label + ""
  296. });
  297. let line = createSVG('g', {
  298. transform: `translate(${ x }, 0)`
  299. });
  300. line.appendChild(l);
  301. line.appendChild(text);
  302. return line;
  303. }
  304. function makeHoriLine(y, label, x1, x2, options={}) {
  305. if(!options.lineType) options.lineType = '';
  306. if (options.shortenNumbers) label = shortenLargeNumber(label);
  307. let className = 'line-horizontal ' + options.className +
  308. (options.lineType === "dashed" ? "dashed": "");
  309. let l = createSVG('line', {
  310. className: className,
  311. x1: x1,
  312. x2: x2,
  313. y1: 0,
  314. y2: 0,
  315. styles: {
  316. stroke: options.stroke
  317. }
  318. });
  319. let text = createSVG('text', {
  320. x: x1 < x2 ? x1 - LABEL_MARGIN : x1 + LABEL_MARGIN,
  321. y: 0,
  322. dy: (FONT_SIZE / 2 - 2) + 'px',
  323. 'font-size': FONT_SIZE + 'px',
  324. 'text-anchor': x1 < x2 ? 'end' : 'start',
  325. innerHTML: label+""
  326. });
  327. let line = createSVG('g', {
  328. transform: `translate(0, ${y})`,
  329. 'stroke-opacity': 1
  330. });
  331. if(text === 0 || text === '0') {
  332. line.style.stroke = "rgba(27, 31, 35, 0.6)";
  333. }
  334. line.appendChild(l);
  335. line.appendChild(text);
  336. return line;
  337. }
  338. export function yLine(y, label, width, options={}) {
  339. if (!isValidNumber(y)) y = 0;
  340. if(!options.pos) options.pos = 'left';
  341. if(!options.offset) options.offset = 0;
  342. if(!options.mode) options.mode = 'span';
  343. if(!options.stroke) options.stroke = BASE_LINE_COLOR;
  344. if(!options.className) options.className = '';
  345. let x1 = -1 * AXIS_TICK_LENGTH;
  346. let x2 = options.mode === 'span' ? width + AXIS_TICK_LENGTH : 0;
  347. if(options.mode === 'tick' && options.pos === 'right') {
  348. x1 = width + AXIS_TICK_LENGTH;
  349. x2 = width;
  350. }
  351. // let offset = options.pos === 'left' ? -1 * options.offset : options.offset;
  352. x1 += options.offset;
  353. x2 += options.offset;
  354. return makeHoriLine(y, label, x1, x2, {
  355. className: options.className,
  356. lineType: options.lineType,
  357. shortenNumbers: options.shortenNumbers
  358. });
  359. }
  360. export function xLine(x, label, height, options={}) {
  361. if (!isValidNumber(x)) x = 0;
  362. if(!options.pos) options.pos = 'bottom';
  363. if(!options.offset) options.offset = 0;
  364. if(!options.mode) options.mode = 'span';
  365. if(!options.className) options.className = '';
  366. // Draw X axis line in span/tick mode with optional label
  367. // y2(span)
  368. // |
  369. // |
  370. // x line |
  371. // |
  372. // |
  373. // ---------------------+-- y2(tick)
  374. // |
  375. // y1
  376. let y1 = height + AXIS_TICK_LENGTH;
  377. let y2 = options.mode === 'span' ? -1 * AXIS_TICK_LENGTH : height;
  378. if(options.mode === 'tick' && options.pos === 'top') {
  379. // top axis ticks
  380. y1 = -1 * AXIS_TICK_LENGTH;
  381. y2 = 0;
  382. }
  383. return makeVertLine(x, label, y1, y2, {
  384. className: options.className,
  385. lineType: options.lineType
  386. });
  387. }
  388. export function yMarker(y, label, width, options={}) {
  389. if(!options.labelPos) options.labelPos = 'right';
  390. let x = options.labelPos === 'left' ? LABEL_MARGIN
  391. : width - getStringWidth(label, 5) - LABEL_MARGIN;
  392. let labelSvg = createSVG('text', {
  393. className: 'chart-label',
  394. x: x,
  395. y: 0,
  396. dy: (FONT_SIZE / -2) + 'px',
  397. 'font-size': FONT_SIZE + 'px',
  398. 'text-anchor': 'start',
  399. innerHTML: label+""
  400. });
  401. let line = makeHoriLine(y, '', 0, width, {
  402. stroke: options.stroke || BASE_LINE_COLOR,
  403. className: options.className || '',
  404. lineType: options.lineType
  405. });
  406. line.appendChild(labelSvg);
  407. return line;
  408. }
  409. export function yRegion(y1, y2, width, label, options={}) {
  410. // return a group
  411. let height = y1 - y2;
  412. let rect = createSVG('rect', {
  413. className: `bar mini`, // remove class
  414. styles: {
  415. fill: `rgba(228, 234, 239, 0.49)`,
  416. stroke: BASE_LINE_COLOR,
  417. 'stroke-dasharray': `${width}, ${height}`
  418. },
  419. // 'data-point-index': index,
  420. x: 0,
  421. y: 0,
  422. width: width,
  423. height: height
  424. });
  425. if(!options.labelPos) options.labelPos = 'right';
  426. let x = options.labelPos === 'left' ? LABEL_MARGIN
  427. : width - getStringWidth(label+"", 4.5) - LABEL_MARGIN;
  428. let labelSvg = createSVG('text', {
  429. className: 'chart-label',
  430. x: x,
  431. y: 0,
  432. dy: (FONT_SIZE / -2) + 'px',
  433. 'font-size': FONT_SIZE + 'px',
  434. 'text-anchor': 'start',
  435. innerHTML: label+""
  436. });
  437. let region = createSVG('g', {
  438. transform: `translate(0, ${y2})`
  439. });
  440. region.appendChild(rect);
  441. region.appendChild(labelSvg);
  442. return region;
  443. }
  444. export function datasetBar(x, yTop, width, color, label='', index=0, offset=0, meta={}) {
  445. let [height, y] = getBarHeightAndYAttr(yTop, meta.zeroLine);
  446. y -= offset;
  447. if(height === 0) {
  448. height = meta.minHeight;
  449. y -= meta.minHeight;
  450. }
  451. // Preprocess numbers to avoid svg building errors
  452. if (!isValidNumber(x)) x = 0;
  453. if (!isValidNumber(y)) y = 0;
  454. if (!isValidNumber(height, true)) height = 0;
  455. if (!isValidNumber(width, true)) width = 0;
  456. // x y h w
  457. // M{x},{y+r}
  458. // q0,-{r} {r},-{r}
  459. // q{r},0 {r},{r}
  460. // v{h-r}
  461. // h-{w}z
  462. // let radius = width/2;
  463. // let pathStr = `M${x},${y+radius} q0,-${radius} ${radius},-${radius} q${radius},0 ${radius},${radius} v${height-radius} h-${width}z`
  464. // let rect = createSVG('path', {
  465. // className: 'bar mini',
  466. // d: pathStr,
  467. // styles: { fill: color },
  468. // x: x,
  469. // y: y,
  470. // 'data-point-index': index,
  471. // });
  472. let rect = createSVG('rect', {
  473. className: `bar mini`,
  474. style: `fill: ${color}`,
  475. 'data-point-index': index,
  476. x: x,
  477. y: y,
  478. width: width,
  479. height: height
  480. });
  481. label += "";
  482. if(!label && !label.length) {
  483. return rect;
  484. } else {
  485. rect.setAttribute('y', 0);
  486. rect.setAttribute('x', 0);
  487. let text = createSVG('text', {
  488. className: 'data-point-value',
  489. x: width/2,
  490. y: 0,
  491. dy: (FONT_SIZE / 2 * -1) + 'px',
  492. 'font-size': FONT_SIZE + 'px',
  493. 'text-anchor': 'middle',
  494. innerHTML: label
  495. });
  496. let group = createSVG('g', {
  497. 'data-point-index': index,
  498. transform: `translate(${x}, ${y})`
  499. });
  500. group.appendChild(rect);
  501. group.appendChild(text);
  502. return group;
  503. }
  504. }
  505. export function datasetDot(x, y, radius, color, label='', index=0) {
  506. let dot = createSVG('circle', {
  507. style: `fill: ${color}`,
  508. 'data-point-index': index,
  509. cx: x,
  510. cy: y,
  511. r: radius
  512. });
  513. label += "";
  514. if(!label && !label.length) {
  515. return dot;
  516. } else {
  517. dot.setAttribute('cy', 0);
  518. dot.setAttribute('cx', 0);
  519. let text = createSVG('text', {
  520. className: 'data-point-value',
  521. x: 0,
  522. y: 0,
  523. dy: (FONT_SIZE / 2 * -1 - radius) + 'px',
  524. 'font-size': FONT_SIZE + 'px',
  525. 'text-anchor': 'middle',
  526. innerHTML: label
  527. });
  528. let group = createSVG('g', {
  529. 'data-point-index': index,
  530. transform: `translate(${x}, ${y})`
  531. });
  532. group.appendChild(dot);
  533. group.appendChild(text);
  534. return group;
  535. }
  536. }
  537. export function getPaths(xList, yList, color, options={}, meta={}) {
  538. let pointsList = yList.map((y, i) => (xList[i] + ',' + y));
  539. let pointsStr = pointsList.join("L");
  540. // Spline
  541. if (options.spline)
  542. pointsStr = getSplineCurvePointsStr(xList, yList);
  543. let path = makePath("M"+pointsStr, 'line-graph-path', color);
  544. // HeatLine
  545. if(options.heatline) {
  546. let gradient_id = makeGradient(meta.svgDefs, color);
  547. path.style.stroke = `url(#${gradient_id})`;
  548. }
  549. let paths = {
  550. path: path
  551. };
  552. // Region
  553. if(options.regionFill) {
  554. let gradient_id_region = makeGradient(meta.svgDefs, color, true);
  555. let pathStr = "M" + `${xList[0]},${meta.zeroLine}L` + pointsStr + `L${xList.slice(-1)[0]},${meta.zeroLine}`;
  556. paths.region = makePath(pathStr, `region-fill`, 'none', `url(#${gradient_id_region})`);
  557. }
  558. return paths;
  559. }
  560. export let makeOverlay = {
  561. 'bar': (unit) => {
  562. let transformValue;
  563. if(unit.nodeName !== 'rect') {
  564. transformValue = unit.getAttribute('transform');
  565. unit = unit.childNodes[0];
  566. }
  567. let overlay = unit.cloneNode();
  568. overlay.style.fill = '#000000';
  569. overlay.style.opacity = '0.4';
  570. if(transformValue) {
  571. overlay.setAttribute('transform', transformValue);
  572. }
  573. return overlay;
  574. },
  575. 'dot': (unit) => {
  576. let transformValue;
  577. if(unit.nodeName !== 'circle') {
  578. transformValue = unit.getAttribute('transform');
  579. unit = unit.childNodes[0];
  580. }
  581. let overlay = unit.cloneNode();
  582. let radius = unit.getAttribute('r');
  583. let fill = unit.getAttribute('fill');
  584. overlay.setAttribute('r', parseInt(radius) + DOT_OVERLAY_SIZE_INCR);
  585. overlay.setAttribute('fill', fill);
  586. overlay.style.opacity = '0.6';
  587. if(transformValue) {
  588. overlay.setAttribute('transform', transformValue);
  589. }
  590. return overlay;
  591. },
  592. 'heat_square': (unit) => {
  593. let transformValue;
  594. if(unit.nodeName !== 'circle') {
  595. transformValue = unit.getAttribute('transform');
  596. unit = unit.childNodes[0];
  597. }
  598. let overlay = unit.cloneNode();
  599. let radius = unit.getAttribute('r');
  600. let fill = unit.getAttribute('fill');
  601. overlay.setAttribute('r', parseInt(radius) + DOT_OVERLAY_SIZE_INCR);
  602. overlay.setAttribute('fill', fill);
  603. overlay.style.opacity = '0.6';
  604. if(transformValue) {
  605. overlay.setAttribute('transform', transformValue);
  606. }
  607. return overlay;
  608. }
  609. };
  610. export let updateOverlay = {
  611. 'bar': (unit, overlay) => {
  612. let transformValue;
  613. if(unit.nodeName !== 'rect') {
  614. transformValue = unit.getAttribute('transform');
  615. unit = unit.childNodes[0];
  616. }
  617. let attributes = ['x', 'y', 'width', 'height'];
  618. Object.values(unit.attributes)
  619. .filter(attr => attributes.includes(attr.name) && attr.specified)
  620. .map(attr => {
  621. overlay.setAttribute(attr.name, attr.nodeValue);
  622. });
  623. if(transformValue) {
  624. overlay.setAttribute('transform', transformValue);
  625. }
  626. },
  627. 'dot': (unit, overlay) => {
  628. let transformValue;
  629. if(unit.nodeName !== 'circle') {
  630. transformValue = unit.getAttribute('transform');
  631. unit = unit.childNodes[0];
  632. }
  633. let attributes = ['cx', 'cy'];
  634. Object.values(unit.attributes)
  635. .filter(attr => attributes.includes(attr.name) && attr.specified)
  636. .map(attr => {
  637. overlay.setAttribute(attr.name, attr.nodeValue);
  638. });
  639. if(transformValue) {
  640. overlay.setAttribute('transform', transformValue);
  641. }
  642. },
  643. 'heat_square': (unit, overlay) => {
  644. let transformValue;
  645. if(unit.nodeName !== 'circle') {
  646. transformValue = unit.getAttribute('transform');
  647. unit = unit.childNodes[0];
  648. }
  649. let attributes = ['cx', 'cy'];
  650. Object.values(unit.attributes)
  651. .filter(attr => attributes.includes(attr.name) && attr.specified)
  652. .map(attr => {
  653. overlay.setAttribute(attr.name, attr.nodeValue);
  654. });
  655. if(transformValue) {
  656. overlay.setAttribute('transform', transformValue);
  657. }
  658. },
  659. };