Du kannst nicht mehr als 25 Themen auswählen Themen müssen entweder mit einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.
 
 
 

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