line-graph.tsx 2.5 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394
  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), 1);
  14. const minValue = Math.min(...data.map((d) => d.value), 0);
  15. const range = maxValue - minValue || 1;
  16. // Calculate points for the SVG path
  17. const points = data.map((d, i) => {
  18. const x = data.length > 1 ? (i / (data.length - 1)) * 100 : 50;
  19. const y = 100 - ((d.value - minValue) / range) * 100;
  20. return { x, y, ...d };
  21. });
  22. const pathD =
  23. points.length > 0
  24. ? `M ${points.map((p) => `${p.x} ${p.y}`).join(" L ")}`
  25. : "";
  26. return (
  27. <div className={`${baseClass} ${customClass}`}>
  28. {title ? (
  29. <div className="text-xs font-medium mb-2 text-left">{title}</div>
  30. ) : null}
  31. <div className="relative h-24">
  32. <svg
  33. viewBox="0 0 100 100"
  34. preserveAspectRatio="none"
  35. className="w-full h-full"
  36. >
  37. {/* Grid lines */}
  38. <line
  39. x1="0"
  40. y1="50"
  41. x2="100"
  42. y2="50"
  43. stroke="currentColor"
  44. strokeOpacity="0.1"
  45. vectorEffect="non-scaling-stroke"
  46. />
  47. {/* Line */}
  48. {pathD && (
  49. <path
  50. d={pathD}
  51. fill="none"
  52. stroke="currentColor"
  53. strokeWidth="2"
  54. strokeLinecap="round"
  55. strokeLinejoin="round"
  56. vectorEffect="non-scaling-stroke"
  57. className="text-foreground/80"
  58. />
  59. )}
  60. {/* Points */}
  61. {points.map((p, i) => (
  62. <circle
  63. key={i}
  64. cx={p.x}
  65. cy={p.y}
  66. r="3"
  67. className="fill-foreground"
  68. vectorEffect="non-scaling-stroke"
  69. />
  70. ))}
  71. </svg>
  72. </div>
  73. {data.length > 0 && (
  74. <div className="flex justify-between mt-1">
  75. {data.map((d, i) => (
  76. <div
  77. key={i}
  78. className="text-[8px] text-muted-foreground text-center"
  79. style={{ width: `${100 / data.length}%` }}
  80. >
  81. {d.label}
  82. </div>
  83. ))}
  84. </div>
  85. )}
  86. </div>
  87. );
  88. }