LineChartCard.tsx 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
  1. import { FC, useState, useEffect, useRef } from 'react';
  2. import { makeStyles } from '@material-ui/core';
  3. import BaseCard from './BaseCard';
  4. import { LineChartCardProps, LinceChartNode } from './Types';
  5. const getStyles = makeStyles(() => ({
  6. root: {
  7. transform: 'scaleY(-1)',
  8. maxWidth: '90%',
  9. },
  10. ycoord: {
  11. cursor: 'pointer',
  12. "&:hover, &:focus": {
  13. "& line": {
  14. transition: 'all .25s',
  15. opacity: 1,
  16. },
  17. },
  18. "&:hover": {
  19. "& circle": {
  20. fill: '#06AFF2',
  21. },
  22. },
  23. "&:focus": {
  24. outline: 'none',
  25. "& circle": {
  26. fill: '#06F3AF',
  27. },
  28. },
  29. }
  30. }));
  31. const LineChartCard: FC<LineChartCardProps> = (props) => {
  32. const FULL_HEIGHT = 60;
  33. const FULL_WIDTH = 300;
  34. const ROUND = 5;
  35. const STEP = 25;
  36. const classes = getStyles();
  37. const { title, value } = props;
  38. const [displayNodes, setDisplayNodes] = useState<LinceChartNode[]>([]);
  39. const [currentNode, setCurrentNode] = useState<LinceChartNode>({
  40. percent: 0,
  41. value: 0,
  42. timestamp: Date.now(),
  43. });
  44. const max = useRef(1);
  45. const isHover = useRef(false);
  46. const nodes = useRef<LinceChartNode[]>([]);
  47. useEffect(() => {
  48. // show at most 10 nodes. so remove the earliest node when nodes exceed 10
  49. if (nodes.current.length > 9) {
  50. nodes.current.shift();
  51. }
  52. if (value && max.current) {
  53. // calculate the y-axis max scale
  54. let currentMax = max.current;
  55. if (value > max.current) {
  56. const pow = Math.ceil(Math.log10(value));
  57. currentMax = Math.pow(10, pow);
  58. max.current = currentMax;
  59. }
  60. // generate a new node and save in ref
  61. if (nodes.current) {
  62. const newNodes = nodes.current.slice(0);
  63. const newNode = {
  64. percent: value / currentMax * 100,
  65. value,
  66. timestamp: Date.now(),
  67. }
  68. newNodes.push(newNode);
  69. nodes.current = newNodes;
  70. // refresh nodes for display when mouse is not hovering on the chart
  71. if (!isHover.current) {
  72. setDisplayNodes(newNodes);
  73. setCurrentNode(newNode);
  74. }
  75. }
  76. }
  77. }, [value]);
  78. return (
  79. nodes.current.length ? (
  80. <BaseCard title={title} content={`${Math.round(currentNode.value)}ms`} desc={new Date(currentNode.timestamp).toLocaleString()}>
  81. <svg className={classes.root} onMouseEnter={() => isHover.current = true} onMouseLeave={() => isHover.current = false} width={FULL_WIDTH} height={FULL_HEIGHT} viewBox={`0 5 ${FULL_WIDTH} ${FULL_HEIGHT}`} fill="white" xmlns="http://www.w3.org/2000/svg">
  82. {
  83. displayNodes.map((node, index) => {
  84. const x1 = FULL_WIDTH - (displayNodes.length - index + 1) * STEP;
  85. const y1 = node.percent * .5 + ROUND * 2;
  86. let line = null;
  87. if (index < displayNodes.length - 1) {
  88. const x2 = FULL_WIDTH - (displayNodes.length - index) * STEP;
  89. const y2 = displayNodes[index + 1]['percent'] * .5 + ROUND * 2;
  90. line = <line x1={x1} y1={y1} x2={x2} y2={y2} stroke="#06AFF2" />;
  91. }
  92. return (
  93. <g key={`${node.value}${index}`}>
  94. {line}
  95. <g className={classes.ycoord} onMouseOver={() => { setCurrentNode(node) }}>
  96. <circle cx={x1} cy={y1} r={ROUND} fill="white" stroke="#06AFF2" />
  97. <rect opacity="0" x={x1 - ROUND} y={0} width={ROUND * 2} height={FULL_HEIGHT} fill="#E9E9ED" />
  98. <line opacity="0" x1={x1} y1={0} x2={x1} y2={FULL_WIDTH} strokeWidth="2" stroke="#06AFF2" strokeDasharray="2.5" />
  99. </g>
  100. </g>
  101. )
  102. })
  103. }
  104. </svg>
  105. </BaseCard >
  106. ) : <BaseCard title={title} />
  107. );
  108. };
  109. export default LineChartCard;