| 1 |
/** |
| 2 |
* Simple Line Chart Component |
| 3 |
* Lightweight line chart for displaying trends |
| 4 |
* Modular and extensible for Pro version enhancements |
| 5 |
*/ |
| 6 |
|
| 7 |
import React from "react"; |
| 8 |
import { __ } from "../../lib/i18n"; |
| 9 |
|
| 10 |
interface DataPoint { |
| 11 |
label: string; |
| 12 |
value: number; |
| 13 |
} |
| 14 |
|
| 15 |
interface SimpleLineChartProps { |
| 16 |
data: DataPoint[]; |
| 17 |
title: string; |
| 18 |
height?: number; |
| 19 |
color?: string; |
| 20 |
showGrid?: boolean; |
| 21 |
} |
| 22 |
|
| 23 |
/** |
| 24 |
* Simple Line Chart Component |
| 25 |
* Renders a basic line chart using SVG |
| 26 |
*/ |
| 27 |
export const SimpleLineChart: React.FC<SimpleLineChartProps> = ({ |
| 28 |
data, |
| 29 |
title, |
| 30 |
height = 200, |
| 31 |
color = "#3b82f6", |
| 32 |
showGrid = true, |
| 33 |
}) => { |
| 34 |
if (!data || data.length === 0) { |
| 35 |
return ( |
| 36 |
<div className="flex items-center justify-center h-full text-gray-500 dark:text-gray-400"> |
| 37 |
{__("No data available", "yatra")} |
| 38 |
</div> |
| 39 |
); |
| 40 |
} |
| 41 |
|
| 42 |
const maxValue = Math.max(...data.map((d) => d.value), 1); |
| 43 |
const minValue = Math.min(...data.map((d) => d.value), 0); |
| 44 |
const range = maxValue - minValue || 1; |
| 45 |
|
| 46 |
const width = 100; |
| 47 |
const chartHeight = height - 40; |
| 48 |
const padding = 10; |
| 49 |
|
| 50 |
// Calculate points for the line |
| 51 |
const points = data.map((point, index) => { |
| 52 |
const x = |
| 53 |
padding + (index * (width - 2 * padding)) / (data.length - 1 || 1); |
| 54 |
const y = |
| 55 |
chartHeight - |
| 56 |
padding - |
| 57 |
((point.value - minValue) / range) * (chartHeight - 2 * padding); |
| 58 |
return { x, y, value: point.value, label: point.label }; |
| 59 |
}); |
| 60 |
|
| 61 |
// Create path for the line |
| 62 |
const pathData = points |
| 63 |
.map((point, index) => { |
| 64 |
return `${index === 0 ? "M" : "L"} ${point.x} ${point.y}`; |
| 65 |
}) |
| 66 |
.join(" "); |
| 67 |
|
| 68 |
// Create area path (for fill) |
| 69 |
const areaPath = `${pathData} L ${points[points.length - 1].x} ${chartHeight - padding} L ${padding} ${chartHeight - padding} Z`; |
| 70 |
|
| 71 |
return ( |
| 72 |
<div className="w-full h-full"> |
| 73 |
{title && ( |
| 74 |
<h3 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-4"> |
| 75 |
{title} |
| 76 |
</h3> |
| 77 |
)} |
| 78 |
<svg |
| 79 |
width="100%" |
| 80 |
height={height} |
| 81 |
viewBox={`0 0 ${width} ${height}`} |
| 82 |
className="overflow-visible" |
| 83 |
> |
| 84 |
{/* Grid lines - subtle */} |
| 85 |
{showGrid && ( |
| 86 |
<g |
| 87 |
stroke="#f3f4f6" |
| 88 |
strokeWidth="0.5" |
| 89 |
className="dark:stroke-gray-700" |
| 90 |
> |
| 91 |
{[0, 0.25, 0.5, 0.75, 1].map((ratio) => { |
| 92 |
const y = |
| 93 |
chartHeight - padding - ratio * (chartHeight - 2 * padding); |
| 94 |
return ( |
| 95 |
<line |
| 96 |
key={ratio} |
| 97 |
x1={padding} |
| 98 |
y1={y} |
| 99 |
x2={width - padding} |
| 100 |
y2={y} |
| 101 |
/> |
| 102 |
); |
| 103 |
})} |
| 104 |
</g> |
| 105 |
)} |
| 106 |
|
| 107 |
{/* Area fill - subtle */} |
| 108 |
<path |
| 109 |
d={areaPath} |
| 110 |
fill={color} |
| 111 |
fillOpacity="0.08" |
| 112 |
className="dark:opacity-20" |
| 113 |
/> |
| 114 |
|
| 115 |
{/* Line */} |
| 116 |
<path |
| 117 |
d={pathData} |
| 118 |
fill="none" |
| 119 |
stroke={color} |
| 120 |
strokeWidth="2.5" |
| 121 |
strokeLinecap="round" |
| 122 |
strokeLinejoin="round" |
| 123 |
/> |
| 124 |
|
| 125 |
{/* Data points */} |
| 126 |
{points.map((point, index) => ( |
| 127 |
<g key={index}> |
| 128 |
<circle |
| 129 |
cx={point.x} |
| 130 |
cy={point.y} |
| 131 |
r="3.5" |
| 132 |
fill="white" |
| 133 |
stroke={color} |
| 134 |
strokeWidth="2" |
| 135 |
className="hover:r-4 transition-all" |
| 136 |
/> |
| 137 |
<title>{`${point.label}: ${point.value}`}</title> |
| 138 |
</g> |
| 139 |
))} |
| 140 |
|
| 141 |
{/* X-axis labels */} |
| 142 |
{points.map((point, index) => { |
| 143 |
// Show all labels but space them better |
| 144 |
const shouldShow = |
| 145 |
index === 0 || |
| 146 |
index === data.length - 1 || |
| 147 |
index % Math.max(1, Math.floor(data.length / 4)) === 0; |
| 148 |
if (shouldShow) { |
| 149 |
// Split long labels (like "MayJun" -> "May" / "Jun") |
| 150 |
const labelParts = |
| 151 |
point.label.length > 5 |
| 152 |
? point.label.match(/.{1,3}/g) || [point.label] |
| 153 |
: [point.label]; |
| 154 |
|
| 155 |
return ( |
| 156 |
<g key={index}> |
| 157 |
{labelParts.map((part, partIndex) => ( |
| 158 |
<text |
| 159 |
key={partIndex} |
| 160 |
x={point.x} |
| 161 |
y={chartHeight - 5 + partIndex * 10} |
| 162 |
textAnchor="middle" |
| 163 |
className="text-xs fill-gray-500 dark:fill-gray-400" |
| 164 |
> |
| 165 |
{part} |
| 166 |
</text> |
| 167 |
))} |
| 168 |
</g> |
| 169 |
); |
| 170 |
} |
| 171 |
return null; |
| 172 |
})} |
| 173 |
</svg> |
| 174 |
</div> |
| 175 |
); |
| 176 |
}; |
| 177 |
|
| 178 |
export default SimpleLineChart; |
| 179 |
|