25개 이상의 토픽을 선택하실 수 없습니다. Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

604 lines
14 KiB

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