diff --git a/package.json b/package.json index 0489116..9fabe12 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/App.tsx b/src/App.tsx index ff61bdd..33a01ac 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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 (
+
); } diff --git a/src/api/population.ts b/src/api/population.ts index 5792d1f..009e89b 100644 --- a/src/api/population.ts +++ b/src/api/population.ts @@ -6,7 +6,7 @@ export const fetchPopulation = async ( ): Promise => { try { const response = await axios.get( - `${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!, diff --git a/src/api/prefectures.ts b/src/api/prefectures.ts index 66061b6..0f5fae4 100644 --- a/src/api/prefectures.ts +++ b/src/api/prefectures.ts @@ -4,19 +4,13 @@ import { Prefecture } from "../types/prefecture"; export const fetchPrefectures = async (): Promise => { 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); } diff --git a/src/components/PopulationChart.tsx b/src/components/PopulationChart.tsx new file mode 100644 index 0000000..c231cb5 --- /dev/null +++ b/src/components/PopulationChart.tsx @@ -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 = ({ + allCategoriesData, +}) => { + const [selectedCategory, setSelectedCategory] = useState( + categories[0] + ); + + const prefectureDataArray = allCategoriesData[selectedCategory] || []; + + if (prefectureDataArray.length === 0) { + return

選択中のカテゴリのデータがありません

; + } + + 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 ( +
+ {/* カテゴリ切り替えUI */} +
+ {categories.map((cat) => ( + + ))} +
+ + + + + + + + + {prefNames.map((pName) => ( + + ))} + + +
+ ); +}; + +export default PopulationChart; diff --git a/src/components/PopulationSelector.tsx b/src/components/PopulationSelector.tsx index d0ec13c..a295cd3 100644 --- a/src/components/PopulationSelector.tsx +++ b/src/components/PopulationSelector.tsx @@ -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[]; diff --git a/src/components/PrefectureCheckbox.tsx b/src/components/PrefectureCheckbox.tsx index 039dbd3..5762480 100644 --- a/src/components/PrefectureCheckbox.tsx +++ b/src/components/PrefectureCheckbox.tsx @@ -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 = ({ 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 (
-

都道府県選択

- {prefectures.map((prefecture) => ( -
+

都道府県を選択

+ + + {prefectures.map((pref) => ( +
- handleCheckboxChange(prefecture.prefCode.toString()) - } + checked={selectedPrefectures.includes(pref.prefCode)} + onChange={() => onSelect(pref.prefCode)} /> - +
))}
diff --git a/src/hooks/usePopulation.ts b/src/hooks/usePopulation.ts new file mode 100644 index 0000000..2e654ad --- /dev/null +++ b/src/hooks/usePopulation.ts @@ -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([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(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; diff --git a/src/pages/PopulationGraphPage.tsx b/src/pages/PopulationGraphPage.tsx new file mode 100644 index 0000000..00fe377 --- /dev/null +++ b/src/pages/PopulationGraphPage.tsx @@ -0,0 +1,111 @@ +import React, { useEffect, useState } from "react"; +import PrefectureCheckbox from "../components/PrefectureCheckbox"; +import PopulationChart from "../components/PopulationChart"; +import usePrefecturesStore from "../store/usePrefecturesStore"; +import { fetchPrefectures } from "../api/prefectures"; +import { fetchPopulation } from "../api/population"; +import { Prefecture } from "../types/prefecture"; +import { PopulationData } from "../types/population"; + +interface PrefectureCategoryData { + prefName: string; + data: PopulationData[]; +} + +interface AllCategoriesData { + [categoryName: string]: PrefectureCategoryData[]; +} + +const categories = ["総人口", "年少人口", "生産年齢人口", "老年人口"]; + +const PopulationGraphPage: React.FC = () => { + const { selectedPrefectures, prefectures, setPrefectures } = + usePrefecturesStore(); + const [allCategoriesData, setAllCategoriesData] = useState( + {} + ); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + const initializePrefectures = async (): Promise => { + try { + const fetchedPrefectures: Prefecture[] = await fetchPrefectures(); + setPrefectures(fetchedPrefectures); + } catch (err) { + setError("都道府県データの取得に失敗しました:" + err); + } + }; + initializePrefectures(); + }, [setPrefectures]); + + useEffect(() => { + const fetchPopulationData = async () => { + setLoading(true); + setError(null); + + const newAllCategoriesData: AllCategoriesData = { + 総人口: [], + 年少人口: [], + 生産年齢人口: [], + 老年人口: [], + }; + + try { + for (const prefCode of selectedPrefectures) { + const categoriesData = await fetchPopulation(prefCode.toString()); + const prefName = + prefectures.find((p) => p.prefCode === Number(prefCode)) + ?.prefName || prefCode.toString(); + + for (const catName of categories) { + const catData = categoriesData.find((c) => c.label === catName); + if (catData) { + newAllCategoriesData[catName].push({ + prefName: prefName, + data: catData.data, + }); + } + } + } + setAllCategoriesData(newAllCategoriesData); + } catch (err) { + setError("人口データの取得に失敗しました:" + err); + } finally { + setLoading(false); + } + }; + + if (selectedPrefectures.length > 0) { + fetchPopulationData(); + } else { + setAllCategoriesData({ + 総人口: [], + 年少人口: [], + 生産年齢人口: [], + 老年人口: [], + }); + } + }, [selectedPrefectures, prefectures]); + + const { togglePrefecture, selectAllPrefectures, clearSelection } = + usePrefecturesStore.getState(); + + return ( +
+

人口推移グラフ

+ togglePrefecture(prefCode)} + onSelectAll={(prefCodes: number[]) => selectAllPrefectures(prefCodes)} + onClearSelection={clearSelection} + /> + {selectedPrefectures.length === 0 &&

都道府県を選択してください。

} + {loading &&

データを取得中...

} + {error &&

{error}

} + +
+ ); +}; + +export default PopulationGraphPage; diff --git a/src/store/usePrefecturesStore.ts b/src/store/usePrefecturesStore.ts new file mode 100644 index 0000000..ac557eb --- /dev/null +++ b/src/store/usePrefecturesStore.ts @@ -0,0 +1,35 @@ +import { create } from "zustand"; + +interface Prefecture { + prefCode: number; + prefName: string; +} + +interface PrefecturesState { + prefectures: Prefecture[]; + selectedPrefectures: number[]; + setPrefectures: (prefectures: Prefecture[]) => void; + togglePrefecture: (prefCode: number) => void; + selectAllPrefectures: (prefCodes: number[]) => void; + clearSelection: () => void; + selectAll: (prefCodes: number[]) => void; +} + +const usePrefecturesStore = create((set) => ({ + prefectures: [], + selectedPrefectures: [], + setPrefectures: (prefectures) => set({ prefectures }), + togglePrefecture: (prefCode) => + set((state) => ({ + selectedPrefectures: state.selectedPrefectures.includes(prefCode) + ? state.selectedPrefectures.filter((code) => code !== prefCode) + : [...state.selectedPrefectures, prefCode], + })), + selectAllPrefectures: (prefCodes: number[]) => + set({ selectedPrefectures: prefCodes }), + clearSelection: () => set({ selectedPrefectures: [] }), + + selectAll: (prefCodes: number[]) => set({ selectedPrefectures: prefCodes }), +})); + +export default usePrefecturesStore; diff --git a/src/tests/PopulationChart.test.tsx b/src/tests/PopulationChart.test.tsx new file mode 100644 index 0000000..c1e905c --- /dev/null +++ b/src/tests/PopulationChart.test.tsx @@ -0,0 +1,80 @@ +import React from "react"; +import { render, screen, fireEvent } from "@testing-library/react"; +import PopulationChart from "../components/PopulationChart"; + +jest.mock("recharts", () => { + const OriginalRecharts = jest.requireActual("recharts"); + return { + ...OriginalRecharts, + ResponsiveContainer: ({ children }: { children: React.ReactNode }) => ( +
{children}
+ ), + }; +}); + +const mockAllCategoriesData = { + 総人口: [ + { + prefName: "北海道", + data: [ + { year: 1960, value: 5000, rate: 0 }, + { year: 1965, value: 6000, rate: 0 }, + ], + }, + { + prefName: "青森県", + data: [ + { year: 1960, value: 4500, rate: 0 }, + { year: 1965, value: 5500, rate: 0 }, + ], + }, + ], + 年少人口: [ + { + prefName: "北海道", + data: [ + { year: 1960, value: 2000, rate: 0 }, + { year: 1965, value: 2500, rate: 0 }, + ], + }, + { + prefName: "青森県", + data: [ + { year: 1960, value: 1800, rate: 0 }, + { year: 1965, value: 2300, rate: 0 }, + ], + }, + ], + 生産年齢人口: [], + 老年人口: [], +}; + +describe("PopulationChartコンポーネントのテスト", () => { + it("グラフが正しくレンダリングされる", () => { + render(); + expect(screen.getByText("総人口")).toBeInTheDocument(); + expect(screen.getByText("年少人口")).toBeInTheDocument(); + expect(screen.getByText("生産年齢人口")).toBeInTheDocument(); + expect(screen.getByText("老年人口")).toBeInTheDocument(); + }); + + it("カテゴリ切り替えボタンが動作する", () => { + render(); + const button = screen.getByText("年少人口"); + fireEvent.click(button); + expect(button).toHaveStyle("background-color: #007bff"); + }); + + it("データがない場合にエラーメッセージが表示される", () => { + const emptyData = { + 総人口: [], + 年少人口: [], + 生産年齢人口: [], + 老年人口: [], + }; + render(); + expect( + screen.getByText("選択中のカテゴリのデータがありません") + ).toBeInTheDocument(); + }); +}); diff --git a/src/tests/PrefectureCheckbox.test.tsx b/src/tests/PrefectureCheckbox.test.tsx index 0f84690..223997e 100644 --- a/src/tests/PrefectureCheckbox.test.tsx +++ b/src/tests/PrefectureCheckbox.test.tsx @@ -1,54 +1,51 @@ import React from "react"; import { render, screen, fireEvent } from "@testing-library/react"; -import PrefectureCheckbox from "./../components/PrefectureCheckbox"; -import { Prefecture } from "../types/prefecture"; +import PrefectureCheckbox from "../components/PrefectureCheckbox"; -const mockPrefectures: Prefecture[] = [ +const mockPrefectures = [ { prefCode: 1, prefName: "北海道" }, { prefCode: 2, prefName: "青森県" }, + { prefCode: 3, prefName: "岩手県" }, ]; -describe("PrefectureCheckbox", () => { - it("都道府県のチェックボックスを表示する", () => { - render( - {}} - /> - ); - - mockPrefectures.forEach((pref) => { - expect(screen.getByLabelText(pref.prefName)).toBeInTheDocument(); - }); - }); - - it("チェックボックスをクリックすると選択状態が変更される", () => { +describe("PrefectureCheckboxコンポーネント", () => { + it("すべて選択ボタンが正しく動作する", () => { const mockOnSelect = jest.fn(); + const mockOnSelectAll = jest.fn(); + const mockOnClearSelection = jest.fn(); + render( ); - const checkbox = screen.getByLabelText("北海道"); - fireEvent.click(checkbox); + const selectAllButton = screen.getByText("すべて選択"); + fireEvent.click(selectAllButton); - expect(mockOnSelect).toHaveBeenCalledWith(["1"]); + expect(mockOnSelectAll).toHaveBeenCalledWith([1, 2, 3]); }); - it("選択済みの都道府県のチェックボックスはチェックされている", () => { + it("選択をクリアボタンが正しく動作する", () => { + const mockOnSelect = jest.fn(); + const mockOnSelectAll = jest.fn(); + const mockOnClearSelection = jest.fn(); + render( {}} + onSelect={mockOnSelect} + onSelectAll={mockOnSelectAll} + onClearSelection={mockOnClearSelection} /> ); - expect(screen.getByLabelText("北海道")).toBeChecked(); - expect(screen.getByLabelText("青森県")).not.toBeChecked(); + const clearButton = screen.getByText("選択をクリア"); + fireEvent.click(clearButton); + + expect(mockOnClearSelection).toHaveBeenCalled(); }); }); diff --git a/src/tests/population.test.ts b/src/tests/population.test.ts index ef58e3c..0a91fdb 100644 --- a/src/tests/population.test.ts +++ b/src/tests/population.test.ts @@ -30,7 +30,7 @@ describe("fetchPopulation", () => { expect(result).toEqual(mockData); expect(mockedAxios.get).toHaveBeenCalledWith( - `${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!, @@ -48,7 +48,7 @@ describe("fetchPopulation", () => { "Error fetching population data: Error: API Error" ); expect(mockedAxios.get).toHaveBeenCalledWith( - `${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!, diff --git a/src/tests/prefectures.test.ts b/src/tests/prefectures.test.ts index f7fb20a..a0997f1 100644 --- a/src/tests/prefectures.test.ts +++ b/src/tests/prefectures.test.ts @@ -1,5 +1,5 @@ import axios from "axios"; -import { fetchPrefectures } from "./../api/prefectures"; +import { fetchPrefectures } from "../api/prefectures"; import { Prefecture } from "../types/prefecture"; jest.mock("axios"); @@ -10,20 +10,20 @@ describe("fetchPrefectures", () => { const mockData: Prefecture[] = [ { prefCode: 1, prefName: "北海道" }, { prefCode: 2, prefName: "青森県" }, - { prefCode: 3, prefName: "宮城県" }, + { prefCode: 3, prefName: "岩手県" }, ]; - mockedAxios.get.mockResolvedValue({ status: 200, data: mockData }); + mockedAxios.get.mockResolvedValue({ + data: { message: null, result: mockData }, + }); const result = await fetchPrefectures(); expect(result).toEqual(mockData); expect(mockedAxios.get).toHaveBeenCalledWith( - `${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 }, } ); }); @@ -31,14 +31,8 @@ describe("fetchPrefectures", () => { it("APIがエラーを返した場合、エラーをスローする", async () => { mockedAxios.get.mockRejectedValue(new Error("API Error")); - await expect(fetchPrefectures()).rejects.toThrow("API Error"); - expect(mockedAxios.get).toHaveBeenCalledWith( - `${process.env.REACT_APP_YUMEMI_API_URL}/prefectures`, - { - headers: { - "X-API-KEY": process.env.REACT_APP_YUMEMI_API_KEY!, - }, - } + await expect(fetchPrefectures()).rejects.toThrow( + "Error fetching prefectures: Error: API Error" ); }); }); diff --git a/src/tests/prefecturesStore.test.ts b/src/tests/prefecturesStore.test.ts new file mode 100644 index 0000000..996ce22 --- /dev/null +++ b/src/tests/prefecturesStore.test.ts @@ -0,0 +1,55 @@ +import { act } from "react"; +import usePrefecturesStore from "../store/usePrefecturesStore"; + +describe("都道府県ストアの動作", () => { + beforeEach(() => { + const { getState, setState } = usePrefecturesStore; + setState(() => getState()); + }); + + it("都道府県の選択状態を切り替えられる", () => { + const { selectedPrefectures, togglePrefecture } = + usePrefecturesStore.getState(); + + expect(selectedPrefectures).toEqual([]); + + act(() => { + togglePrefecture(1); + }); + expect(usePrefecturesStore.getState().selectedPrefectures).toEqual([1]); + + act(() => { + togglePrefecture(1); + }); + expect(usePrefecturesStore.getState().selectedPrefectures).toEqual([]); + }); + + it("全ての都道府県を選択できる", () => { + const { selectedPrefectures, selectAll } = usePrefecturesStore.getState(); + + expect(selectedPrefectures).toEqual([]); + + act(() => { + selectAll([1, 2, 3]); + }); + expect(usePrefecturesStore.getState().selectedPrefectures).toEqual([ + 1, 2, 3, + ]); + }); + + it("選択状態を全て解除できる", () => { + const { clearSelection, selectAll } = usePrefecturesStore.getState(); + + act(() => { + selectAll([1, 2, 3]); + }); + expect(usePrefecturesStore.getState().selectedPrefectures).toEqual([ + 1, 2, 3, + ]); + + act(() => { + clearSelection(); + }); + expect(usePrefecturesStore.getState().selectedPrefectures).toEqual([]); + }); +}); diff --git a/src/tests/setupTests.ts b/src/tests/setupTests.ts index 1dd407a..6c41415 100644 --- a/src/tests/setupTests.ts +++ b/src/tests/setupTests.ts @@ -3,3 +3,9 @@ // expect(element).toHaveTextContent(/react/i) // learn more: https://github.com/testing-library/jest-dom import "@testing-library/jest-dom"; + +global.ResizeObserver = class { + observe() {} + unobserve() {} + disconnect() {} +}; diff --git a/yarn.lock b/yarn.lock index 0b6ec53..8af7cb4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1054,7 +1054,7 @@ "@babel/plugin-transform-modules-commonjs" "^7.25.9" "@babel/plugin-transform-typescript" "^7.25.9" -"@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.16.3", "@babel/runtime@^7.8.4": +"@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.16.3", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7": version "7.26.0" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.0.tgz#8600c2f595f277c60815256418b85356a65173c1" integrity sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw== @@ -2170,6 +2170,57 @@ dependencies: "@types/node" "*" +"@types/d3-array@^3.0.3": + version "3.2.1" + resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-3.2.1.tgz#1f6658e3d2006c4fceac53fde464166859f8b8c5" + integrity sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg== + +"@types/d3-color@*": + version "3.1.3" + resolved "https://registry.yarnpkg.com/@types/d3-color/-/d3-color-3.1.3.tgz#368c961a18de721da8200e80bf3943fb53136af2" + integrity sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A== + +"@types/d3-ease@^3.0.0": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/d3-ease/-/d3-ease-3.0.2.tgz#e28db1bfbfa617076f7770dd1d9a48eaa3b6c51b" + integrity sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA== + +"@types/d3-interpolate@^3.0.1": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz#412b90e84870285f2ff8a846c6eb60344f12a41c" + integrity sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA== + dependencies: + "@types/d3-color" "*" + +"@types/d3-path@*": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@types/d3-path/-/d3-path-3.1.0.tgz#2b907adce762a78e98828f0b438eaca339ae410a" + integrity sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ== + +"@types/d3-scale@^4.0.2": + version "4.0.8" + resolved "https://registry.yarnpkg.com/@types/d3-scale/-/d3-scale-4.0.8.tgz#d409b5f9dcf63074464bf8ddfb8ee5a1f95945bb" + integrity sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ== + dependencies: + "@types/d3-time" "*" + +"@types/d3-shape@^3.1.0": + version "3.1.6" + resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-3.1.6.tgz#65d40d5a548f0a023821773e39012805e6e31a72" + integrity sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA== + dependencies: + "@types/d3-path" "*" + +"@types/d3-time@*", "@types/d3-time@^3.0.0": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/d3-time/-/d3-time-3.0.4.tgz#8472feecd639691450dd8000eb33edd444e1323f" + integrity sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g== + +"@types/d3-timer@^3.0.0": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/d3-timer/-/d3-timer-3.0.2.tgz#70bbda77dc23aa727413e22e214afa3f0e852f70" + integrity sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw== + "@types/eslint-scope@^3.7.7": version "3.7.7" resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.7.tgz#3108bd5f18b0cdb277c867b3dd449c9ed7079ac5" @@ -3633,6 +3684,11 @@ cliui@^8.0.1: strip-ansi "^6.0.1" wrap-ansi "^7.0.0" +clsx@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999" + integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA== + co@^4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" @@ -4061,6 +4117,77 @@ csstype@^3.0.2: resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81" integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== +"d3-array@2 - 3", "d3-array@2.10.0 - 3", d3-array@^3.1.6: + version "3.2.4" + resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-3.2.4.tgz#15fec33b237f97ac5d7c986dc77da273a8ed0bb5" + integrity sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg== + dependencies: + internmap "1 - 2" + +"d3-color@1 - 3": + version "3.1.0" + resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-3.1.0.tgz#395b2833dfac71507f12ac2f7af23bf819de24e2" + integrity sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA== + +d3-ease@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-3.0.1.tgz#9658ac38a2140d59d346160f1f6c30fda0bd12f4" + integrity sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w== + +"d3-format@1 - 3": + version "3.1.0" + resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-3.1.0.tgz#9260e23a28ea5cb109e93b21a06e24e2ebd55641" + integrity sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA== + +"d3-interpolate@1.2.0 - 3", d3-interpolate@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz#3c47aa5b32c5b3dfb56ef3fd4342078a632b400d" + integrity sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g== + dependencies: + d3-color "1 - 3" + +d3-path@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-3.1.0.tgz#22df939032fb5a71ae8b1800d61ddb7851c42526" + integrity sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ== + +d3-scale@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-4.0.2.tgz#82b38e8e8ff7080764f8dcec77bd4be393689396" + integrity sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ== + dependencies: + d3-array "2.10.0 - 3" + d3-format "1 - 3" + d3-interpolate "1.2.0 - 3" + d3-time "2.1.1 - 3" + d3-time-format "2 - 4" + +d3-shape@^3.1.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-3.2.0.tgz#a1a839cbd9ba45f28674c69d7f855bcf91dfc6a5" + integrity sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA== + dependencies: + d3-path "^3.1.0" + +"d3-time-format@2 - 4": + version "4.1.0" + resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-4.1.0.tgz#7ab5257a5041d11ecb4fe70a5c7d16a195bb408a" + integrity sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg== + dependencies: + d3-time "1 - 3" + +"d3-time@1 - 3", "d3-time@2.1.1 - 3", d3-time@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-3.1.0.tgz#9310db56e992e3c0175e1ef385e545e48a9bb5c7" + integrity sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q== + dependencies: + d3-array "2 - 3" + +d3-timer@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-3.0.1.tgz#6284d2a2708285b1abb7e201eda4380af35e63b0" + integrity sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA== + damerau-levenshtein@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7" @@ -4132,6 +4259,11 @@ debug@^3.2.7: dependencies: ms "^2.1.1" +decimal.js-light@^2.4.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/decimal.js-light/-/decimal.js-light-2.5.1.tgz#134fd32508f19e208f4fb2f8dac0d2626a867934" + integrity sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg== + decimal.js@^10.2.1, decimal.js@^10.4.2: version "10.4.3" resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.4.3.tgz#1044092884d245d1b7f65725fa4ad4c6f781cc23" @@ -4295,6 +4427,14 @@ dom-converter@^0.2.0: dependencies: utila "~0.4" +dom-helpers@^5.0.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.1.tgz#d9400536b2bf8225ad98fe052e029451ac40e902" + integrity sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA== + dependencies: + "@babel/runtime" "^7.8.7" + csstype "^3.0.2" + dom-serializer@0: version "0.2.2" resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51" @@ -5008,7 +5148,7 @@ etag@~1.8.1: resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== -eventemitter3@^4.0.0: +eventemitter3@^4.0.0, eventemitter3@^4.0.1: version "4.0.7" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== @@ -5101,6 +5241,11 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== +fast-equals@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/fast-equals/-/fast-equals-5.0.1.tgz#a4eefe3c5d1c0d021aeed0bc10ba5e0c12ee405d" + integrity sha512-WF1Wi8PwwSY7/6Kx0vKXtw8RwuSGoM1bvDaJbu7MxDlR1vovZjIAKrnzyrThgAjm6JDTu0fVgWXDlMGspodfoQ== + fast-glob@^3.2.9, fast-glob@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" @@ -5872,6 +6017,11 @@ internal-slot@^1.0.7: hasown "^2.0.0" side-channel "^1.0.4" +"internmap@1 - 2": + version "2.0.3" + resolved "https://registry.yarnpkg.com/internmap/-/internmap-2.0.3.tgz#6685f23755e43c524e251d29cbc97248e3061009" + integrity sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg== + ipaddr.js@1.9.1: version "1.9.1" resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" @@ -8705,7 +8855,7 @@ prompts@^2.0.1, prompts@^2.4.2: kleur "^3.0.3" sisteransi "^1.0.5" -prop-types@^15.8.1: +prop-types@^15.6.2, prop-types@^15.8.1: version "15.8.1" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== @@ -8859,7 +9009,7 @@ react-is@^17.0.1: resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== -react-is@^18.0.0: +react-is@^18.0.0, react-is@^18.3.1: version "18.3.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e" integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg== @@ -8924,6 +9074,25 @@ react-scripts@5.0.1: optionalDependencies: fsevents "^2.3.2" +react-smooth@^4.0.0: + version "4.0.4" + resolved "https://registry.yarnpkg.com/react-smooth/-/react-smooth-4.0.4.tgz#a5875f8bb61963ca61b819cedc569dc2453894b4" + integrity sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q== + dependencies: + fast-equals "^5.0.1" + prop-types "^15.8.1" + react-transition-group "^4.4.5" + +react-transition-group@^4.4.5: + version "4.4.5" + resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.5.tgz#e53d4e3f3344da8521489fbef8f2581d42becdd1" + integrity sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g== + dependencies: + "@babel/runtime" "^7.5.5" + dom-helpers "^5.0.1" + loose-envify "^1.4.0" + prop-types "^15.6.2" + react@^19.0.0: version "19.0.0" resolved "https://registry.yarnpkg.com/react/-/react-19.0.0.tgz#6e1969251b9f108870aa4bff37a0ce9ddfaaabdd" @@ -8965,6 +9134,27 @@ readdirp@~3.6.0: dependencies: picomatch "^2.2.1" +recharts-scale@^0.4.4: + version "0.4.5" + resolved "https://registry.yarnpkg.com/recharts-scale/-/recharts-scale-0.4.5.tgz#0969271f14e732e642fcc5bd4ab270d6e87dd1d9" + integrity sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w== + dependencies: + decimal.js-light "^2.4.1" + +recharts@^2.15.0: + version "2.15.0" + resolved "https://registry.yarnpkg.com/recharts/-/recharts-2.15.0.tgz#0b77bff57a43885df9769ae649a14cb1a7fe19aa" + integrity sha512-cIvMxDfpAmqAmVgc4yb7pgm/O1tmmkl/CjrvXuW+62/+7jj/iF9Ykm+hb/UJt42TREHMyd3gb+pkgoa2MxgDIw== + dependencies: + clsx "^2.0.0" + eventemitter3 "^4.0.1" + lodash "^4.17.21" + react-is "^18.3.1" + react-smooth "^4.0.0" + recharts-scale "^0.4.4" + tiny-invariant "^1.3.1" + victory-vendor "^36.6.8" + recursive-readdir@^2.2.2: version "2.2.3" resolved "https://registry.yarnpkg.com/recursive-readdir/-/recursive-readdir-2.2.3.tgz#e726f328c0d69153bcabd5c322d3195252379372" @@ -10033,6 +10223,11 @@ thunky@^1.0.2: resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d" integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA== +tiny-invariant@^1.3.1: + version "1.3.3" + resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz#46680b7a873a0d5d10005995eb90a70d74d60127" + integrity sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg== + tmpl@1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc" @@ -10397,6 +10592,26 @@ vary@~1.1.2: resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== +victory-vendor@^36.6.8: + version "36.9.2" + resolved "https://registry.yarnpkg.com/victory-vendor/-/victory-vendor-36.9.2.tgz#668b02a448fa4ea0f788dbf4228b7e64669ff801" + integrity sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ== + dependencies: + "@types/d3-array" "^3.0.3" + "@types/d3-ease" "^3.0.0" + "@types/d3-interpolate" "^3.0.1" + "@types/d3-scale" "^4.0.2" + "@types/d3-shape" "^3.1.0" + "@types/d3-time" "^3.0.0" + "@types/d3-timer" "^3.0.0" + d3-array "^3.1.6" + d3-ease "^3.0.1" + d3-interpolate "^3.0.1" + d3-scale "^4.0.2" + d3-shape "^3.1.0" + d3-time "^3.0.0" + d3-timer "^3.0.1" + w3c-hr-time@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz#0a89cdf5cc15822df9c360543676963e0cc308cd" @@ -11013,3 +11228,8 @@ yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== + +zustand@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/zustand/-/zustand-5.0.2.tgz#f7595ada55a565f1fd6464f002a91e701ee0cfca" + integrity sha512-8qNdnJVJlHlrKXi50LDqqUNmUbuBjoKLrYQBnoChIbVph7vni+sY+YpvdjXG9YLd/Bxr6scMcR+rm5H3aSqPaw==