line-graph.tsx 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116
  1. "use client";
  2. import type { ComponentRenderProps } from "./types";
  3. import { baseClass, getCustomClass } from "./utils";
  4. interface DataPoint {
  5. label: string;
  6. value: number;
  7. }
  8. export function LineGraph({ element }: ComponentRenderProps) {
  9. const { props } = element;
  10. const customClass = getCustomClass(props);
  11. const data = (props.data as DataPoint[]) || [];
  12. const title = props.title as string | undefined;
  13. const maxValue = Math.max(...data.map((d) => d.value));
  14. const minValue = Math.min(...data.map((d) => d.value));
  15. const range = maxValue - minValue || 1;
  16. // SVG dimensions with padding
  17. const width = 300;
  18. const height = 100;
  19. const padding = { top: 10, right: 10, bottom: 10, left: 10 };
  20. const chartWidth = width - padding.left - padding.right;
  21. const chartHeight = height - padding.top - padding.bottom;
  22. // Calculate points for the SVG path
  23. const points = data.map((d, i) => {
  24. const x =
  25. padding.left +
  26. (data.length > 1 ? (i / (data.length - 1)) * chartWidth : chartWidth / 2);
  27. const y =
  28. padding.top + chartHeight - ((d.value - minValue) / range) * chartHeight;
  29. return { x, y, ...d };
  30. });
  31. const pathD =
  32. points.length > 0
  33. ? `M ${points.map((p) => `${p.x} ${p.y}`).join(" L ")}`
  34. : "";
  35. return (
  36. <div className={`${baseClass} ${customClass}`}>
  37. {title ? (
  38. <div className="text-xs font-medium mb-2 text-left">{title}</div>
  39. ) : null}
  40. <div className="relative h-24">
  41. <svg viewBox={`0 0 ${width} ${height}`} className="w-full h-full">
  42. {/* Grid lines */}
  43. <line
  44. x1={padding.left}
  45. y1={padding.top + chartHeight / 2}
  46. x2={width - padding.right}
  47. y2={padding.top + chartHeight / 2}
  48. stroke="currentColor"
  49. strokeOpacity="0.1"
  50. strokeWidth="1"
  51. />
  52. <line
  53. x1={padding.left}
  54. y1={padding.top}
  55. x2={width - padding.right}
  56. y2={padding.top}
  57. stroke="currentColor"
  58. strokeOpacity="0.1"
  59. strokeWidth="1"
  60. />
  61. <line
  62. x1={padding.left}
  63. y1={height - padding.bottom}
  64. x2={width - padding.right}
  65. y2={height - padding.bottom}
  66. stroke="currentColor"
  67. strokeOpacity="0.1"
  68. strokeWidth="1"
  69. />
  70. {/* Line */}
  71. {pathD && (
  72. <path
  73. d={pathD}
  74. fill="none"
  75. stroke="currentColor"
  76. strokeWidth="2"
  77. strokeLinecap="round"
  78. strokeLinejoin="round"
  79. className="text-foreground/80"
  80. />
  81. )}
  82. {/* Points */}
  83. {points.map((p, i) => (
  84. <circle
  85. key={i}
  86. cx={p.x}
  87. cy={p.y}
  88. r="4"
  89. className="fill-foreground"
  90. />
  91. ))}
  92. </svg>
  93. </div>
  94. {data.length > 0 && (
  95. <div className="flex justify-between mt-1">
  96. {data.map((d, i) => (
  97. <div
  98. key={i}
  99. className="text-[8px] text-muted-foreground text-center"
  100. style={{ width: `${100 / data.length}%` }}
  101. >
  102. {d.label}
  103. </div>
  104. ))}
  105. </div>
  106. )}
  107. </div>
  108. );
  109. }