Skip to content

Commit 3909e2c

Browse files
authored
Setting up structures for dashboard usage (#174)
* Recharts install * Added year filter action to store; updated filters to clear when switching between grants and dashboard; set up basic chart and checkboxes but fixing sync * Fixed filtering by changing processGrantData * Updating border sizes * Calendar date selected format * Sample grant calculations * Updating filter checkboxes to persist on refresh * Calculation functions for grant metrics and tests * Attempting to fix build errors
1 parent cbe8aa3 commit 3909e2c

30 files changed

+1217
-72
lines changed

frontend/package-lock.json

Lines changed: 369 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
"core-js": "^3.38.1",
2121
"framer-motion": "^11.16.0",
2222
"fuse": "^0.12.1",
23-
2423
"fuse.js": "^7.1.0",
2524
"mobx": "^6.13.2",
2625
"mobx-react-lite": "^4.1.0",
@@ -31,6 +30,7 @@
3130
"react-icons": "^5.4.0",
3231
"react-router-dom": "^6.26.2",
3332
"react-transition-group": "^4.4.5",
33+
"recharts": "^3.2.1",
3434
"satcheljs": "^4.3.1"
3535
},
3636
"devDependencies": {

frontend/src/external/bcanSatchel/actions.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ export const updateEndDateFilter = action (
4949
'updateEndDateFilter',
5050
(endDateFilter: Date | null) => ({endDateFilter})
5151
)
52+
export const updateYearFilter = action (
53+
'updateYearFilter',
54+
(yearFilter: number[] | null) => ({yearFilter})
55+
)
5256

5357
/**
5458
* Append a new grant to the current list of grants.

frontend/src/external/bcanSatchel/mutators.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
logoutUser,
66
fetchAllGrants,
77
updateFilter,
8-
updateStartDateFilter, updateEndDateFilter
8+
updateStartDateFilter, updateEndDateFilter, updateYearFilter
99
} from './actions';
1010
import { getAppStore } from './store';
1111

@@ -68,3 +68,8 @@ mutator(updateEndDateFilter, (actionMessage) => {
6868
const store = getAppStore();
6969
store.endDateFilter = actionMessage.endDateFilter;
7070
})
71+
72+
mutator(updateYearFilter, (actionMessage) => {
73+
const store = getAppStore();
74+
store.yearFilter = actionMessage.yearFilter;
75+
})

frontend/src/external/bcanSatchel/store.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export interface AppState {
1212
// TODO: should this be the ISODate type?
1313
startDateFilter: Date | null;
1414
endDateFilter: Date | null;
15-
}
15+
yearFilter:number[] | null;}
1616

1717
// Define initial state
1818
const initialState: AppState = {
@@ -23,6 +23,7 @@ const initialState: AppState = {
2323
filterStatus: null,
2424
startDateFilter: null,
2525
endDateFilter: null,
26+
yearFilter: null,
2627
};
2728

2829
const store = createStore<AppState>('appStore', initialState);

frontend/src/main-page/MainPage.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import Header from "./header/Header";
55
import Users from "./users/Users";
66

77
function MainPage() {
8+
9+
810
return (
911
<div className="w-full">
1012
<Header />
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { BarChart, Bar, CartesianGrid, XAxis, YAxis, Tooltip } from "recharts";
2+
import { observer } from "mobx-react-lite";
3+
import { useProcessGrantData } from "../../../main-page/grants/filter-bar/processGrantData";
4+
import { aggregateMoneyGrantsByYear, YearAmount } from "../grantCalculations";
5+
6+
const SampleChart: React.FC = observer(() => {
7+
const { grants } = useProcessGrantData();
8+
const data = aggregateMoneyGrantsByYear(grants, "status").map(
9+
(grant: YearAmount) => ({
10+
name: grant.year.toString(),
11+
active: grant.Active,
12+
inactive: grant.Inactive,
13+
})
14+
);
15+
16+
return (
17+
<div>
18+
<BarChart
19+
width={600}
20+
height={300}
21+
data={data}
22+
margin={{ top: 5, right: 20, bottom: 5, left: 0 }}
23+
>
24+
<CartesianGrid stroke="#aaa" strokeDasharray="5 5" />
25+
<Bar
26+
type="monotone"
27+
stackId="a"
28+
dataKey="active"
29+
fill="#90c4e5"
30+
strokeWidth={2}
31+
name="My data series name"
32+
/>
33+
<Bar
34+
type="monotone"
35+
stackId="a"
36+
dataKey="inactive"
37+
fill="#F58D5C"
38+
strokeWidth={2}
39+
name="My data series name"
40+
/>
41+
<XAxis dataKey="name" />
42+
<YAxis
43+
width="auto"
44+
label={{ value: "UV", position: "insideLeft", angle: -90 }}
45+
/>
46+
<Tooltip />
47+
</BarChart>
48+
</div>
49+
);
50+
});
51+
52+
export default SampleChart;
Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,27 @@
1+
2+
import DateFilter from "./DateFilter";
13
import "./styles/Dashboard.css";
4+
import { observer } from "mobx-react-lite";
5+
import SampleChart from "./Charts/SampleChart";
6+
import { useEffect } from "react";
7+
import { updateYearFilter, updateFilter, updateEndDateFilter, updateStartDateFilter } from "../../external/bcanSatchel/actions";
8+
9+
const Dashboard = observer(() => {
210

3-
function Dashboard() {
11+
// reset filters on initial render
12+
useEffect(() => {
13+
updateYearFilter(null);
14+
updateFilter(null);
15+
updateEndDateFilter(null);
16+
updateStartDateFilter(null);
17+
}, []);
418

519
return (
6-
<div className="dashboard-page">
20+
<div className="dashboard-page px-12 py-4">
21+
<DateFilter />
22+
<SampleChart/>
723
</div>
824
);
9-
}
25+
})
1026

1127
export default Dashboard;
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { useState, useEffect } from "react";
2+
import { updateYearFilter } from "../../external/bcanSatchel/actions";
3+
import { getAppStore } from "../../external/bcanSatchel/store";
4+
import { observer } from "mobx-react-lite";
5+
6+
const DateFilter: React.FC = observer(() => {
7+
const { allGrants, yearFilter } = getAppStore();
8+
9+
// Generate unique years dynamically from grants
10+
const uniqueYears = Array.from(
11+
new Set(
12+
allGrants.map((g) => new Date(g.application_deadline).getFullYear())
13+
)
14+
).sort((a, b) => a - b);
15+
16+
// Initialize selection from store or fallback to all years
17+
const [selectedYears, setSelectedYears] = useState<number[]>(uniqueYears);
18+
19+
// Keep local selection in sync if store changes
20+
useEffect(() => {
21+
(yearFilter && yearFilter.length) === 0
22+
? setSelectedYears(uniqueYears)
23+
: setSelectedYears(yearFilter ?? uniqueYears);
24+
}, [yearFilter, uniqueYears]);
25+
26+
// Update local store and state on checkbox change
27+
const handleCheckboxChange = (event: React.ChangeEvent<HTMLInputElement>) => {
28+
const year = Number(event.target.value);
29+
const checked = event.target.checked;
30+
31+
let updatedYears;
32+
if (checked) {
33+
updatedYears = [...selectedYears, year];
34+
} else {
35+
updatedYears = selectedYears.filter((y) => y !== year);
36+
}
37+
38+
setSelectedYears(updatedYears);
39+
updateYearFilter(updatedYears);
40+
};
41+
42+
return (
43+
<div className="flex flex-col space-y-2 mb-4">
44+
{uniqueYears.map((year) => (
45+
<label key={year} className="flex items-center space-x-2">
46+
<input
47+
type="checkbox"
48+
value={year}
49+
checked={selectedYears.includes(year)}
50+
onChange={handleCheckboxChange}
51+
defaultChecked={true}
52+
/>
53+
<span>{year}</span>
54+
</label>
55+
))}
56+
</div>
57+
);
58+
});
59+
60+
export default DateFilter;
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { Grant } from "../../../../middle-layer/types/Grant";
2+
3+
export type YearAmount = {
4+
year: number;
5+
[key: string]: number;
6+
};
7+
8+
/**
9+
* Aggregates total grant amounts by year, optionally grouped by a secondary key.
10+
*
11+
* @param grants - Array of grants.
12+
* @param groupBy - Optional secondary grouping key (e.g., "status" or "organization").
13+
* @returns Array of { year, [groupValue]: amount }.
14+
*/
15+
export function aggregateMoneyGrantsByYear(
16+
grants: Grant[],
17+
groupBy?: keyof Grant
18+
): YearAmount[] {
19+
const grouped: Record<number, Record<string, number>> = {};
20+
21+
for (const grant of grants) {
22+
const year = new Date(grant.application_deadline).getUTCFullYear();
23+
const groupValue = groupBy ? String(grant[groupBy] ?? "Unknown") : "All";
24+
25+
grouped[year] ??= {};
26+
grouped[year][groupValue] = (grouped[year][groupValue] ?? 0) + grant.amount;
27+
}
28+
29+
return Object.entries(grouped)
30+
.map(([year, groups]) => ({
31+
year: Number(year),
32+
...groups,
33+
}))
34+
.sort((a, b) => a.year - b.year);
35+
}
36+
37+
/**
38+
* Aggregates distinct grant counts by year, optionally grouped by a secondary key.
39+
*
40+
* @param grants - Array of grants.
41+
* @param groupBy - Optional secondary grouping key (e.g., "status" or "organization").
42+
* @returns Array of { year, [groupValue]: count }.
43+
*/
44+
export function aggregateCountGrantsByYear(
45+
grants: Grant[],
46+
groupBy?: keyof Grant
47+
): YearAmount[] {
48+
const grouped: Record<number, Record<string, Set<number>>> = {};
49+
50+
for (const grant of grants) {
51+
const year = new Date(grant.application_deadline).getUTCFullYear();
52+
const groupValue = groupBy ? String(grant[groupBy] ?? "Unknown") : "All";
53+
54+
grouped[year] ??= {};
55+
grouped[year][groupValue] ??= new Set<number>();
56+
grouped[year][groupValue].add(grant.grantId);
57+
}
58+
59+
return Object.entries(grouped)
60+
.map(([year, groups]) => {
61+
const counts: Record<string, number> = {};
62+
for (const [key, ids] of Object.entries(groups)) {
63+
counts[key] = ids.size;
64+
}
65+
return { year: Number(year), ...counts };
66+
})
67+
.sort((a, b) => a.year - b.year);
68+
}

0 commit comments

Comments
 (0)