Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.
 
 
 

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