Skip to content

Commit db64427

Browse files
committed
Scope<COVID>: updating API and improving chart
-> Updated API as the previous one was deprecated. -> Improved CountryTrendChart to enhance readability by adjusting the position of grid lines and labels.
1 parent a404d5f commit db64427

File tree

2 files changed

+188
-54
lines changed

2 files changed

+188
-54
lines changed

src/components/CountryTrendChart.jsx

Lines changed: 133 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,143 @@
1-
import { useEffect, useState, useRef } from 'react';
1+
import { useEffect, useState, useRef, useMemo, useState as useLocalState } from 'react';
22
import Loading from './Loading.jsx';
33
import ChartPlaceholder from './ChartPlaceholder.jsx';
4+
import ErrorMessage from './ErrorMessage.jsx';
45

5-
// Tiny, dependency-free line chart that accepts array of {date, value}
6-
function SimpleLineChart({ data, width = 600, height = 200 }) {
6+
function SimpleLineChart({ data, width = 600, height = 250 }) {
7+
const [hover, setHover] = useLocalState(null);
78
if (!data || data.length === 0) return <ChartPlaceholder label="No data" />;
89

9-
const padding = 20;
10+
const padding = 40;
1011
const dates = data.map(d => new Date(d.date));
1112
const values = data.map(d => d.value);
13+
1214
const minY = Math.min(...values);
1315
const maxY = Math.max(...values);
16+
const yRange = maxY - minY || 1;
17+
18+
const x = i => padding + (i / (data.length - 1)) * (width - padding * 2);
19+
const y = v => height - padding - ((v - minY) / yRange) * (height - padding * 2);
20+
21+
// Smooth path using quadratic curves
22+
const path = data.reduce((acc, d, i, arr) => {
23+
const px = x(i);
24+
const py = y(d.value);
25+
if (i === 0) return `M ${px} ${py}`;
26+
const prevX = x(i - 1);
27+
const prevY = y(arr[i - 1].value);
28+
const midX = (px + prevX) / 2;
29+
const midY = (py + prevY) / 2;
30+
return acc + ` Q ${prevX} ${prevY}, ${midX} ${midY}`;
31+
}, '');
32+
33+
const yTicks = 5;
34+
const yStep = yRange / yTicks;
35+
const yLabels = Array.from({ length: yTicks + 1 }, (_, i) => minY + i * yStep);
1436

15-
const x = i => {
16-
const t = i / (data.length - 1 || 1);
17-
return padding + t * (width - padding * 2);
18-
};
19-
const y = v => {
20-
const range = maxY - minY || 1;
21-
const t = (v - minY) / range;
22-
return height - padding - t * (height - padding * 2);
23-
};
37+
return (
38+
<svg
39+
width="100%"
40+
viewBox={`0 0 ${width} ${height}`}
41+
preserveAspectRatio="none"
42+
style={{ background: 'transparent', borderRadius: 6 }}// give it trasparent background
43+
onMouseLeave={() => setHover(null)}
44+
>
45+
{/* Grid lines */}
46+
{yLabels.map((v, i) => (
47+
<line
48+
key={i}
49+
x1={padding + 10}
50+
x2={width - padding}
51+
y1={y(v)}
52+
y2={y(v)}
53+
stroke="#e5e7eb"
54+
strokeWidth="1"
55+
/>
56+
))}
2457

25-
const path = data.map((d, i) => `${i === 0 ? 'M' : 'L'} ${x(i)} ${y(d.value)}`).join(' ');
58+
{/* Y-axis labels */}
59+
{yLabels.map((v, i) => (
60+
<text
61+
key={i}
62+
x={padding + 2}
63+
y={y(v) + 4}
64+
textAnchor="end"
65+
fontSize="10"
66+
fill="#6b7280"
67+
>
68+
{Math.round(v).toLocaleString()}
69+
</text>
70+
))}
2671

27-
return (
28-
<svg width="100%" viewBox={`0 0 ${width} ${height}`} preserveAspectRatio="none">
72+
{/* Area fill */}
2973
<defs>
3074
<linearGradient id="g" x1="0" x2="0" y1="0" y2="1">
31-
<stop offset="0%" stopColor="#60a5fa" stopOpacity="0.4" />
32-
<stop offset="100%" stopColor="#60a5fa" stopOpacity="0.05" />
75+
<stop offset="0%" stopColor="#3b82f6" stopOpacity="0.3" />
76+
<stop offset="100%" stopColor="#3b82f6" stopOpacity="0.05" />
3377
</linearGradient>
3478
</defs>
35-
<path d={`${path} L ${x(data.length - 1)} ${height - padding} L ${x(0)} ${height - padding} Z`} fill="url(#g)" />
36-
<path d={path} fill="none" stroke="#2563eb" strokeWidth="2" strokeLinejoin="round" strokeLinecap="round" />
37-
{data.map((d, i) => (
38-
<circle key={d.date} cx={x(i)} cy={y(d.value)} r={i === data.length - 1 ? 3.5 : 2} fill="#1e3a8a" />
39-
))}
79+
<path
80+
d={`${path} L ${x(data.length - 1)} ${height - padding} L ${x(0)} ${height - padding} Z`}
81+
fill="url(#g)"
82+
/>
83+
<path
84+
d={path}
85+
fill="none"
86+
stroke="#2563eb"
87+
strokeWidth="2.5"
88+
strokeLinejoin="round"
89+
strokeLinecap="round"
90+
/>
91+
92+
{/* Data points */}
93+
{data.map((d, i) => {
94+
const cx = x(i);
95+
const cy = y(d.value);
96+
return (
97+
<circle
98+
key={d.date}
99+
cx={cx}
100+
cy={cy}
101+
r={hover === i ? 4 : 2.5}
102+
fill={hover === i ? '#1e40af' : '#1d4ed8'}
103+
onMouseEnter={() => setHover(i)}
104+
/>
105+
);
106+
})}
107+
108+
{/* Tooltip */}
109+
{hover !== null && (
110+
<>
111+
<rect
112+
x={x(hover) - 45}
113+
y={y(data[hover].value) - 45}
114+
width="90"
115+
height="32"
116+
rx="4"
117+
fill="#111827"
118+
opacity="0.85"
119+
/>
120+
<text
121+
x={x(hover)}
122+
y={y(data[hover].value) - 28}
123+
textAnchor="middle"
124+
fill="white"
125+
fontSize="10"
126+
>
127+
{new Date(data[hover].date).toLocaleDateString()}
128+
</text>
129+
<text
130+
x={x(hover)}
131+
y={y(data[hover].value) - 15}
132+
textAnchor="middle"
133+
fill="#a5b4fc"
134+
fontSize="11"
135+
fontWeight="bold"
136+
>
137+
{data[hover].value.toLocaleString()}
138+
</text>
139+
</>
140+
)}
40141
</svg>
41142
);
42143
}
@@ -67,13 +168,20 @@ export default function CountryTrendChart({ slug }) {
67168
setError(null);
68169
setSeries([]);
69170
try {
70-
const res = await fetch(`https://api.covid19api.com/dayone/country/${slug}`, { signal: controller.signal });
171+
const res = await fetch(
172+
`https://disease.sh/v3/covid-19/historical/${slug}?lastdays=all`,
173+
{ signal: controller.signal }
174+
);
71175
if (!res.ok) throw new Error('Failed to fetch country trends');
72176
const json = await res.json();
73177

74178
if (!mounted || lastSlugRef.current !== slug) return;
75179

76-
const seriesData = json.map(item => ({ date: item.Date, value: Number(item.Confirmed || 0) }));
180+
const cases = json.timeline?.cases || {};
181+
const seriesData = Object.entries(cases).map(([date, value]) => ({
182+
date,
183+
value: Number(value || 0),
184+
}));
77185

78186
seriesData.sort((a, b) => new Date(a.date) - new Date(b.date));
79187

src/pages/Covid.jsx

Lines changed: 55 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
* - [ ] Offline cache last fetch
1818
* - [ ] Extract service + hook (useCovidSummary, useCountryTrends)
1919
*/
20-
import { useEffect, useState, useCallback } from 'react';
20+
import { useEffect, useState, useCallback, useRef } from 'react';
2121
import Loading from '../components/Loading.jsx';
2222
import ErrorMessage from '../components/ErrorMessage.jsx';
2323
import Card from '../components/Card.jsx';
@@ -28,61 +28,87 @@ export default function Covid() {
2828
const [loading, setLoading] = useState(false);
2929
const [error, setError] = useState(null);
3030
const [country, setCountry] = useState('');
31+
const isFetchingRef = useRef(false);
3132

3233
const fetchSummary = useCallback(async () => {
33-
if (loading) return;
34+
if (isFetchingRef.current) return;
35+
isFetchingRef.current = true;
3436
try {
35-
setLoading(true);
37+
setLoading(true);
3638
setError(null);
37-
const res = await fetch('https://api.covid19api.com/summary');
38-
if (!res.ok) throw new Error('Failed to fetch');
39+
const res = await fetch('https://disease.sh/v3/covid-19/countries');
40+
if (!res.ok) throw new Error('Failed to fetch country data');
41+
3942
const json = await res.json();
4043
setSummary(json);
41-
} catch (e) {
42-
setError(e);
43-
} finally {
44-
setLoading(false);
44+
} catch (e) {
45+
setError(e);
46+
} finally {
47+
setLoading(false);
48+
isFetchingRef.current = false;
4549
}
46-
}, [loading]);
50+
}, []);
4751

4852
useEffect(() => {
4953
fetchSummary();
50-
}, []);
54+
}, [fetchSummary]);
5155

52-
const global = summary?.Global;
53-
const countries = summary?.Countries || [];
54-
const selected = countries.find(c => c.Slug === country);
56+
const countries = summary || [];
57+
const selected = countries.find(c => c.countryInfo.iso3 === country || c.country === country);
58+
59+
// Compute simple global summary (sum across all countries)
60+
const global = countries.reduce(
61+
(acc, c) => {
62+
acc.cases += c.cases;
63+
acc.todayCases += c.todayCases;
64+
acc.deaths += c.deaths;
65+
acc.todayDeaths += c.todayDeaths;
66+
return acc;
67+
},
68+
{ cases: 0, todayCases: 0, deaths: 0, todayDeaths: 0 }
69+
);
5570

5671
return (
5772
<div>
5873
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem', flexWrap: 'wrap' }}>
59-
<h2>COVID-19 Tracker</h2>
60-
<button onClick={fetchSummary} disabled={loading}>
61-
{loading ? 'Refreshing...' : 'Refresh'}
62-
</button>
63-
</div>
74+
<h2>COVID-19 Tracker</h2>
75+
<button onClick={fetchSummary} disabled={loading}>
76+
{loading ? 'Refreshing...' : 'Refresh'}
77+
</button>
78+
</div>
79+
6480
{loading && !summary && <Loading />}
6581
<ErrorMessage error={error} />
66-
{global && (
82+
83+
{countries.length > 0 && (
6784
<Card title="Global Stats">
68-
<p>New Confirmed: {global.NewConfirmed.toLocaleString()}</p>
69-
<p>Total Confirmed: {global.TotalConfirmed.toLocaleString()}</p>
70-
<p>Total Deaths: {global.TotalDeaths.toLocaleString()}</p>
85+
<p>New Confirmed: {global.todayCases.toLocaleString()}</p>
86+
<p>Total Confirmed: {global.cases.toLocaleString()}</p>
87+
<p>Total Deaths: {global.deaths.toLocaleString()}</p>
7188
</Card>
7289
)}
73-
<label>Select Country:
90+
91+
<label>
92+
Select Country:{' '}
7493
<select value={country} onChange={e => setCountry(e.target.value)}>
7594
<option value="">--</option>
76-
{countries.map(c => <option key={c.Slug} value={c.Slug}>{c.Country}</option>)}
95+
{countries.map(c => (
96+
<option key={c.countryInfo._id || c.country} value={c.country}>
97+
{c.country}
98+
</option>
99+
))}
77100
</select>
78101
</label>
102+
79103
{selected && (
80-
<Card title={selected.Country}>
81-
<p>New Confirmed: {selected.NewConfirmed.toLocaleString()}</p>
82-
<p>Total Confirmed: {selected.TotalConfirmed.toLocaleString()}</p>
83-
<p>Total Deaths: {selected.TotalDeaths.toLocaleString()}</p>
104+
<Card title={selected.country}>
105+
<p>New Confirmed: {selected.todayCases.toLocaleString()}</p>
106+
<p>Total Confirmed: {selected.cases.toLocaleString()}</p>
107+
<p>Total Deaths: {selected.deaths.toLocaleString()}</p>
108+
<p>Updated: {new Date(selected.updated).toLocaleString()}</p>
84109
</Card>
85110
)}
111+
86112
<CountryTrendChart slug={country} />
87113
</div>
88114
);

0 commit comments

Comments
 (0)