From bad4d9d3fd736c20c34dfa0eb74e166b22dba3fe Mon Sep 17 00:00:00 2001 From: GreenAsJade Date: Thu, 21 Nov 2024 15:25:36 +1030 Subject: [PATCH] Redo the group graphs to match the new data relating to the new voting policies --- .../ReportsCenterCMDashboard.tsx | 232 +++++++++++++++++- 1 file changed, 227 insertions(+), 5 deletions(-) diff --git a/src/views/ReportsCenter/ReportsCenterCMDashboard.tsx b/src/views/ReportsCenter/ReportsCenterCMDashboard.tsx index 62dcab1a59..1895aee7e2 100644 --- a/src/views/ReportsCenter/ReportsCenterCMDashboard.tsx +++ b/src/views/ReportsCenter/ReportsCenterCMDashboard.tsx @@ -39,10 +39,21 @@ interface ReportCount { non_consensus: number; } +interface ReportAlignmentCount { + date: string; + escalated: number; + unanimous: number; + non_unanimous: number; +} + interface CMVotingOutcomeData { [reportType: string]: ReportCount[]; } +interface CMGroupVotingAlignmentData { + [reportType: string]: ReportAlignmentCount[]; +} + interface IndividualCMVotingOutcomeData { user_id: number; vote_data: CMVotingOutcomeData; @@ -62,13 +73,14 @@ function startOfWeek(the_date: Date): Date { // TBD: it might be nice if this number was dynamically provided by the server, but // we are already possibly hitting it hard for these rollups -const EXPECTED_MAX_WEEKLY_CM_REPORTS = 160; -const Y_STEP_SIZE = 40; // must divide evenly into EXPECTED_MAX_WEEKLY_CM_REPORTS +const EXPECTED_MAX_WEEKLY_CM_REPORTS = 200; +const Y_STEP_SIZE = 40; // must divide nicely into EXPECTED_MAX_WEEKLY_CM_REPORTS interface CMVoteCountGraphProps { vote_data: ReportCount[]; period: number; } + const CMVoteCountGraph = ({ vote_data, period }: CMVoteCountGraphProps): JSX.Element => { if (!vote_data) { vote_data = []; @@ -283,10 +295,220 @@ const CMVoteCountGraph = ({ vote_data, period }: CMVoteCountGraphProps): JSX.Ele ); }; +interface CMVotingGroupGraphProps { + vote_data: ReportAlignmentCount[]; + period: number; +} + +const CMVotingGroupGraph = ({ vote_data, period }: CMVotingGroupGraphProps): JSX.Element => { + if (!vote_data) { + vote_data = []; + } + + const aggregateDataByWeek = React.useMemo(() => { + const aggregated: { + [key: string]: { + escalated: number; + unanimous: number; + non_unanimous: number; + total: number; + }; + } = {}; + + vote_data.forEach(({ date, escalated, unanimous, non_unanimous }) => { + const weekStart = startOfWeek(new Date(date)).toISOString().slice(0, 10); // Get week start and convert to ISO string for key + + if (!aggregated[weekStart]) { + aggregated[weekStart] = { escalated: 0, unanimous: 0, non_unanimous: 0, total: 0 }; + } + aggregated[weekStart].escalated += escalated; + aggregated[weekStart].unanimous += unanimous; + aggregated[weekStart].non_unanimous += non_unanimous; + aggregated[weekStart].total += unanimous + non_unanimous; + }); + + return Object.entries(aggregated).map(([date, counts]) => ({ + date, + ...counts, + })); + }, [vote_data]); + + const totals_data = React.useMemo(() => { + return [ + { + id: "unanimous", + data: dropCurrentPeriod( + aggregateDataByWeek.map((week) => ({ + x: week.date, + y: week.unanimous, + })), + ), + }, + { + id: "escalated", + data: dropCurrentPeriod( + aggregateDataByWeek.map((week) => ({ + x: week.date, + y: week.escalated, + })), + ), + }, + { + id: "non-unanimous", + data: dropCurrentPeriod( + aggregateDataByWeek.map((week) => ({ + x: week.date, + y: week.non_unanimous, + })), + ), + }, + ]; + }, [aggregateDataByWeek]); + + const percent_data = React.useMemo( + () => [ + { + id: "unanimous", + data: aggregateDataByWeek.map((week) => ({ + x: week.date, + y: week.unanimous / week.total, + })), + }, + { + id: "escalated", + data: aggregateDataByWeek.map((week) => ({ + x: week.date, + y: week.escalated / week.total, + })), + }, + { + id: "non-unanimous", + data: aggregateDataByWeek.map((week) => ({ + x: week.date, + y: week.non_unanimous / week.total, + })), + }, + ], + [aggregateDataByWeek], + ); + + const chart_theme = + data.get("theme") === "light" // (Accessible theme TBD - this assumes accessible is dark for now) + ? { + /* nivo defaults work well with our light theme */ + } + : { + text: { fill: "#FFFFFF" }, + tooltip: { container: { color: "#111111" } }, + grid: { line: { stroke: "#444444" } }, + }; + + const line_colors = { + unanimous: "rgba(0, 128, 0, 1)", // green + escalated: "rgba(255, 165, 0, 1)", // orange + "non-unanimous": "rgba(255, 0, 0, 1)", // red + }; + + if (!totals_data[0].data.length) { + return
No activity yet
; + } + + return ( +
+
+ line_colors[id as keyof typeof line_colors]} + animate + curve="monotoneX" + enablePoints={false} + enableSlices="x" + axisBottom={{ + format: "%d %b %g", + tickValues: "every week", + }} + xFormat="time:%Y-%m-%d" + xScale={{ + type: "time", + min: format( + startOfWeekDateFns(subDays(new Date(), period), { weekStartsOn: 1 }), + "yyyy-MM-dd", + ), + format: "%Y-%m-%d", + useUTC: false, + precision: "day", + }} + axisLeft={{ + tickValues: Array.from( + { length: EXPECTED_MAX_WEEKLY_CM_REPORTS / Y_STEP_SIZE + 1 }, + (_, i) => i * Y_STEP_SIZE, + ), + }} + yScale={{ + stacked: false, + type: "linear", + min: 0, + max: EXPECTED_MAX_WEEKLY_CM_REPORTS, + }} + margin={{ + bottom: 40, + left: 60, + right: 40, + top: 5, + }} + theme={chart_theme} + /> +
+
+ line_colors[id as keyof typeof line_colors]} + animate + curve="monotoneX" + enablePoints={false} + enableSlices="x" + axisBottom={{ + format: "%d %b %g", + tickValues: "every week", + }} + xFormat="time:%Y-%m-%d" + xScale={{ + type: "time", + min: format( + startOfWeekDateFns(subDays(new Date(), period), { weekStartsOn: 1 }), + "yyyy-MM-dd", + ), + format: "%Y-%m-%d", + useUTC: false, + precision: "day", + }} + axisLeft={{ + format: (d) => `${Math.round(d * 100)}%`, // Format ticks as percentages + tickValues: 6, + }} + yFormat=" >-.0p" + yScale={{ + stacked: false, + type: "linear", + max: 1, + }} + margin={{ + bottom: 40, + left: 60, + right: 40, + top: 5, + }} + theme={chart_theme} + /> +
+
+ ); +}; + export function ReportsCenterCMDashboard(): JSX.Element { const user = useUser(); const [selectedTabIndex, setSelectedTabIndex] = React.useState(user.moderator_powers ? 0 : 1); - const [vote_data, setVoteData] = React.useState(null); + const [vote_data, setVoteData] = React.useState(null); const [users_data, setUsersData] = React.useState(null); // `Tabs` isn't expecting the possibility that the initial tab is not zero. @@ -308,7 +530,7 @@ export function ReportsCenterCMDashboard(): JSX.Element { const fetchVoteData = () => { get(`moderation/cm_voting_outcomes`) .then((response) => { - const fetchedData: CMVotingOutcomeData = response; + const fetchedData: CMGroupVotingAlignmentData = response; setVoteData(fetchedData); }) .catch((err) => { @@ -361,7 +583,7 @@ export function ReportsCenterCMDashboard(): JSX.Element {

{report_type}

{vote_data[report_type] ? ( -