Skip to content

Commit f79680c

Browse files
authored
Merge pull request #116 from Berkeley-CS61B/stats
Stats
2 parents c50ea04 + b8605e7 commit f79680c

File tree

8 files changed

+1051
-2
lines changed

8 files changed

+1051
-2
lines changed

package-lock.json

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

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
"react-dom": "18.2.0",
5050
"react-icons": "^4.6.0",
5151
"react-table": "^7.8.0",
52+
"recharts": "^2.5.0",
5253
"superjson": "^1.12.1",
5354
"zod": "^3.18.0"
5455
},
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import { useEffect, useState } from 'react';
2+
import { Flex, FormControl } from '@chakra-ui/react';
3+
import { Select } from 'chakra-react-select';
4+
import { TicketStats } from '../../server/trpc/router/stats';
5+
import { LineChart, Line, CartesianGrid, Legend, XAxis, YAxis, Tooltip, ResponsiveContainer } from 'recharts';
6+
import { TimeRange } from './StatsView';
7+
import { TicketStatus } from '@prisma/client';
8+
import { computeMean, computeMedian, helpTime, resolveTime } from '../../utils/utils';
9+
10+
export interface StatsGraphProps {
11+
timeRange: TimeRange | undefined;
12+
stats: TicketStats[];
13+
}
14+
15+
export enum StatType {
16+
HELP_TIME = "helpTime",
17+
RESOLVE_TIME = "resolveTime",
18+
NUMBER_OF_TICKETS = "numberOfTickets"
19+
}
20+
21+
const statTypeOptions = [
22+
{
23+
label: "Help Time",
24+
value: StatType.HELP_TIME
25+
},
26+
{
27+
label: "Resolve Time",
28+
value: StatType.RESOLVE_TIME
29+
},
30+
{
31+
label: "Number of Tickets",
32+
value: StatType.NUMBER_OF_TICKETS
33+
}
34+
]
35+
36+
const StatsGraph = (props: StatsGraphProps) => {
37+
const { timeRange, stats } = props;
38+
const [statType, setStatType] = useState(statTypeOptions[0]);
39+
const [data, setData] = useState<any[]>([]);
40+
41+
const binStatsByTime = () => {
42+
if (!timeRange) return {};
43+
44+
const filteredStatsInRange = stats.filter(s => {
45+
return s.resolvedAt && timeRange.startTime <= s.resolvedAt && s.resolvedAt < timeRange.endTime;
46+
});
47+
48+
const bins: {[key: string]: TicketStats[]} = {};
49+
for (let t = timeRange.startTime; t <= timeRange.endTime; t = timeRange.type!.increment(t)) {
50+
const start = t;
51+
const end = timeRange.type!.increment(start);
52+
bins[timeRange.type!.formatString(start)] = filteredStatsInRange.filter(s => {
53+
return s.resolvedAt && start <= s.resolvedAt && s.resolvedAt < end;
54+
});
55+
}
56+
return bins;
57+
};
58+
59+
const getResolveTimeStats = (bins: {[key: string]: TicketStats[]}) => {
60+
const data = Object.keys(bins).map(b => ({
61+
name: b,
62+
data: bins[b]!.filter(s => s.createdAt && s.resolvedAt).map(t => resolveTime(t)).sort()
63+
}));
64+
65+
return data.map(d => ({
66+
name: d.name,
67+
meanResolveTime: computeMean(d.data),
68+
medianResolveTime: computeMedian(d.data)
69+
}));
70+
};
71+
72+
const getHelpTimeStats = (bins: {[key: string]: TicketStats[]}) => {
73+
const data = Object.keys(bins).map(b => ({
74+
name: b,
75+
data: bins[b]!.filter(s => s.createdAt && s.resolvedAt).map(t => helpTime(t)).sort((a, b) => a - b)
76+
}));
77+
78+
return data.map(d => ({
79+
name: d.name,
80+
meanHelpTime: computeMean(d.data),
81+
medianHelpTime: computeMedian(d.data)
82+
}));
83+
};
84+
85+
const getNumberOfTicketStats = (bins: {[key: string]: TicketStats[]}) => {
86+
return Object.keys(bins).map(b => ({
87+
name: b,
88+
numberOfTickets: bins[b]?.length,
89+
numberOfUnresolvedTickets: bins[b]?.filter(s => s.status === TicketStatus.CLOSED).length
90+
}));
91+
};
92+
93+
const getGraphLines = () => {
94+
if (statType === undefined) {
95+
return <></>;
96+
}
97+
switch (statType.value) {
98+
case StatType.HELP_TIME:
99+
return <>
100+
<Line type="monotone" dataKey="meanHelpTime" name="Mean Help Time" stroke="#3486eb" />
101+
<Line type="monotone" dataKey="medianHelpTime" name="Median Help Time" stroke="#8884d8" />
102+
</>;
103+
case StatType.RESOLVE_TIME:
104+
return <>
105+
<Line type="monotone" dataKey="meanResolveTime" name="Mean Resolve Time" stroke="#3486eb" />
106+
<Line type="monotone" dataKey="medianResolveTime" name="Median Resolve Time" stroke="#8884d8" />
107+
</>;
108+
case StatType.NUMBER_OF_TICKETS:
109+
return <>
110+
<Line type="monotone" dataKey="numberOfTickets" name="Number of Tickets" stroke="#3486eb" />
111+
<Line type="monotone" dataKey="numberOfUnresolvedTickets" name="Number of Unresolved Tickets" stroke="#8884d8" />
112+
</>;
113+
default:
114+
return <></>
115+
}
116+
}
117+
118+
useEffect(() => {
119+
if (statType !== undefined && statType.value === StatType.HELP_TIME) {
120+
setData(getHelpTimeStats(binStatsByTime()));
121+
} else if (statType !== undefined && statType.value === StatType.RESOLVE_TIME) {
122+
setData(getResolveTimeStats(binStatsByTime()))
123+
} else if (statType !== undefined && statType.value === StatType.NUMBER_OF_TICKETS) {
124+
setData(getNumberOfTicketStats(binStatsByTime()));
125+
}
126+
}, [timeRange, statType]);
127+
128+
return (
129+
<Flex direction="column" w="100%" h="100%">
130+
<FormControl w="100%" mt={6} isRequired>
131+
<Flex direction={"row"} justifyContent={"space-between"}>
132+
<Select value={statType} onChange={val => setStatType(val ?? undefined)} options={statTypeOptions}
133+
chakraStyles={{
134+
container: (provided) => ({
135+
...provided,
136+
width: "100%",
137+
margin: "0 10px 0 0"
138+
})
139+
}} />
140+
</Flex>
141+
</FormControl>
142+
<ResponsiveContainer width="100%" height={400}>
143+
<LineChart width={400} height={400} data={data} margin={{ top: 20, right: 20, bottom: 5, left: 0 }}>
144+
<CartesianGrid stroke="#ccc" strokeDasharray="5 5" />
145+
<Legend verticalAlign="top" height={36}/>
146+
<XAxis dataKey="name" />
147+
<YAxis />
148+
<Tooltip labelStyle={{ color: "#454545" }}/>
149+
{ getGraphLines() }
150+
</LineChart>
151+
</ResponsiveContainer>
152+
</Flex>
153+
);
154+
}
155+
156+
export default StatsGraph;
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { Box, Flex, Text } from '@chakra-ui/react';
2+
import { TicketStats } from '../../server/trpc/router/stats';
3+
import { TimeRange } from './StatsView';
4+
import { computeMean, computeMedian, helpTime, resolveTime } from '../../utils/utils';
5+
6+
export interface StatsGraphProps {
7+
timeRange: TimeRange | undefined;
8+
stats: TicketStats[];
9+
}
10+
11+
const StatsPanel = (props: StatsGraphProps) => {
12+
const { timeRange, stats } = props;
13+
14+
const filterTicketsInRange = () => {
15+
if (timeRange === undefined) return [];
16+
return stats.filter(s => {
17+
return s.resolvedAt && timeRange.startTime <= s.resolvedAt && s.resolvedAt < timeRange.endTime;
18+
});
19+
};
20+
const filteredStats = filterTicketsInRange();
21+
22+
const helpedTickets = (stats: TicketStats[]) => {
23+
return stats.filter(s => s.helpedAt).length;
24+
}
25+
26+
return (
27+
<Flex direction="column" w="100%" h="100%">
28+
<Box><Text as="b">Tickets Helped:</Text> {helpedTickets(filteredStats)}</Box>
29+
<Box><Text as="b">Mean Resolve Time:</Text> {computeMean(filteredStats.map(s => resolveTime(s)))} minutes</Box>
30+
<Box><Text as="b">Median Resolve Time:</Text> {computeMedian(filteredStats.map(s => resolveTime(s)))} minutes</Box>
31+
<Box><Text as="b">Mean Help Time:</Text> {computeMean(filteredStats.map(s => helpTime(s)))} minutes</Box>
32+
<Box><Text as="b">Median Help Time:</Text> {computeMedian(filteredStats.map(s => helpTime(s)))} minutes</Box>
33+
</Flex>
34+
);
35+
}
36+
37+
export default StatsPanel;

0 commit comments

Comments
 (0)