您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符
 
 
 

685 行
16 KiB

  1. import { getBarHeightAndYAttr } from './draw-utils';
  2. import { getStringWidth } 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. export const FONT_SIZE = 10;
  8. const BASE_LINE_COLOR = '#dadada';
  9. const FONT_FILL = '#555b51';
  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=0) {
  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){
  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 0 ${clockWise ? 1 : 0}
  104. ${arcEndX} ${arcEndY} z`;
  105. }
  106. export function makeArcStrokePathStr(startPosition, endPosition, center, radius, clockWise=1){
  107. let [arcStartX, arcStartY] = [center.x + startPosition.x, center.y + startPosition.y];
  108. let [arcEndX, arcEndY] = [center.x + endPosition.x, center.y + endPosition.y];
  109. return `M${arcStartX} ${arcStartY}
  110. A ${radius} ${radius} 0 0 ${clockWise ? 1 : 0}
  111. ${arcEndX} ${arcEndY}`;
  112. }
  113. export function makeGradient(svgDefElem, color, lighter = false) {
  114. let gradientId ='path-fill-gradient' + '-' + color + '-' +(lighter ? 'lighter' : 'default');
  115. let gradientDef = renderVerticalGradient(svgDefElem, gradientId);
  116. let opacities = [1, 0.6, 0.2];
  117. if(lighter) {
  118. opacities = [0.4, 0.2, 0];
  119. }
  120. setGradientStop(gradientDef, "0%", color, opacities[0]);
  121. setGradientStop(gradientDef, "50%", color, opacities[1]);
  122. setGradientStop(gradientDef, "100%", color, opacities[2]);
  123. return gradientId;
  124. }
  125. export function percentageBar(x, y, width, height,
  126. depth=PERCENTAGE_BAR_DEFAULT_DEPTH, fill='none') {
  127. let args = {
  128. className: 'percentage-bar',
  129. x: x,
  130. y: y,
  131. width: width,
  132. height: height,
  133. fill: fill,
  134. styles: {
  135. 'stroke': lightenDarkenColor(fill, -25),
  136. // Diabolically good: https://stackoverflow.com/a/9000859
  137. // https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/stroke-dasharray
  138. 'stroke-dasharray': `0, ${height + width}, ${width}, ${height}`,
  139. 'stroke-width': depth
  140. },
  141. };
  142. return createSVG("rect", args);
  143. }
  144. export function heatSquare(className, x, y, size, fill='none', data={}) {
  145. let args = {
  146. className: className,
  147. x: x,
  148. y: y,
  149. width: size,
  150. height: size,
  151. fill: fill
  152. };
  153. Object.keys(data).map(key => {
  154. args[key] = data[key];
  155. });
  156. return createSVG("rect", args);
  157. }
  158. export function legendBar(x, y, size, fill='none', label) {
  159. let args = {
  160. className: 'legend-bar',
  161. x: 0,
  162. y: 0,
  163. width: size,
  164. height: '2px',
  165. fill: fill
  166. };
  167. let text = createSVG('text', {
  168. className: 'legend-dataset-text',
  169. x: 0,
  170. y: 0,
  171. dy: (FONT_SIZE * 2) + 'px',
  172. 'font-size': (FONT_SIZE * 1.2) + 'px',
  173. 'text-anchor': 'start',
  174. fill: FONT_FILL,
  175. innerHTML: label
  176. });
  177. let group = createSVG('g', {
  178. transform: `translate(${x}, ${y})`
  179. });
  180. group.appendChild(createSVG("rect", args));
  181. group.appendChild(text);
  182. return group;
  183. }
  184. export function legendDot(x, y, size, fill='none', label) {
  185. let args = {
  186. className: 'legend-dot',
  187. cx: 0,
  188. cy: 0,
  189. r: size,
  190. fill: fill
  191. };
  192. let text = createSVG('text', {
  193. className: 'legend-dataset-text',
  194. x: 0,
  195. y: 0,
  196. dx: (FONT_SIZE) + 'px',
  197. dy: (FONT_SIZE/3) + 'px',
  198. 'font-size': (FONT_SIZE * 1.2) + 'px',
  199. 'text-anchor': 'start',
  200. fill: FONT_FILL,
  201. innerHTML: label
  202. });
  203. let group = createSVG('g', {
  204. transform: `translate(${x}, ${y})`
  205. });
  206. group.appendChild(createSVG("circle", args));
  207. group.appendChild(text);
  208. return group;
  209. }
  210. export function makeText(className, x, y, content, options = {}) {
  211. let fontSize = options.fontSize || FONT_SIZE;
  212. let dy = options.dy !== undefined ? options.dy : (fontSize / 2);
  213. let fill = options.fill || FONT_FILL;
  214. let textAnchor = options.textAnchor || 'start';
  215. return createSVG('text', {
  216. className: className,
  217. x: x,
  218. y: y,
  219. dy: dy + 'px',
  220. 'font-size': fontSize + 'px',
  221. fill: fill,
  222. 'text-anchor': textAnchor,
  223. innerHTML: content
  224. });
  225. }
  226. function makeVertLine(x, label, y1, y2, options={}) {
  227. if(!options.stroke) options.stroke = BASE_LINE_COLOR;
  228. let l = createSVG('line', {
  229. className: 'line-vertical ' + options.className,
  230. x1: 0,
  231. x2: 0,
  232. y1: y1,
  233. y2: y2,
  234. styles: {
  235. stroke: options.stroke
  236. }
  237. });
  238. let text = createSVG('text', {
  239. x: 0,
  240. y: y1 > y2 ? y1 + LABEL_MARGIN : y1 - LABEL_MARGIN - FONT_SIZE,
  241. dy: FONT_SIZE + 'px',
  242. 'font-size': FONT_SIZE + 'px',
  243. 'text-anchor': 'middle',
  244. innerHTML: label + ""
  245. });
  246. let line = createSVG('g', {
  247. transform: `translate(${ x }, 0)`
  248. });
  249. line.appendChild(l);
  250. line.appendChild(text);
  251. return line;
  252. }
  253. function makeHoriLine(y, label, x1, x2, options={}) {
  254. if(!options.stroke) options.stroke = BASE_LINE_COLOR;
  255. if(!options.lineType) options.lineType = '';
  256. let className = 'line-horizontal ' + options.className +
  257. (options.lineType === "dashed" ? "dashed": "");
  258. let l = createSVG('line', {
  259. className: className,
  260. x1: x1,
  261. x2: x2,
  262. y1: 0,
  263. y2: 0,
  264. styles: {
  265. stroke: options.stroke
  266. }
  267. });
  268. let text = createSVG('text', {
  269. x: x1 < x2 ? x1 - LABEL_MARGIN : x1 + LABEL_MARGIN,
  270. y: 0,
  271. dy: (FONT_SIZE / 2 - 2) + 'px',
  272. 'font-size': FONT_SIZE + 'px',
  273. 'text-anchor': x1 < x2 ? 'end' : 'start',
  274. innerHTML: label+""
  275. });
  276. let line = createSVG('g', {
  277. transform: `translate(0, ${y})`,
  278. 'stroke-opacity': 1
  279. });
  280. if(text === 0 || text === '0') {
  281. line.style.stroke = "rgba(27, 31, 35, 0.6)";
  282. }
  283. line.appendChild(l);
  284. line.appendChild(text);
  285. return line;
  286. }
  287. export function yLine(y, label, width, options={}) {
  288. if(!options.pos) options.pos = 'left';
  289. if(!options.offset) options.offset = 0;
  290. if(!options.mode) options.mode = 'span';
  291. if(!options.stroke) options.stroke = BASE_LINE_COLOR;
  292. if(!options.className) options.className = '';
  293. let x1 = -1 * AXIS_TICK_LENGTH;
  294. let x2 = options.mode === 'span' ? width + AXIS_TICK_LENGTH : 0;
  295. if(options.mode === 'tick' && options.pos === 'right') {
  296. x1 = width + AXIS_TICK_LENGTH;
  297. x2 = width;
  298. }
  299. // let offset = options.pos === 'left' ? -1 * options.offset : options.offset;
  300. x1 += options.offset;
  301. x2 += options.offset;
  302. return makeHoriLine(y, label, x1, x2, {
  303. stroke: options.stroke,
  304. className: options.className,
  305. lineType: options.lineType
  306. });
  307. }
  308. export function xLine(x, label, height, options={}) {
  309. if(!options.pos) options.pos = 'bottom';
  310. if(!options.offset) options.offset = 0;
  311. if(!options.mode) options.mode = 'span';
  312. if(!options.stroke) options.stroke = BASE_LINE_COLOR;
  313. if(!options.className) options.className = '';
  314. // Draw X axis line in span/tick mode with optional label
  315. // y2(span)
  316. // |
  317. // |
  318. // x line |
  319. // |
  320. // |
  321. // ---------------------+-- y2(tick)
  322. // |
  323. // y1
  324. let y1 = height + AXIS_TICK_LENGTH;
  325. let y2 = options.mode === 'span' ? -1 * AXIS_TICK_LENGTH : height;
  326. if(options.mode === 'tick' && options.pos === 'top') {
  327. // top axis ticks
  328. y1 = -1 * AXIS_TICK_LENGTH;
  329. y2 = 0;
  330. }
  331. return makeVertLine(x, label, y1, y2, {
  332. stroke: options.stroke,
  333. className: options.className,
  334. lineType: options.lineType
  335. });
  336. }
  337. export function yMarker(y, label, width, options={}) {
  338. if(!options.labelPos) options.labelPos = 'right';
  339. let x = options.labelPos === 'left' ? LABEL_MARGIN
  340. : width - getStringWidth(label, 5) - LABEL_MARGIN;
  341. let labelSvg = createSVG('text', {
  342. className: 'chart-label',
  343. x: x,
  344. y: 0,
  345. dy: (FONT_SIZE / -2) + 'px',
  346. 'font-size': FONT_SIZE + 'px',
  347. 'text-anchor': 'start',
  348. innerHTML: label+""
  349. });
  350. let line = makeHoriLine(y, '', 0, width, {
  351. stroke: options.stroke || BASE_LINE_COLOR,
  352. className: options.className || '',
  353. lineType: options.lineType
  354. });
  355. line.appendChild(labelSvg);
  356. return line;
  357. }
  358. export function yRegion(y1, y2, width, label, options={}) {
  359. // return a group
  360. let height = y1 - y2;
  361. let rect = createSVG('rect', {
  362. className: `bar mini`, // remove class
  363. styles: {
  364. fill: `rgba(228, 234, 239, 0.49)`,
  365. stroke: BASE_LINE_COLOR,
  366. 'stroke-dasharray': `${width}, ${height}`
  367. },
  368. // 'data-point-index': index,
  369. x: 0,
  370. y: 0,
  371. width: width,
  372. height: height
  373. });
  374. if(!options.labelPos) options.labelPos = 'right';
  375. let x = options.labelPos === 'left' ? LABEL_MARGIN
  376. : width - getStringWidth(label+"", 4.5) - LABEL_MARGIN;
  377. let labelSvg = createSVG('text', {
  378. className: 'chart-label',
  379. x: x,
  380. y: 0,
  381. dy: (FONT_SIZE / -2) + 'px',
  382. 'font-size': FONT_SIZE + 'px',
  383. 'text-anchor': 'start',
  384. innerHTML: label+""
  385. });
  386. let region = createSVG('g', {
  387. transform: `translate(0, ${y2})`
  388. });
  389. region.appendChild(rect);
  390. region.appendChild(labelSvg);
  391. return region;
  392. }
  393. export function datasetBar(x, yTop, width, color, label='', index=0, offset=0, meta={}) {
  394. let [height, y] = getBarHeightAndYAttr(yTop, meta.zeroLine);
  395. y -= offset;
  396. if(height === 0) {
  397. height = meta.minHeight;
  398. y -= meta.minHeight;
  399. }
  400. let rect = createSVG('rect', {
  401. className: `bar mini`,
  402. style: `fill: ${color}`,
  403. 'data-point-index': index,
  404. x: x,
  405. y: y,
  406. width: width,
  407. height: height
  408. });
  409. label += "";
  410. if(!label && !label.length) {
  411. return rect;
  412. } else {
  413. rect.setAttribute('y', 0);
  414. rect.setAttribute('x', 0);
  415. let text = createSVG('text', {
  416. className: 'data-point-value',
  417. x: width/2,
  418. y: 0,
  419. dy: (FONT_SIZE / 2 * -1) + 'px',
  420. 'font-size': FONT_SIZE + 'px',
  421. 'text-anchor': 'middle',
  422. innerHTML: label
  423. });
  424. let group = createSVG('g', {
  425. 'data-point-index': index,
  426. transform: `translate(${x}, ${y})`
  427. });
  428. group.appendChild(rect);
  429. group.appendChild(text);
  430. return group;
  431. }
  432. }
  433. export function datasetDot(x, y, radius, color, label='', index=0) {
  434. let dot = createSVG('circle', {
  435. style: `fill: ${color}`,
  436. 'data-point-index': index,
  437. cx: x,
  438. cy: y,
  439. r: radius
  440. });
  441. label += "";
  442. if(!label && !label.length) {
  443. return dot;
  444. } else {
  445. dot.setAttribute('cy', 0);
  446. dot.setAttribute('cx', 0);
  447. let text = createSVG('text', {
  448. className: 'data-point-value',
  449. x: 0,
  450. y: 0,
  451. dy: (FONT_SIZE / 2 * -1 - radius) + 'px',
  452. 'font-size': FONT_SIZE + 'px',
  453. 'text-anchor': 'middle',
  454. innerHTML: label
  455. });
  456. let group = createSVG('g', {
  457. 'data-point-index': index,
  458. transform: `translate(${x}, ${y})`
  459. });
  460. group.appendChild(dot);
  461. group.appendChild(text);
  462. return group;
  463. }
  464. }
  465. export function getPaths(xList, yList, color, options={}, meta={}) {
  466. let pointsList = yList.map((y, i) => (xList[i] + ',' + y));
  467. let pointsStr = pointsList.join("L");
  468. let path = makePath("M"+pointsStr, 'line-graph-path', color);
  469. // HeatLine
  470. if(options.heatline) {
  471. let gradient_id = makeGradient(meta.svgDefs, color);
  472. path.style.stroke = `url(#${gradient_id})`;
  473. }
  474. let paths = {
  475. path: path
  476. };
  477. // Region
  478. if(options.regionFill) {
  479. let gradient_id_region = makeGradient(meta.svgDefs, color, true);
  480. let pathStr = "M" + `${xList[0]},${meta.zeroLine}L` + pointsStr + `L${xList.slice(-1)[0]},${meta.zeroLine}`;
  481. paths.region = makePath(pathStr, `region-fill`, 'none', `url(#${gradient_id_region})`);
  482. }
  483. return paths;
  484. }
  485. export let makeOverlay = {
  486. 'bar': (unit) => {
  487. let transformValue;
  488. if(unit.nodeName !== 'rect') {
  489. transformValue = unit.getAttribute('transform');
  490. unit = unit.childNodes[0];
  491. }
  492. let overlay = unit.cloneNode();
  493. overlay.style.fill = '#000000';
  494. overlay.style.opacity = '0.4';
  495. if(transformValue) {
  496. overlay.setAttribute('transform', transformValue);
  497. }
  498. return overlay;
  499. },
  500. 'dot': (unit) => {
  501. let transformValue;
  502. if(unit.nodeName !== 'circle') {
  503. transformValue = unit.getAttribute('transform');
  504. unit = unit.childNodes[0];
  505. }
  506. let overlay = unit.cloneNode();
  507. let radius = unit.getAttribute('r');
  508. let fill = unit.getAttribute('fill');
  509. overlay.setAttribute('r', parseInt(radius) + DOT_OVERLAY_SIZE_INCR);
  510. overlay.setAttribute('fill', fill);
  511. overlay.style.opacity = '0.6';
  512. if(transformValue) {
  513. overlay.setAttribute('transform', transformValue);
  514. }
  515. return overlay;
  516. },
  517. 'heat_square': (unit) => {
  518. let transformValue;
  519. if(unit.nodeName !== 'circle') {
  520. transformValue = unit.getAttribute('transform');
  521. unit = unit.childNodes[0];
  522. }
  523. let overlay = unit.cloneNode();
  524. let radius = unit.getAttribute('r');
  525. let fill = unit.getAttribute('fill');
  526. overlay.setAttribute('r', parseInt(radius) + DOT_OVERLAY_SIZE_INCR);
  527. overlay.setAttribute('fill', fill);
  528. overlay.style.opacity = '0.6';
  529. if(transformValue) {
  530. overlay.setAttribute('transform', transformValue);
  531. }
  532. return overlay;
  533. }
  534. };
  535. export let updateOverlay = {
  536. 'bar': (unit, overlay) => {
  537. let transformValue;
  538. if(unit.nodeName !== 'rect') {
  539. transformValue = unit.getAttribute('transform');
  540. unit = unit.childNodes[0];
  541. }
  542. let attributes = ['x', 'y', 'width', 'height'];
  543. Object.values(unit.attributes)
  544. .filter(attr => attributes.includes(attr.name) && attr.specified)
  545. .map(attr => {
  546. overlay.setAttribute(attr.name, attr.nodeValue);
  547. });
  548. if(transformValue) {
  549. overlay.setAttribute('transform', transformValue);
  550. }
  551. },
  552. 'dot': (unit, overlay) => {
  553. let transformValue;
  554. if(unit.nodeName !== 'circle') {
  555. transformValue = unit.getAttribute('transform');
  556. unit = unit.childNodes[0];
  557. }
  558. let attributes = ['cx', 'cy'];
  559. Object.values(unit.attributes)
  560. .filter(attr => attributes.includes(attr.name) && attr.specified)
  561. .map(attr => {
  562. overlay.setAttribute(attr.name, attr.nodeValue);
  563. });
  564. if(transformValue) {
  565. overlay.setAttribute('transform', transformValue);
  566. }
  567. },
  568. 'heat_square': (unit, overlay) => {
  569. let transformValue;
  570. if(unit.nodeName !== 'circle') {
  571. transformValue = unit.getAttribute('transform');
  572. unit = unit.childNodes[0];
  573. }
  574. let attributes = ['cx', 'cy'];
  575. Object.values(unit.attributes)
  576. .filter(attr => attributes.includes(attr.name) && attr.specified)
  577. .map(attr => {
  578. overlay.setAttribute(attr.name, attr.nodeValue);
  579. });
  580. if(transformValue) {
  581. overlay.setAttribute('transform', transformValue);
  582. }
  583. },
  584. };