Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

マルチカテゴリ対応の人口推移グラフ機能追加および関連修正 #7

Merged
merged 18 commits into from
Dec 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
ba465c2
refactor: コメントを削除
crab85193 Dec 17, 2024
194d5df
fix: APIのURLを正しいものに変更
crab85193 Dec 17, 2024
21f202a
feat: Zustandを用いて都道府県ストアを実装
crab85193 Dec 18, 2024
4ff5938
test: 都道府県ストアのテストコードを追加
crab85193 Dec 18, 2024
27d7c6a
feat: グラフ描画コンポーネント PopulationChart.tsx を作成
crab85193 Dec 18, 2024
346bd8a
feat: usePopulationカスタムフックを作成
crab85193 Dec 18, 2024
cfe5641
test: PopulationChartコンポーネントのテストを追加
crab85193 Dec 18, 2024
272ce13
fix: PrefectureCheckboxコンポーネントとテストを修正
crab85193 Dec 18, 2024
596db37
feat: usePrefecturesStoreを作成
crab85193 Dec 18, 2024
972662d
feat: PopulationGraphPageを作成
crab85193 Dec 18, 2024
0c0b513
feat: レイアウト修正とグラフ描画機能の統合準備
crab85193 Dec 18, 2024
a6f3100
fix: 人口データ取得ロジックの改善と型安全性の向上
crab85193 Dec 18, 2024
505ceeb
fix: 都道府県データ取得APIのエラーハンドリング修正
crab85193 Dec 18, 2024
a92b47a
feat: グラフ描画に複数都道府県対応を追加
crab85193 Dec 18, 2024
438c042
fix: チェックボックスの選択状態管理のバグ修正
crab85193 Dec 18, 2024
3e027ca
feat: 都道府県データと人口データの連携処理を実装
crab85193 Dec 18, 2024
86d1495
test: PopulationChartコンポーネントのテストに都道府県名の検証を追加
crab85193 Dec 18, 2024
806af23
feat: マルチカテゴリ・マルチ都道府県対応のグラフ機能を実装
crab85193 Dec 18, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-scripts": "5.0.1",
"recharts": "^2.15.0",
"typescript": "^4.4.2",
"web-vitals": "^2.1.0"
"web-vitals": "^2.1.0",
"zustand": "^5.0.2"
},
"scripts": {
"start": "react-scripts start",
Expand Down
2 changes: 2 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import React from "react";
import Header from "./components/Header";
import PopulationGraphPage from "./pages/PopulationGraphPage";
import "./assets/styles/reset.css";

function App() {
return (
<div className="layout">
<Header />
<PopulationGraphPage />
</div>
);
}
Expand Down
2 changes: 1 addition & 1 deletion src/api/population.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export const fetchPopulation = async (
): Promise<PopulationCategory[]> => {
try {
const response = await axios.get<PopulationResponse>(
`${process.env.REACT_APP_YUMEMI_API_URL}/population/${prefCode}`,
`${process.env.REACT_APP_YUMEMI_API_URL}/api/v1/population/composition/perYear?prefCode=${prefCode}`,
{
headers: {
"X-API-KEY": process.env.REACT_APP_YUMEMI_API_KEY!,
Expand Down
12 changes: 3 additions & 9 deletions src/api/prefectures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,13 @@ import { Prefecture } from "../types/prefecture";
export const fetchPrefectures = async (): Promise<Prefecture[]> => {
try {
const response = await axios.get(
`${process.env.REACT_APP_YUMEMI_API_URL}/prefectures`,
`${process.env.REACT_APP_YUMEMI_API_URL}/api/v1/prefectures`,
{
headers: {
"X-API-KEY": process.env.REACT_APP_YUMEMI_API_KEY!,
},
headers: { "X-API-KEY": process.env.REACT_APP_YUMEMI_API_KEY },
}
);

if (response.status !== 200) {
throw new Error("Failed to fetch prefectures");
}

return response.data as Prefecture[];
return response.data.result as Prefecture[];
} catch (error) {
throw new Error("Error fetching prefectures: " + error);
}
Expand Down
129 changes: 129 additions & 0 deletions src/components/PopulationChart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import React, { useState } from "react";
import {
LineChart,
Line,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
Legend,
ResponsiveContainer,
} from "recharts";

interface PopulationData {
year: number;
value: number;
rate: number;
}

interface PrefectureCategoryData {
prefName: string;
data: PopulationData[];
}

interface AllCategoriesData {
[categoryName: string]: PrefectureCategoryData[];
}

interface PopulationChartProps {
allCategoriesData: AllCategoriesData;
}

const categories = ["総人口", "年少人口", "生産年齢人口", "老年人口"];

const colorMap: { [prefName: string]: string } = {};

const generateRandomColor = (): string => {
const letters = "0123456789ABCDEF";
let color = "#";
for (let i = 0; i < 6; i++) {
color += letters[Math.floor(Math.random() * 16)];
}
return color;
};

const getColorForPrefName = (pName: string) => {
if (!colorMap[pName]) {
colorMap[pName] = generateRandomColor();
}
return colorMap[pName];
};

const PopulationChart: React.FC<PopulationChartProps> = ({
allCategoriesData,
}) => {
const [selectedCategory, setSelectedCategory] = useState<string>(
categories[0]
);

const prefectureDataArray = allCategoriesData[selectedCategory] || [];

if (prefectureDataArray.length === 0) {
return <p>選択中のカテゴリのデータがありません</p>;
}

const allYears = prefectureDataArray.flatMap((p) =>
p.data.map((d) => d.year)
);
const uniqueYears = Array.from(new Set(allYears)).sort((a, b) => a - b);

const combinedData = uniqueYears.map((year) => {
const entry: { [key: string]: number | string } = { year };
for (const pData of prefectureDataArray) {
const yearData = pData.data.find((d) => d.year === year);
entry[pData.prefName] = yearData ? yearData.value : 0;
}
return entry;
});

const prefNames = prefectureDataArray.map((p) => p.prefName);

return (
<div style={{ width: "100%", height: "500px" }}>
{/* カテゴリ切り替えUI */}
<div style={{ marginBottom: "10px" }}>
{categories.map((cat) => (
<button
key={cat}
onClick={() => setSelectedCategory(cat)}
style={{
marginRight: "5px",
padding: "5px 10px",
backgroundColor: cat === selectedCategory ? "#007bff" : "#e0e0e0",
color: cat === selectedCategory ? "#fff" : "#000",
border: "none",
borderRadius: "3px",
cursor: "pointer",
}}
>
{cat}
</button>
))}
</div>

<ResponsiveContainer width="100%" height="100%">
<LineChart
data={combinedData}
margin={{ top: 20, right: 30, left: 20, bottom: 5 }}
>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="year" />
<YAxis />
<Tooltip />
<Legend />
{prefNames.map((pName) => (
<Line
key={pName}
type="monotone"
dataKey={pName}
name={pName}
stroke={getColorForPrefName(pName)}
/>
))}
</LineChart>
</ResponsiveContainer>
</div>
);
};

export default PopulationChart;
6 changes: 3 additions & 3 deletions src/components/PopulationSelector.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { useState } from "react";
import { fetchPopulation } from "../api/population"; // 人口データを取得するAPI
import { Prefecture } from "../types/prefecture"; // 都道府県の型
import { PopulationCategory } from "../types/population"; // 人口データのカテゴリ型
import { fetchPopulation } from "../api/population";
import { Prefecture } from "../types/prefecture";
import { PopulationCategory } from "../types/population";

interface PopulationSelectorProps {
prefectures: Prefecture[];
Expand Down
43 changes: 20 additions & 23 deletions src/components/PrefectureCheckbox.tsx
Original file line number Diff line number Diff line change
@@ -1,43 +1,40 @@
import React from "react";
import { Prefecture } from "../types/prefecture";
import usePrefecturesStore from "../store/usePrefecturesStore";

interface PrefectureCheckboxProps {
prefectures: Prefecture[];
selectedPrefectures: string[];
onSelect: (selected: string[]) => void;
onSelect: (prefCode: number) => void;
onSelectAll: (prefCodes: number[]) => void;
onClearSelection: () => void;
}

const PrefectureCheckbox: React.FC<PrefectureCheckboxProps> = ({
prefectures,
selectedPrefectures,
onSelect,
onSelectAll,
onClearSelection,
}) => {
const handleCheckboxChange = (prefCode: string) => {
if (selectedPrefectures.includes(prefCode)) {
onSelect(selectedPrefectures.filter((code) => code !== prefCode));
} else {
onSelect([...selectedPrefectures, prefCode]);
}
};
const { selectedPrefectures } = usePrefecturesStore();

return (
<div>
<h3>都道府県選択</h3>
{prefectures.map((prefecture) => (
<div key={prefecture.prefCode}>
<h3>都道府県を選択</h3>
<button
onClick={() => onSelectAll(prefectures.map((pref) => pref.prefCode))}
>
すべて選択
</button>
<button onClick={onClearSelection}>選択をクリア</button>
{prefectures.map((pref) => (
<div key={pref.prefCode}>
<input
id={`pref-${pref.prefCode}`}
type="checkbox"
id={prefecture.prefCode.toString()}
checked={selectedPrefectures.includes(
prefecture.prefCode.toString()
)}
onChange={() =>
handleCheckboxChange(prefecture.prefCode.toString())
}
checked={selectedPrefectures.includes(pref.prefCode)}
onChange={() => onSelect(pref.prefCode)}
/>
<label htmlFor={prefecture.prefCode.toString()}>
{prefecture.prefName}
</label>
<label htmlFor={`pref-${pref.prefCode}`}>{pref.prefName}</label>
</div>
))}
</div>
Expand Down
32 changes: 32 additions & 0 deletions src/hooks/usePopulation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { useState, useEffect } from "react";
import { fetchPopulation } from "../api/population";
import { PopulationCategory } from "../types/population";

const usePopulation = (prefCode: number | null) => {
const [data, setData] = useState<PopulationCategory[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);

useEffect(() => {
if (!prefCode) return;

const fetchData = async () => {
setLoading(true);
setError(null);
try {
const result = await fetchPopulation(prefCode.toString());
setData(result);
} catch (err) {
setError("人口データの取得に失敗しました. " + err);
} finally {
setLoading(false);
}
};

fetchData();
}, [prefCode]);

return { data, loading, error };
};

export default usePopulation;
Loading
Loading